Quantcast
Channel: CSDN博客推荐文章
Viewing all 35570 articles
Browse latest View live

Python3与OpenCV3.3 图像处理(四)--色彩空间

$
0
0

一、本节简述

本节讲解图像色彩空间的处理和色彩空间的基础知识

二、色彩空间基础知识

什么是色彩空间,人们建立了多种色彩模型,以一维、二维、三维甚至四维空间坐标来表示某一色彩,这种坐标系统所能定义的色彩范围即色彩空间

色彩空间有很多,但是常用的色彩空间一共5种:RGB、HSV、HSI、YCrCb、YUV,简单讲一下这5个色彩空间。

  • RGB就不用多说了,RGB是我门经常用到的;
  • HSV也称六角锥体模型,是根据颜色的直观特性创建的一种颜色空间,这个颜色空间是本节课讲解的一个重点。
  • HSI是从人的视觉系统出发,用色调(  Hue  )、色饱和  度(  Saturation  或  Chroma  )和亮度(  Intensity  或  Brightness  )来描述颜色。  HSI  颜色空间可以用一个圆  锥空间模型来描述
  • YCrCb主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视,这个可以用来检测皮肤和检测人脸
  • YUV是被欧洲电视系统所采用的一种颜色编码方法(属于PAL),是PAL和SECAM模拟彩色电视制式采用的颜色空间。


三、色彩空间的转换

OpenCV提供多种将图像的色彩空间转换为另一个色彩空间的方法,转换方法的方法名一般为 “原色彩空间2需要转化的色彩空间”,下面我们以图像的RGB色彩转换为其他四种色彩空间和GRAY色彩空间。

def ColorSpace(image):
    """
    色彩空间转化
    RGB转换为其他色彩空间
    """
    gray=cv.cvtColor(image,cv.COLOR_BGR2GRAY)
    cv.imshow("gray",gray)
    hsv=cv.cvtColor(image,cv.COLOR_RGB2HSV)
    cv.imshow("hsv",hsv)
    yuv=cv.cvtColor(image,cv.COLOR_RGB2YUV)
    cv.imshow("yuv",yuv)
    ycrcb=cv.cvtColor(image,cv.COLOR_RGB2YCrCb)
    cv.imshow("ycrcb",ycrcb)


四、标记图像中的特定颜色

一般对颜色空间的图像进行有效处理都是在HSV空间进行的,然后对于基本色中对应的HSV分量需要给定一个严格的范围,下面是网友通过实验计算的模糊范围(准确的范围在网上都没有给出)。

H:  0 180

S:  0 255

V:  0 255

以下是不同颜色的HSV最大最小的范围:

以下代码是标注出图像中的黑色部分,黑色部分将以白色显示,其他颜色部分将以黑色显示,颜色标注OpenCV 提供了一个方法,inRange()。该方法提供三个参数,第一个参数是图像色彩空间即hsv值,第二个参数是hsv的最小查找范围,第三个参数是hsv的最大查找范围。代码运行后,将会标注出图像的黑色部分。

capture=cv.VideoCapture("test.mp4")
    while(True):
        ret,frame=capture.read()
        if ret==False:
            break;
        hsv=cv.cvtColor(frame,cv.COLOR_BGR2HSV)
        lower_hsv=np.array([0,0,0])
        upperb_hsv = np.array([180, 255, 46])
        mask=cv.inRange(hsv,lowerb=lower_hsv,upperb=upperb_hsv)
        cv.imshow("video_mask", mask)
        cv.imshow("video",frame)
        c=cv.waitKey(40)
        if c==27:
            break;


作者:gangzhucoll 发表于2017/11/19 17:25:03 原文链接
阅读:22 评论:0 查看评论

LWC 59:729. My Calendar I

$
0
0

LWC 59:729. My Calendar I

传送门:729. My Calendar I

Problem:

Implement a MyCalendar class to store your events. A new event can be added if adding the event will not cause a double booking.

Your class will have the method, book(int start, int end). Formally, this represents a booking on the half open interval [start, end), the range of real numbers x such that start <= x < end.

A double booking happens when two events have some non-empty intersection (ie., there is some time that is common to both events.)

For each call to the method MyCalendar.book, return true if the event can be added to the calendar successfully without causing a double booking. Otherwise, return false and do not add the event to the calendar.

Your class will be called like this: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

Example 1:

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(15, 25); // returns false
MyCalendar.book(20, 30); // returns true
Explanation:
The first event can be booked. The second can’t because time 15 is already booked by another event.
The third event can be booked, as the first event takes every time less than 20, but not including 20.

Note:

  • The number of calls to MyCalendar.book per test case will be at most 1000.
  • In calls to MyCalendar.book(start, end), start and end are integers in the range [0, 10^9].

思路:
判断新加入的区间是否与原来的重叠,总共有三种重叠情况,左相交,右相交和包含,如下图:
alt text

当然这里还有一种情况,即黄色块完全包含于红色块,但这种情况已经包含在了A情况或者B情况中,所以不需要重复判断。

代码如下:

class MyCalendar {

    class Interval{
        int s;
        int e;

        Interval(int s, int e){
            this.s = s;
            this.e = e;
        }
    }

    List<Interval> mem;

    public MyCalendar() {
        mem = new ArrayList<>();
    }

    public boolean book(int start, int end) {
        Interval candicate = new Interval(start, end);
        if (overlap(candicate)) {
            return false;
        }
        else {
            mem.add(candicate);
            return true;
        }
    }

    public boolean overlap(Interval cand) {
        for (Interval tmp : mem) {
            if (overlap(tmp, cand)) return true;
        }
        return false;
    }

    boolean overlap(Interval a, Interval b) {
        if (b.s >= a.s && b.s < a.e || b.e <= a.e && b.e > a.s || b.s <= a.s && b.e >= a.e) {
            return true;
        }
        return false;
    }
}
作者:u014688145 发表于2017/11/19 17:34:44 原文链接
阅读:24 评论:0 查看评论

[bzoj1834][网络流]network 网络扩容

$
0
0

Description

给定一张有向图,每条边都有一个容量C和一个扩容费用W。这里扩容费用是指将容量扩大1所需的费用。求: 1、 在不扩容的情况下,1到N的最大流;
2、 将1到N的最大流增加K所需的最小扩容费用。

Input

输入文件的第一行包含三个整数N,M,K,表示有向图的点数、边数以及所需要增加的流量。
接下来的M行每行包含四个整数u,v,C,W,表示一条从u到v,容量为C,扩容费用为W的边。

Output

输出文件一行包含两个整数,分别表示问题1和问题2的答案。

Sample Input

5 8 2
1 2 5 8
2 5 9 9
5 1 6 2
5 1 1 8
1 2 8 7
2 5 4 9
1 2 1 1
1 4 2 1

Sample Output

13 19

HINT

30%的数据中,N<=100

100%的数据中,N<=1000,M<=5000,K<=10

题解

第一问不用说吧。。网络流模板大家都会我就不弄了
第二问,实际上就是在第一问的残余网络上重新建图,跑费用流
怎么建图??可以想到,如果最大流要+K,那么残余网络图中至少有一条权为K的增广路径。那么,我们在残留网络上建图。
ins(x,y,c,w)表示x~y有一条权为c,值为w的边。要不要删原图了??当然不用啦
跑spfa的时候,如果x->y原本有残余网络,那么一定会选择值较小的边走对吧。所以不能删原图。
至于x->y没有残余网络了,那就在新图上跑咯
顺带学了一下费用流模板

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
using namespace std;
struct node{int x,y,c,d,next,other;}a[211000];int len,last[110000];
void ins(int x,int y,int c,int d)
{
    int k1,k2;
    k1=++len;
    a[len].x=x;a[len].y=y;a[len].c=c;a[len].d=d;
    a[len].next=last[x];last[x]=len;

    k2=++len;
    a[len].x=y;a[len].y=x;a[len].c=0;a[len].d=-d;
    a[len].next=last[y];last[y]=len;

    a[k1].other=k2;a[k2].other=k1;
}
int list[11000];
int head,tail,st,ed;
int n,m,upd;
int h[11000];
bool bt_h()
{
    head=1;tail=2;
    memset(h,0,sizeof(h));
    h[st]=1;list[1]=st;
    while(head!=tail)
    {
        int x=list[head];
        for(int k=last[x];k;k=a[k].next)
        {
            int y=a[k].y;
            if(h[y]==0 && a[k].c>0)
            {
                h[y]=h[x]+1;
                list[tail++]=y;
            }
        }
        head++;
    }
    if(h[ed]==0)return false;
    return true;
}
int find_flow(int x,int f)
{
    if(x==ed)return f;
    int s=0,t;
    for(int k=last[x];k;k=a[k].next)
    {
        int y=a[k].y;
        if(h[y]==h[x]+1 && a[k].c>0 && s<f)
        {
            s+=(t=find_flow(y,min(a[k].c,f-s)));
            a[k].c-=t;a[a[k].other].c+=t;
        }
    }
    if(s==0)h[x]=0;
    return s;
}
int pos[21000],tmp[21000];
int d[21000],v[21000];
bool spfa()
{
    for(int i=1;i<=n+1;i++)d[i]=999999999;
    d[st]=0;
    memset(v,false,sizeof(v));v[st]=true;
    list[1]=st;head=1;tail=2;
    while(head!=tail)
    {
        int x=list[head];
        for(int k=last[x];k;k=a[k].next)
        {
            int y=a[k].y;
            if(a[k].c>0 && d[y]>d[x]+a[k].d)
            {
                pos[y]=x;tmp[y]=k;
                d[y]=d[x]+a[k].d;
                if(v[y]==false)
                {
                    v[y]=true;
                    list[tail++]=y;
                    if(tail==n+2)tail=1;
                }
            }
        }
        head++;
        if(head==n+2)head=1;
        v[x]=false;
    }
    if(d[n+1]==999999999)return false;
    return true;
}
int p[21000],q[21000],g[21000],co[21000];
int main()
{
    scanf("%d%d%d",&n,&m,&upd);
    len=0;memset(last,0,sizeof(last));
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d%d",&p[i],&q[i],&g[i],&co[i]);
        ins(p[i],q[i],g[i],0);
    }
    st=1;ed=n;
    int ans=0;
    while(bt_h())ans+=find_flow(st,999999999);
    printf("%d ",ans);
    for(int i=1;i<=m;i++)ins(p[i],q[i],upd,co[i]);
    ins(n,n+1,upd,0);
    ans=0;
    while(spfa())
    {
        int tp=n+1,minn=999999999;
        while(tp!=st)
        {
            minn=min(minn,a[tmp[tp]].c);
            tp=pos[tp];
        }
        tp=n+1;
        while(tp!=st)
        {
            ans+=minn*a[tmp[tp]].d;
            a[tmp[tp]].c-=minn;a[a[tmp[tp]].other].c+=minn;
            tp=pos[tp];
        }
    }
    printf("%d\n",ans);
    return 0;
}
作者:Rose_max 发表于2017/11/19 18:13:18 原文链接
阅读:39 评论:0 查看评论

MyBatis的级联查询(两种方式)

$
0
0



与上次唯一不同的一下几个类

Department.java

package com.cn.zhu.bean;

public class Department {
	private 	Integer  id;
	private  String  departmentName;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public String getDepartmentName() {
		return departmentName;
	}
	public void setDepartmentName(String departmentName) {
		this.departmentName = departmentName;
	}
	@Override
	public String toString() {
		return "Department [departmentName=" + departmentName + ", id=" + id
				+ "]";
	}
	
}
Employee.java
package com.cn.zhu.bean;

import org.apache.ibatis.type.Alias;

@Alias("emp")
public class Employee {
	private Integer id;
	private String lastName;
	private String email;
	private String gender;
	private Department dept;
	
	public Employee() {
		super();
		// TODO Auto-generated constructor stub
	}
	
	public Employee(Integer id, String lastName, String email, String gender) {
		super();
		this.id = id;
		this.lastName = lastName;
		this.email = email;
		this.gender = gender;
	}

	
	public Department getDept() {
		return dept;
	}

	public void setDept(Department dept) {
		this.dept = dept;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getGender() {
		return gender;
	}
	public void setGender(String gender) {
		this.gender = gender;
	}
	@Override
	public String toString() {
		return "Employee [email=" + email + ", gender=" + gender + ", id=" + id
		+ ", lastName=" + lastName + "]";
	}

}

EmployeeMapperPlus.java

package com.cn.mybatis.dao;

import java.util.List;
import java.util.Map;


import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Param;

import com.cn.zhu.bean.Employee;

public interface EmployeeMapperPlus {
	public Employee getEmpById(Integer id);
	public Employee getEmpAndDept(Integer id);
 }

EmployeeMapperPlus.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cn.mybatis.dao.EmployeeMapperPlus">

	<!--
		自定义某个javaBean的封装规则 id 定义主键会底层有优化 type: 自定义规则的java类型 property :
		唯一id方便引用
	-->
	<!--
		<resultMap type="com.cn.zhu.bean.Employee" id="MySimpleEmp"> 指定主键的封装规则
		column 指定哪一列 property 指定对应的javaBean属性 <id column="id" property="id"/>
		定义普通列封装 <result column="last_name" property="lastName"/> 其他不指定的列会自动封装
		,我们只要写resultMap就把全部的映射规则都写上 <result column="email" property="email"/>
		<result column="gender" property="gender"/> </resultMap>
	--><!-- public Employee getEmpById(Integer id); -->
	<!-- resultMap: 自定义结果集映射规则 -->
	<!--
		<select id="getEmpById" resultMap="MyEmp"> select * from tal_employee
		where id=#{id} </select>
	-->
	<!--
		场景一 : 查询Employee的同时查询员工对应的部门 Employee ===department 一个员工有与之对应的部门信息
	-->

	<resultMap type="com.cn.zhu.bean.Employee" id="MyDifEmp">
		<!--
			指定主键的封装规则 column 指定哪一列 property 指定对应的javaBean属性
		-->
		<!-- 
                  联合查询 :级联属性封装结果
        -->
		<id column="id" property="id" />
		<!-- 定义普通列封装 -->
		<result column="last_name" property="lastName" />
		<!-- 其他不指定的列会自动封装 ,我们只要写resultMap就把全部的映射规则都写上 -->
		<result column="gender" property="gender" />
		<result column="did" property="dept.id" />
		<result column="dept_name" property="dept.departmentName" />
	</resultMap>

  <!--  使用association  定义单个对象的封装规则 -->
	<resultMap type="com.cn.zhu.bean.Employee" id="MyDifEmp2">
		<id column="id" property="id" />
		<!-- 定义普通列封装 -->
		<result column="last_name" property="lastName" />
		<!-- 其他不指定的列会自动封装 ,我们只要写resultMap就把全部的映射规则都写上 -->
		<result column="gender" property="gender" />
		<!--association  可以指定联合的javaBean对象
		  property 指定哪个是联合的对象
		  javaType: 指定这个属性对象的类型
		  
		  -->
		<association property="dept" javaType="com.cn.zhu.bean.Department">
		  <id column="did" property="id"/>
		   <result column="dept_name" property="departmentName"/>
		</association>
	</resultMap>


	<!-- public Employee getEmpAndDept(Integer id); -->
	<select id="getEmpAndDept" resultMap="MyDifEmp2">
		SELECT e.id id,e.last_name last_name ,e.gender gender,e.d_id d_id,
		d.id did,d.dept_name dept_name from tbl_employee e,tbl_dept d
		where e.d_id=d.id and e.id=#{id}
	</select>
</mapper>

以上是两种映射封装结果


增加EmployeeMapperPlus.xml 后,一定要在mybatis-config.xml里加上这个配置文件的声明,

<mappers>
		<mapper resource="mybatis/mapper/EmployeeMapperPlus.xml" />
	</mappers>
另外sql语句一定要正确,可以先在navict数据库图形化管理工具中,测试sql



下面编写测试类(这里直接写测试方法吧):详细请看映射一

@Test
	public void test05() throws IOException{
		SqlSessionFactory sqlsessionFactory=getSqlSessionFactory();
		// 1  获取到的sqlsession不会自动提交数据
		SqlSession openSession=sqlsessionFactory.openSession();

		try{
			
			EmployeeMapperPlus mapper=openSession.getMapper(EmployeeMapperPlus.class);
            Employee empAndDept=mapper.getEmpAndDept(1);
            System.out.println(empAndDept);
            System.out.println(empAndDept.getDept());
		}finally{
			openSession.commit();
		}
	}

测试完成



接下来,会写分布查询,请看下一篇文章

作者:zhupengqq 发表于2017/11/19 18:24:14 原文链接
阅读:3 评论:0 查看评论

LWC 59:731. My Calendar II

$
0
0

LWC 59:731. My Calendar II

传送门:729. My Calendar II

Problem:

Implement a MyCalendarTwo class to store your events. A new event can be added if adding the event will not cause a triple booking.

Your class will have one method, book(int start, int end). Formally, this represents a booking on the half open interval [start, end), the range of real numbers x such that start <= x < end.

A triple booking happens when three events have some non-empty intersection (ie., there is some time that is common to all 3 events.)

For each call to the method MyCalendar.book, return true if the event can be added to the calendar successfully without causing a triple booking. Otherwise, return false and do not add the event to the calendar.

Your class will be called like this: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

Example 1:

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(50, 60); // returns true
MyCalendar.book(10, 40); // returns true
MyCalendar.book(5, 15); // returns false
MyCalendar.book(5, 10); // returns true
MyCalendar.book(25, 55); // returns true
Explanation:
The first two events can be booked. The third event can be double booked.
The fourth event (5, 15) can’t be booked, because it would result in a triple booking.
The fifth event (5, 10) can be booked, as it does not use time 10 which is already double booked.
The sixth event (25, 55) can be booked, as the time in [25, 40) will be double booked with the third event;
the time [40, 50) will be single booked, and the time [50, 55) will be double booked with the second event.

Note:

  • The number of calls to MyCalendar.book per test case will be at most 1000.
  • In calls to MyCalendar.book(start, end), start and end are integers in the range [0, 10^9].

思路:
水过,当新加入一个区间时,如果在该区间内,出现重复的区间时,则认为是a triple booking,关键在于如何求出两个区间的重叠区间,画个图就能明白,重叠区间为:

s = max(s1, s2), si = 区间i的开始坐标
e = min(e1, e2), ei = 区间i的结束坐标

首先排除一部分冗余区间,左侧的与候选区间的不重叠区间和右侧的不重叠区间均可以排除,而中间那一部分,则需要以O(n2)的时间复杂度来计算每两个区间的重叠区域,是否与候选区域发生重叠。

代码如下:

class MyCalendarTwo {

    class Interval implements Comparable<Interval>{
        int s;
        int e;
        Interval(int s, int e){
            this.s = s;
            this.e = e;
        }

        @Override
        public int compareTo(Interval o) {
            return this.s == o.s ? this.e - o.e : this.s - o.s;
        }

        @Override
        public String toString() {
            return "[" + s + ", " + e +"]";
        }
    }

    List<Interval> mem;

    public MyCalendarTwo() {
        mem = new ArrayList<>();
    }

    public boolean book(int start, int end) {
        Interval candicate = new Interval(start, end);
        if (tripleOverlap(candicate)) {
            return false;
        }
        else {
            mem.add(candicate);
            return true;
        }
    }

    boolean tripleOverlap(Interval candicate) {
        Collections.sort(mem);
        int n = mem.size();
        int left = 0;
        int right = n - 1;
        while (left < n && candicate.s >= mem.get(left).e) left ++;
        while (right >= 0 && candicate.e <= mem.get(right).s) right --;
        for (int i = left; i <= right; ++i) {
            for (int j = left; j < i; ++j){
                Interval ans = new Interval(-1, -1);
                if (overlap(mem.get(j), mem.get(i), ans)) {
                    if (overlap(ans, candicate, new Interval(-1, -1))) return true;
                }
            }
        }
        return false;
    }

    boolean overlap(Interval a, Interval b, Interval ans) {
        if (b.s >= a.s && b.s < a.e || b.e <= a.e && b.e > a.s || b.s <= a.s && b.e >= a.e) {
            ans.s = Math.max(b.s, a.s);
            ans.e = Math.min(a.e, b.e);
            return true;
        }
        return false;
    }
}

再来一种积分的思路,时间复杂度为O(n2),加入新的区间后,判断当前区间的累积值是否超过3,超过3则说明该区间不该被加入,重新去除。

具体查看imos 累积法

JAVA代码超时,C++则AC,代码如下:

class MyCalendarTwo {
public:
    map<int, int> m;
    MyCalendarTwo() {
    }

    bool book(int start, int end) {
        m[start] ++;
        m[end] --;
        int s = 0;
        for (auto it : m) {
            s += it.second;
            if (s >= 3) {
                m[end] ++;
                m[start] --;
                return false;
            }
        }
        return true;
    }
};
作者:u014688145 发表于2017/11/19 18:24:24 原文链接
阅读:40 评论:0 查看评论

Object Detection系列(五) R-FCN

$
0
0

这里写图片描述

Object Detection系列(一) R-CNN
Object Detection系列(二) SPP-Net
Object Detection系列(三) Fast R-CNN
Object Detection系列(四) Faster R-CNN
Object Detection系列(五) R-FCN

R-FCN简介:

上面这张图在这个系列文章中都会出现,可以看到,在时间轴上R-FCN并不应该出现在第五篇中,但是R-FCN在内容上是承接Faster R-CNN的,同样是何凯明团队提出,所以在这里把R-FCN移到了前面。

CNN的旧形态

用于图像分类的基础CNN模型,有一个旧形态与新形态的区分,基于旧形态的CNN结构如AlexNet,VGG,Network-in-Network,ZF-Net等等,它们都有一个特点是卷积之后保留了几层用于逻辑判断的全连接网络。何凯明和RBG团队的R-CNN系列在Faster R-CNN之前都是在这种旧形态的CNN模型上改出来了,又因为Faster R-CNN及其之前的网络一直在解决的问题就是如何充分的利用原有模型的卷积层作共享计算呢?所以才会有用卷积层完成整幅图像的特征提取,区域建议的生成等等工作。

CNN的新形态

我们都知道基础的CNN模型,一般情况下层数越深,特征图的厚度也就会越大,这样一来,为了适应第一层的全连接的维度,往往会在最后一层特征图上做全尺寸的卷积,这层卷积的参数量是非常巨大的,比如AlexNet一共只有60M个参数,但是这一层卷积的参数量就会占去一多半,所以新形态的CNN呈现全卷积化的趋势,比如ResNet,GoogleNet,DenseNet等等,而且最后一层一般采用全局池化而不是全尺寸卷积,这样一来可以在尽量减少参数的情况下增加网络的深度。

R-CNN系列在新形态CNN下的问题

Faster R-CNN及其之前的结构都是基于旧形态CNN设计的,如果把新形态的CNN迁移到Faster R-CNN中就会出现问题,对于这个问题,作者给出了如下解释:
图片分类任务与目标检测任务性质是有所差异的
分类任务想要的是对于变换的不变性(Translation invariance),也就是说不管这个类别的东西在图片的那个位置,对分类的结果不应该产生影响。
检测任务想要的是对于变换的敏感性(Translation variance),因为需要知道物体到底在哪里。
但是卷积的层数越深,不变性就越强,敏感性就会变弱。所以Faster R-CNN的结构并不适合新形态的CNN。
在Faster R-CNN的时候,作者是用了ResNet-101作为基础模型的,但是ResNet-101没有分成98层+3层,而是分为91层+10层,这个实验的结果相比于旧形态的CNN,mAP高了,但是时间也变长了,具体的值在最后的实验结果中可以看到。通过这个实验可以得出的结论是新形态的CNN模型,不适用于这种前几层共享卷积计算,后几层不共享的方式。为了解决这个问题,就有了R-FCN。

这里需要说明一点:这些观点大多来源于论文,其实在现在的很多工程实践中,Faster R-CNN就是在用ResNet,在这里之所以把论文的观点说出来主要是为了理解作者是如何一步一步的改进,最后完成R-CNN整个系列,个人认为这个思路的理解要比理解单个模型或者跑一遍代码更为重要一些。

R-FCN

R-FCN是为了适应全卷积化的CNN结构,首先R-FCN在共享所有的卷积层的,其次为了解决上面提到的问题,R-FCN提出了:
位置敏感分值图(Position-sensitive score maps)
它用来判断某一个框到底属于哪一个类别
位置敏感池化(Position-sensitive RoI pooling)
在位置敏感分值图的基础上提出的一种池化操作。

这里写图片描述
上面这张图就是R-FCN的结构,在绿色框里的内容就是ResNet-101模型,这部分卷积计算还是用来被RPN和Position-sensitive Net共享的,他就相当于Faster R-CNN里面的前五层卷积一样;蓝色的框内是RPN,它在R-FCN中的作用和在Faster R-CNN中是一样的,负责输出建议区域与边界框;最重要的部分,就是红色框内的位置敏感卷积与池化,实际上,这部分的结构在Faster R-CNN中是RoI pooling+全连接+多任务损失函数等等,在R-FCN中,这部分内容换成了对位置敏感卷积,并把RPN的建议框扣在在分值图上做位置敏感池化。

R-FCN之所以起这个名字,是因为图像分割任务中出现了一个FCN(全卷积网络),而这种全卷积的说法和R-FCN想要表达的意思很契合,或许这就是为啥这一版的名字没有叫Fastest R-CNN吧,哈哈。

位置敏感卷积:

这里写图片描述
上面图中,feature maps后面那根线就是位置敏感卷积层,它的卷积核个数是K^2(C+1),其中K是超参数,在论文用的比较多的是k=3,K^2是Grid的个数,这个个数与位置敏感池化操作后的尺寸相关联的。C是物体的类别数量,加1是因为还有一个背景类。
经过了这一层卷积之后的输出就是位置敏感分值图,分值图的宽高尺寸是与feature maps的宽高一致的,分值图的通道数就是K^2(C+1),即每一个类别都有K^2个通道。
虽然这一层卷积操作叫位置敏感卷积,并输出了位置敏感分值图,但是它本质上只是一个常规的卷积,不同的地方其实在于配合它一起使用的位置敏感池化。

位置敏感卷池化:

这里写图片描述
位置敏感池化是在分值图上的一种池化操作,它是RoI池化的变种,之前说分值图的通道是K^2(C+1),每一个类别有K^2个通道,换句话说,分值图上就有k^2个C+1个通道的组合。那么假设k=3的话,这种情况就像上面图示的那样,每一个颜色都有C+1个通道,RPN的区域建议扣在分值图上后,位置敏感池化会把这个区域在每一个通道上平均分为K^2份,然后在每一个bin内做Max pooling,但是关键在K*K格子的位置是和通道对应的,如上图中深黄色的通道数有C+1个,那么位置敏感池化操作的时候,只要深黄色通道为左上角的bin内的值,作为K*K格子的左上角位置的值,所以K*K格子的左上角位置也是深黄色的。
这种对应关系就是从左到右,从上到下。这样的话,会得到一个K*K的格子,厚度是C+1,也就是每一个通道代表一个类别。
下面这张图可以更直观的说明位置敏感池化:
这里写图片描述
红色的箭头就说明了这种对应关系。

得到K*K格子之后,再做一步全局平均池化,就得到了1*1*(C+1)的特征,刚好是C+1个,这样一来,维度固定了,同样实现了将不同的输入整理成相同维度的输出,同时维度刚好与分类数相等。

R-FCN损失函数

这里写图片描述
这个损失函数,和之前的没啥区别,同样是一个分类+回归的多任务损失,最后在一个batch加和计算loss。
用于分类的是还是负的概率log值;
用于回归的是smooth L1。

关键的地方时,R-FCN把什么特征送到了Bounding box回归模型里面,在Fast R-CNN里是conv5特征经过RoI pooling之后的特征,在RPN里是用一个卷积分支专门生产的特征,在R-FCN里面也是用一个单独的卷积分支生产的,特征的通道数是4K^2,方法和位置敏感卷积时一样的,只是C+1改成了4。

R-FCN训练

R-FCN训练的步骤与Faster R-CNN相同,同样是分步训练法,只是把Fast R-CNN换成了R-FCN。
此外,文章提出了一个叫做OHEM(Online Hard Example Mining)的训练技巧:
当一个图片生成N个区域建议后,会使用当前的网络一次计算所有N个区域的loss,并根据loss从大到小排序建议区域,并从这N个排序后的区域中取前Batch size个。
这是因为,如何某区域的loss更大,那么说明网络中的参数并没有照顾到这种特征,而这种特征应该是被学习到的,如何把本来loss就很小的特征在送入网络中参与训练,对参数的更新也没啥影响。

R-FCN性能评价

这里写图片描述
上面这张图说明了超参数k对最后的mAP的影响:
初始的Faster R-CNN RoI pooling的k选择为1时,mAP为61.7%,选择为7时,mAP为68.9%;
R-FCN RoI pooling的k选择为1时是没有位置敏感信息的,直接fail,k选择为3和7时,mAP分别为75.5%与76.6%。

这里写图片描述
上面这张图从多个角度对比了Faster R-CNN与R-FCN,其中Faster R-CNN用的是Resnet101,把前91层做共享卷积计算,后10层代替原来的3层全连接,所以这个Faster R-CNN的测试时间不是之前说的0.18s,而是mAP也不是66.9%。
关注中间一行,在使用OHEM的情况下,以ResNet-101为初始模型的两个结构,单张训练时间,R-FCN比Faster R-CNN快3倍多,单张测试时间R-FCN比Faster R-CNN快2.5倍左右。而0.17s这个时间,和Faster R-CNN使用旧形态的CNN模型时间是差不多的,但是mAP确实79.5%,优于原来的69.9%。

作者:chaipp0607 发表于2017/11/19 19:21:32 原文链接
阅读:23 评论:0 查看评论

安卓调用系统录像功能:1、启动录像返回视频,2、启动录像将视频存储在指定路径下

$
0
0

全栈工程师开发手册 (作者:栾鹏)

安卓教程全解

安卓调用系统录像功能,两种方式获取拍摄的视频。

1、启动系统录像intent,并直接返回视频数据

2、启动系统录像intent,录像后存储在指定的路径下,返回后app主动读取路径下的视频文件。


第一种方式:启动系统相机录像,返回视频数据

  private static final int RECORD_VIDEO = 0;
  private void takevideo() {
    //生成Intent.
    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    //启动摄像头应用程序
    startActivityForResult(intent, RECORD_VIDEO);
  }

第二种方式:启动系统录像,视频存储到指定路径下

 //使用一个intent请求录像,视频存储在指定位置
  private Uri outputFileUri;  
  private static final int RECORD_VIDEO_SAVE = 1;
  public void takevideo_save() {
    //创建输出文件
      File file = new File(Environment.getExternalStorageDirectory(),"test.mp4");  //存放在sd卡的根目录下
      outputFileUri = Uri.fromFile(file);

      //生成Intent.
      Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
      intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);

      //启动摄像头应用程序
      startActivityForResult(intent, RECORD_VIDEO_SAVE);
  }

接收系统录像的事件的返回结果(1返回视频数据,2返回视频地址)

除了可以根据返回的intent,也可以根据请求码来区别。

对于包含视频数据的,直接将数据给VideoView播放,对于不包含视频数据的这判定为存储在了指定位置。通过uri获取视频路径,将地址给VideoView播放。

  //1、获取录制视频使用VideoView播放,2、获取视频存储地址
  @Override
  protected void onActivityResult(int requestCode,int resultCode, Intent data) {
      //读取直接返回的视频数据
    if (requestCode == RECORD_VIDEO) {
          VideoView videoView = (VideoView)findViewById(R.id.activity1_video1);
          Uri uri=data.getData();
          videoView.setVideoURI(uri); 
          videoView.start();
          Log.v("系统录像", "直接返回视频数据"+uri.getPath());
    }
    //读取指定路径的视频文件
    else if (requestCode == RECORD_VIDEO_SAVE) {
        VideoView videoView = (VideoView)findViewById(R.id.activity1_video1);
        videoView.setKeepScreenOn(true);
        String path = outputFileUri.getPath();
        String path1=Environment.getExternalStorageDirectory()+"/test.mp4";
        videoView.setVideoPath(path1);
        Log.v("系统录像", path+"读取"+path1+"下的视频文件");
    }
  } 
作者:luanpeng825485697 发表于2017/11/19 20:00:49 原文链接
阅读:3 评论:0 查看评论

福建第六届省赛 最长连续串(贪心)

$
0
0

ZB is playing a card game where the goal is to make straights. Each card in the deck has a number between 1 and M(including 1 and M). A straight is a sequence of cards with consecutive values. Values do not wrap around, so 1 does not come after M. In addition to regular cards, the deck also contains jokers. Each joker can be used as any valid number (between 1 and M, including 1 and M).

You will be given N integers card[1] .. card[n] referring to the cards in your hand. Jokers are represented by zeros, and other cards are represented by their values. ZB wants to know the number of cards in the longest straight that can be formed using one or more cards from his hand.

Input

The first line contains an integer T, meaning the number of the cases.

For each test case:

The first line there are two integers N and M in the first line (1 <= N, M <= 100000), and the second line contains N integers card[i] (0 <= card[i] <= M).

Output

For each test case, output a single integer in a line -- the longest straight ZB can get.

Sample Input
2
7 11
0 6 5 3 0 10 11
8 1000
100 100 100 101 100 99 97 103
Sample Output
5
3


【experience】:题意真爽!

先是看到样例以为是最长递增子序列,交了5发WA。

然后曲解题意,以为是找连续的子串,没调出来。

经队友提示,才真正明白这道题的意思是干啥!md...

【分析】:

题意是从手中的卡片中排出一个最长的连续序列,与输入的顺序无关!!0可以代表任意任意数字。


用数组c标记哪个数字存在,同时记下0的数量。

设两个下标,i和last,扫一遍,i在前,last在后。过程中保证last~i之间的空位可以用0补全。


【代码】:

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int c[101010];
int n,m,x;
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);
        memset(c,0,sizeof(c));
        int r0=0;
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&x);
            if(x==0)r0++;
            c[x]=1;
        }
        int ans=0;
        for(int i=1,c0=0,last=0;i<=m;i++)
        {
            if(!c[i])c0++;
            while(c0>r0)
            {
                last++;
                if(!c[last])c0--;
            }
            ans=max(ans,i-last);
        }
        cout<<ans<<endl;
    }
}


作者:winter2121 发表于2017/11/19 20:30:28 原文链接
阅读:19 评论:0 查看评论

nginx include conf

$
0
0

nginx 的配置很灵活,支持include配置文件,如果我们的域名都配置到nginx.conf. 这个文件就会比较乱, 也影响管理和阅读.所以直接拆分出来,分成不同的配置文件.

怎么实现呢
see
这里写图片描述

这里写图片描述

如果你想在vhost下放多个配置文件
我想都加载到nginx中

include /usr/local/nginx/conf/vhost.d/*.conf

然后nginx -s reload 就ok了

作者:kwy15732621629 发表于2017/11/19 20:31:24 原文链接
阅读:22 评论:0 查看评论

Unity Shader 学习笔记(14) 阴影

$
0
0

Unity Shader 学习笔记(14) 阴影

参考书籍:《Unity Shader 入门精要》

*版本:2017.1.1f1


阴影

实现原理

  使用Shadow Map技术。把摄像机与光源位置重合,光源的阴影部分就是摄像机看不到的地方。

  前向渲染路径中,最重要的平行光如果开启了阴影,Unity就会为光源计算阴影映射纹理(shadowmap),本质就是深度图,记录光源出发到最近表面位置。两种方法:
1. 摄像机放在光源位置,然后按正常渲染流程(调用Base Pass 和 Additional Pass)来更新深度信息,得到阴影映射纹理。
2. 摄像机放在光源位置,调用额外的Pass:LightMode = ShadowCaster,把顶点变换到光源空间,渲染目标不是帧缓存,而是阴影映射纹理。

阴影采样

  • 传统方法:
    • 正常渲染Pass,计算顶点的光源空间,用xy分量对纹理采样,如果顶点值大于该深度值,就说明在阴影区域。
  • Unity5及以后:
    • 屏幕空间的阴影映射技术(Screenapce Shadow Map)。是在延迟渲染中产生阴影的方法。不过需要显卡支持MRT。根据阴影映射纹理和深度纹理得到屏幕空间的阴影图。阴影图包含了屏幕空间所有阴影区域。

接收其他物体阴影

  在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,结果和最后的光照相乘即可。

向其他物体投射阴影

  把物体加入到光源的阴影映射纹理计算中(让其他物体可以得到该物体信息),即执行LightMode 为 ShadowCaster的Pass。


具体实现

使用AutoLight.cginc文件内的三个宏:
- SHADOW_COORDS:声明了一个名为_ShadowCoord的阴影纹理坐标变量。
- TRANSFER_SHADOW:根据不同平台实现。如果使用了屏幕空间的阴影映射技术,会使用内置ComputeScreenPos函数计算_ShadowCoord;否则就直接把顶点转换到光源空间存到_ShadowCoord
- SHADOW_ATTENUATION:对_ShadowCoord采样,得到阴影信息。

需要注意:内置宏用了一些变量名需要在自己定义的时候要匹配:a2f的顶点坐标为vertex,输出的v2f结构体名为v,v2f中顶点位置名为pos。

  1. 计算阴影深度,绘制深度纹理:
  2. 绘制阴影:
  3. 最后图形:

BasePass:

...

#include "AutoLight.cginc"          // 计算阴影的宏

...

struct v2f {
    ...
    // 对阴影纹理采样的坐标,参数为下一个可用插值寄存器的索引值(前面TEXCOORD0和1,所以这里是2)
    SHADOW_COORDS(2)
};

v2f vert(a2v v) {
    ... 
    // 计算阴影纹理坐标
    TRANSFER_SHADOW(o); 
    return o;
}

fixed4 frag(v2f i) : SV_Target {
    ...
    // 计算阴影值
    fixed shadow = SHADOW_ATTENUATION(i);
    return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}

衰减和阴影统一管理

  使用AutoLight.cginc的内置宏UNITY_LIGHT_ATTENUATION,不再需要自己判断光源类型等,也不用在BasePass中单独处理阴影。如果Additional Pass需要添加阴影,用#pragma multi_compile_fwdadd_fullshadows命令。
  第一个参数变量名,宏会创建这个名字的变量;第二个参数为v2f结构体,用来计算阴影;第三个参数是世界空间的坐标,计算光照衰减。

fixed4 frag(v2f i) : SV_Target {
    ... // 不需要定义atten,下面宏会自己定义

    // 使用内置宏同时计算光照衰减和阴影。自动声明atten变量。
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

部分源码如下。可以看到点光源、聚光灯、平行光的计算。后面还有部分代码是根据是否启用cookie的情况做不同版本的宏。


透明物体的阴影

透明测试与阴影

如果直接使用FallBack默认回调(VertexLit、Diffuse、Specular),往往无法得到正确的阴影,因为在透明测试中某些片元丢弃了,而默认回调没有进行这样的处理。可以使用回调Transparent/Cutout/VertexLit。可见源文件Alpha-VertexLit.shader。

第一个:FallBack “Transparent/VertexLit”;第二个:FallBack “Transparent/Cutout/VertexLit”;第三个:基于第二个同时在MeshRenderer的Cast Shadows选择Two Sided。

透明混合与阴影

由于需要关闭深度写入,阴影处理变得复杂,所以内置半透明的Shader都是没有阴影效果的。可以修改Fallback为VertexLit、Diffuse等不透明物体用的UnityShader。

默认不投射不接收阴影(FallBack “Transparent/VertexLit”):

强制接收阴影(FallBack “VertexLit”),但不能透过立方体看到后面墙壁的阴影:

作者:l773575310 发表于2017/11/19 21:13:01 原文链接
阅读:0 评论:0 查看评论

Java4Android笔记之Java中的对象的转型

$
0
0

对象的向上转型

什么是向上转型

向上转型——将子类的对象赋值给父类的引用:

Student s = new Student();
Person p = s;

class Student extends Person{
    String address;

    void introduce(){
        super.introduce();
        System.out.println("我的家在"+address);
    }

    void study(){
        System.out.println("我在学习");
    }
}


class Person{
    String name;
    int age;

    void introduce(){
        System.out.println("我的姓名是"+name+",我的年龄是"+age);
    }   
}

一个引用能够调用哪些成员(变量和函数),取决于这个引用的类型

一个引用调用的是哪一个方法,取决于这个引用所指向的对象

class Test{
    public static void main(String args[]){
        Student s = new Student();
        Person p = s;//要求为继承关系
        //或者这样写:
        //Person p = new Student();
        //一个引用能够调用哪些成员(变量和函数),取决于这个引用的类型
        p.name = "张三";
        p.age = 20;
        //p.address = "北京";//报错,提示找不到此符号
        p.introduce();//调用的Student的introduce()
        //p.study();//报错,提示找不到符号
    }
}   

对象的向下转型

什么是向下转型

向下转型——将父类的对象赋值给子类的引用

向下转型的前提是向上转型

Student s1 = new Student();
Person p = s1;
Student s2 = (Student)p;

class Test{
    public static void main(String args[]){
        Person p = new Student();
        Student s = (Student)p;

        //错误:
        //Person p = new Person();
        //Student s = (Student)p;
    }
}
作者:wudongjiang333 发表于2017/11/19 21:22:07 原文链接
阅读:31 评论:0 查看评论

LWC 59:730. Count Different Palindromic Subsequences

$
0
0

LWC 59:730. Count Different Palindromic Subsequences

传送门:730. Count Different Palindromic Subsequences

Problem:

Given a string S, find the number of different non-empty palindromic subsequences in S, and return that number modulo 10^9 + 7.

A subsequence of a string S is obtained by deleting 0 or more characters from S.

A sequence is palindromic if it is equal to the sequence reversed.

Two sequences A_1, A_2, … and B_1, B_2, … are different if there is some i for which A_i != B_i.

Example 1:

Input:
S = ‘bccb’
Output: 6
Explanation:
The 6 different non-empty palindromic subsequences are ‘b’, ‘c’, ‘bb’, ‘cc’, ‘bcb’, ‘bccb’.
Note that ‘bcb’ is counted only once, even though it occurs twice.

Example 2:

Input:
S = ‘abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba’
Output: 104860361
Explanation:
There are 3104860382 different non-empty palindromic subsequences, which is 104860361 modulo 10^9 + 7.

Note:

  • The length of S will be in the range [1, 1000].
  • Each character S[i] will be in the set {‘a’, ‘b’, ‘c’, ‘d’}.

思路:
难点在于如何划分子问题,才能保证更新dp时没有重复,其中需要解决重复元素子串的表达。为了保证每个子问题的回文在原问题中没有出现过,定义如下规则:子问题求出的回文串必须套上一层外壳,即子问题中的回文串集合Set = {s | s 为回文}, 有新的回文 s’ = “a” + s + “a” or “b” + s + “b”,….

定义函数如下f(i, j) 表示当前对应S[i,…j]的不重复回文串个数,于是有:

初始化: ans = 0
1. 子问题的回文串批层外衣,有 ans += f(i + 1, j - 1) , 其中S[i] == S[j]
2. 考虑"a_..._a", "_..._"表示子问题的回文串,抽出a'= a...a,其中"..."表示x个a,那么有新的回文串aa...a 和 aa...aa,有ans += 2

代码如下:

    public int countPalindromicSubsequences(String S) {
        int n = S.length();
        int[][] next = new int[4][1010];
        int[][] prev = new int[4][1010];

        char[] cs = S.toCharArray();

        for (int i = 0; i < 4; ++i) Arrays.fill(next[i], n);
        for (int i = n - 1; i >= 0; --i) {
            int c = cs[i] - 'a';
            for (int j = 0; j < 4; ++j) next[j][i] = i + 1 == n ? n : next[j][i + 1];
            next[c][i] = i;
        }

        for (int i = 0; i < 4; ++i) Arrays.fill(prev[i], -1);
        for (int i = 0; i < n; ++i) {
            int c = cs[i] - 'a';
            for (int j = 0; j < 4; ++j) prev[j][i] = i - 1 == -1 ? -1 : prev[j][i - 1];
            prev[c][i] = i;
        }
        dp = new int[1010][1010];
        return f(cs, next, prev, 0, n - 1);
    }

    int mod = 1000000000 + 7;
    int[][] dp;

    int f(char[] cs, int[][] next, int[][] prev, int s, int e) {
        if (s > e) return 0;
        if (dp[s][e] > 0) return dp[s][e];
        long ans = 0;
        for (int i = 0; i < 4; ++i) {
            int ns = next[i][s];
            int ne = prev[i][e];
            if (ns > ne) continue;
            if (ns != ne) ans += 1;
            ans ++;
            ans += f(cs, next, prev, ns + 1, ne - 1);
        }
        dp[s][e] = (int)(ans % mod);
        return dp[s][e];
    }
作者:u014688145 发表于2017/11/19 21:45:11 原文链接
阅读:10 评论:0 查看评论

Java4Android笔记之Java中的抽象类和抽象函数

$
0
0

抽象函数的语法特征

定义

只有函数的定义,没有函数体的函数被称为抽象函数:

abstract void fun();

例如:

class Person(){//编译报错,Person不是抽象的
    String name;
    int age;

    void introduce(){
        System.out.println("...");
    }

    abstract void eat();
}

抽象类的语法特征

定义

使用abstract定义的类被称之为抽象类;

  1. 抽象类不能够生成对象;
  2. 如果一个类当中包含有抽象函数,那么这个类必须被声明为抽象类;
  3. 如果一个类当中没有抽象函数,那么这个类也可以被声明为抽象类;

    abstract class Person(){//编译报错,Person不是抽象的
        String name;
        int age;
    
        Person(){
            System.out.println("Person()");
        }
    
        void introduce(){
            System.out.println("...");
        }
    
        abstract void eat();
    }
    
    
    
    class Chinese extends Person{
        Chinese(){
            System.out.println("Chinese()");
        }
        //覆写
        void eat(){
            System.out.println("用筷子吃饭");
        }
    }       
    
    
    class Test{
        public static void main(String[] args){
            Person p = new Chinese();
            p.eat();
        }
    }   
    

抽象类的作用

  1. 用作基类
  2. 抽象类不能生成对象,但是可以有构造函数!!因为在子类中可以通过super关键字调用父类的构造函数
作者:wudongjiang333 发表于2017/11/19 21:53:30 原文链接
阅读:0 评论:0 查看评论

Chromium分发输入事件给WebKit处理的过程分析

$
0
0

       Chromium的Render进程接收到Browser进程分发过来的输入事件之后,会在Compoistor线程中处理掉滑动和捏合手势这两种特殊的输入事件,其它类型的输入事件则交给Main线程处理。Main线程又会进一步将输入事件分发给WebKit处理。WebKit则根据输入事件发生的位置在网页中找到对应的HTML元素进行处理。本文接下来详细分析Chromium分发输入事件给WebKit处理的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       以Touch事件为例。WebKit接收到Chromium分发给它的Touch事件通知后,所做的第一件事情是做Hit Test,也就是检测Touch事件发生在哪一个HTML元素上,然后再将接收到的Touch事件分发给该它处理,如图1所示:


图1 WebKit处理Touch事件的过程

       HTML元素对应于网页DOM Tree中的Node。要从DOM Tree中找到输入事件的目标Node,必须要知道所有Node的Z轴位置大小,然后按照从上到下的顺序进行Hit Test。从前面Chromium网页Graphics Layer Tree创建过程分析一文可以知道,DOM Tree中的Node在Z轴上的实际位置,除了跟它们本身的Z-Index属性值有关之外,还与它们所处理的Stacking Context的Z-Index值有关。网页的Render Layer Tree记录了所有的Stacking Context,因此,WebKit要从网页的Render Layer Tree入手,才能在DOM Tree中找到正确的目标Node处理当前发生的输入事件。

       接下来,我们就从Compoistor线程将输入事件分发给Main线程开始,分析WebKit接收和处理输入事件的过程。从前面Chromium网页滑动和捏合手势处理过程分析一文可以知道,Compoistor线程是在InputEventFilter类的成员函数ForwardToHandler中将它不处理的输入事件发分给Main线程处理的,如下所示:

void InputEventFilter::ForwardToHandler(const IPC::Message& message) {  
  ......  
  
  int routing_id = message.routing_id();  
  InputMsg_HandleInputEvent::Param params;  
  if (!InputMsg_HandleInputEvent::Read(&message, &params))  
    return;  
  const WebInputEvent* event = params.a;  
  ui::LatencyInfo latency_info = params.b;  
  .......  
  
  InputEventAckState ack_state = handler_.Run(routing_id, event, &latency_info);  
  
  if (ack_state == INPUT_EVENT_ACK_STATE_NOT_CONSUMED) {  
    ......  
    IPC::Message new_msg = InputMsg_HandleInputEvent(  
        routing_id, event, latency_info, is_keyboard_shortcut);  
    main_loop_->PostTask(  
        FROM_HERE,  
        base::Bind(&InputEventFilter::ForwardToMainListener,  
                   this, new_msg));  
    return;  
  }  
  
  ......   
}
       这个函数定义在文件external/chromium_org/content/renderer/input/input_event_filter.cc中。

       InputEventFilter类的成员函数ForwardToHandler的详细分析可以参考前面Chromium网页滑动和捏合手势处理过程分析一文。对于Compositor线程不处理的输入事件,InputEventFilter类的成员函数ForwardToHandler会将它重新封装在一个InputMsg_HandleInputEvent消息中。这个InputMsg_HandleInputEvent消息又会进一步封装在一个Task中,并且发送给Main线程的消息队列。这个Task绑定了InputEventFilter类的成员函数ForwardToMainListener。

       这意味着接下来InputEventFilter类的成员函数ForwardToMainListener就会在Main线程中被调用,用来处理Compositor线程分发过来的输入事件,如下所示:

void InputEventFilter::ForwardToMainListener(const IPC::Message& message) {
  main_listener_->OnMessageReceived(message);
}
       这个函数定义在文件external/chromium_org/content/renderer/input/input_event_filter.cc中。

       从前面Chromium网页滑动和捏合手势处理过程分析一文可以知道,InputEventFilter类的成员变量main_listener_指向的是一个RenderThreadImpl对象。这个RenderThreadImpl对象描述的就是Render进程中的Render Thread,也就是Main Thread。InputEventFilter类的成员函数ForwardToMainListener调用这个RenderThreadImpl对象的成员函数OnMessageReceived处理参数message描述的输入事件。

       RenderThreadImpl类的成员函数OnMessageReceived是从父类ChildThread继承下来的,它的实现如下所示:

bool ChildThread::OnMessageReceived(const IPC::Message& msg) {
  .....

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(ChildThread, msg)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  if (handled)
    return true;

  ......

  return router_.OnMessageReceived(msg);
}
       这个函数定义在文件external/chromium_org/content/child/child_thread.cc 中。

       ChildThread类的成员函数OnMessageReceived首先检查参数msg描述的消息是否要由自己处理。如果不处理,那么就再分发给注册在它里面的Route进行处理。

       从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程会为每一个网页创建一个RenderViewImpl对象,用来代表网页所加载在的控件。这个RenderViewImpl对象在初始化的过程中,会通过父类RenderWidget的成员函数DoInit将自己注册为Render Thread中的一个Route,如下所示:

bool RenderWidget::DoInit(int32 opener_id,
                          WebWidget* web_widget,
                          IPC::SyncMessage* create_widget_message) {
  ......

  bool result = RenderThread::Get()->Send(create_widget_message);
  if (result) {
    RenderThread::Get()->AddRoute(routing_id_, this);
    .......
    return true;
  } 

  ......
}
      这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

      这意味着前面分析的RenderThreadImpl类的成员函数OnMessageReceived会将接收到的、自己又不处理的消息分发给RenderWidget类处理。RenderWidget类通过成员函数OnMessageReceived接收RenderThreadImpl类分发过来的消息,并且判断分发过来的消息是否与输入事件有关,也就是判断是否为一个类型为InputMsg_HandleInputEvent的消息。如果是的话,那么就会进行处理。如下所示:

bool RenderWidget::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RenderWidget, message)
    IPC_MESSAGE_HANDLER(InputMsg_HandleInputEvent, OnHandleInputEvent)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

       RenderWidget类通过成员函数OnMessageReceived会将类型为InputMsg_HandleInputEvent的消息分发给另外一个成员函数OnHandleInputEvent处理,如下所示:

void RenderWidget::OnHandleInputEvent(const blink::WebInputEvent* input_event,
                                      const ui::LatencyInfo& latency_info,
                                      bool is_keyboard_shortcut) {
  ......

  base::TimeTicks start_time;
  if (base::TimeTicks::IsHighResNowFastAndReliable())
    start_time = base::TimeTicks::HighResNow();
  ......

  bool prevent_default = false;
  if (WebInputEvent::isMouseEventType(input_event->type)) {
    const WebMouseEvent& mouse_event =
        *static_cast<const WebMouseEvent*>(input_event);
    ......
    prevent_default = WillHandleMouseEvent(mouse_event);
  }

  if (WebInputEvent::isKeyboardEventType(input_event->type)) {
    ......
#if defined(OS_ANDROID)
    // The DPAD_CENTER key on Android has a dual semantic: (1) in the general
    // case it should behave like a select key (i.e. causing a click if a button
    // is focused). However, if a text field is focused (2), its intended
    // behavior is to just show the IME and don't propagate the key.
    // A typical use case is a web form: the DPAD_CENTER should bring up the IME
    // when clicked on an input text field and cause the form submit if clicked
    // when the submit button is focused, but not vice-versa.
    // The UI layer takes care of translating DPAD_CENTER into a RETURN key,
    // but at this point we have to swallow the event for the scenario (2).
    const WebKeyboardEvent& key_event =
        *static_cast<const WebKeyboardEvent*>(input_event);
    if (key_event.nativeKeyCode == AKEYCODE_DPAD_CENTER &&
        GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) {
      OnShowImeIfNeeded();
      prevent_default = true;
    }
#endif
  }

  if (WebInputEvent::isGestureEventType(input_event->type)) {
    const WebGestureEvent& gesture_event =
        *static_cast<const WebGestureEvent*>(input_event);
    ......
    prevent_default = prevent_default || WillHandleGestureEvent(gesture_event);
  }

  bool processed = prevent_default;
  if (input_event->type != WebInputEvent::Char || !suppress_next_char_events_) {
    suppress_next_char_events_ = false;
    if (!processed && webwidget_)
      processed = webwidget_->handleInputEvent(*input_event);
  }

  // If this RawKeyDown event corresponds to a browser keyboard shortcut and
  // it's not processed by webkit, then we need to suppress the upcoming Char
  // events.
  if (!processed && is_keyboard_shortcut)
    suppress_next_char_events_ = true;

  InputEventAckState ack_result = processed ?
      INPUT_EVENT_ACK_STATE_CONSUMED : INPUT_EVENT_ACK_STATE_NOT_CONSUMED;  
  ......

  // dispatch compositor-handled scroll gestures.
  bool event_type_can_be_rate_limited =
      input_event->type == WebInputEvent::MouseMove ||
      input_event->type == WebInputEvent::MouseWheel ||
      (input_event->type == WebInputEvent::TouchMove &&
       ack_result == INPUT_EVENT_ACK_STATE_CONSUMED);

  bool frame_pending = compositor_ && compositor_->BeginMainFrameRequested();

  // If we don't have a fast and accurate HighResNow, we assume the input
  // handlers are heavy and rate limit them.
  bool rate_limiting_wanted = true;
  if (base::TimeTicks::IsHighResNowFastAndReliable()) {
      base::TimeTicks end_time = base::TimeTicks::HighResNow();
      total_input_handling_time_this_frame_ += (end_time - start_time);
      rate_limiting_wanted =
          total_input_handling_time_this_frame_.InMicroseconds() >
          kInputHandlingTimeThrottlingThresholdMicroseconds;
  }

  // Note that we can't use handling_event_type_ here since it will be overriden
  // by reentrant calls for events after the paused one.
  bool no_ack = ignore_ack_for_mouse_move_from_debugger_ &&
      input_event->type == WebInputEvent::MouseMove;
  if (!WebInputEventTraits::IgnoresAckDisposition(*input_event) && !no_ack) {
    InputHostMsg_HandleInputEvent_ACK_Params ack;
    ack.type = input_event->type;
    ack.state = ack_result;
    ack.latency = swap_latency_info;
    scoped_ptr<IPC::Message> response(
        new InputHostMsg_HandleInputEvent_ACK(routing_id_, ack));
    if (rate_limiting_wanted && event_type_can_be_rate_limited &&
        frame_pending && !is_hidden_) {
      // We want to rate limit the input events in this case, so we'll wait for
      // painting to finish before ACKing this message.
      ......
      if (pending_input_event_ack_) {
        // As two different kinds of events could cause us to postpone an ack
        // we send it now, if we have one pending. The Browser should never
        // send us the same kind of event we are delaying the ack for.
        Send(pending_input_event_ack_.release());
      }
      pending_input_event_ack_ = response.Pass();
      ......
    } else {
      Send(response.release());
    }
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

       RenderWidget类的成员函数OnHandleInputEvent将参数input_event描述的输入事件分发给WebKit之前,会做以下三件事情:

       1. 检查参数input_event描述的输入事件是否是一个鼠标事件。如果是的话,那么就会调用另外一个成员函数WillHandleMouseEvent询问RenderWidget类是否要对它进行处理。如果处理,那么本地变量prevent_default的值就会被设置为true。

       2. 检查参数input_event描述的输入事件是否是一个DPAD键盘事件。如果是的话,并且当前获得焦点的是一个Input Text控件,那么就调用另外一个成员函数OnShowImeIfNeeded弹出输入法,以便用户可以输入文本给Input Text控件去。注意,这个检查是针对Android平台的,因为Android平台才会存在DPAD键。

       3. 检查参数input_event描述的输入事件是否是一个手势操作。如果是的话,那么就会调用另外一个成员函数WillHandleGestureEvent询问RenderWidget类是否要对它进行处理。如果处理,那么本地变量prevent_default的值也会被设置为true。

       RenderWidget类的成员函数WillHandleMouseEvent和WillHandleGestureEvent的返回值均为false,这表明RenderWidget类不会处理鼠标和手势操作这两种输入事件。

       RenderWidget类的成员函数OnHandleInputEvent接下来继续判断参数input_event描述的输入事件是否为键盘字符输入事件。如果不是,并且RenderWidget类不对它进行处理,那么RenderWidget类的成员函数OnHandleInputEvent就会将它分发给WebKit处理。这表明在RenderWidget类不处理的情况下,非键盘字符输入事件一定会分发给WebKit处理。

       另一方面,对于键盘字符输入事件,它也会分发给WebKit处理。不过,分发给WebKit之后,如果WebKit决定不对它进行处理,并且它被设置为浏览器的快捷键,即参数is_keyboard_shortcut的值等于true,那么下一个键盘字符输入事件将不会再分发给WebKit处理。因为下一个键盘字符输入事件将会作为浏览器快捷键的另外一部分。在这种情况下,RenderWidget类的成员变量suppress_next_char_events_的值会被设置为true。

       将参数input_event描述的输入事件分发给WebKit之后,RenderWidget类的成员函数OnHandleInputEvent考虑是否需要给Browser进程发送一个ACK。从前面Chromium网页输入事件捕捉和手势检测过程分析一文可以知道,Browser进程收到Render进程对上一个输入事件的ACK之后,才会给它分发下一个输入事件。因此,Render进程是否给Browser进程发送ACK,可以用来控制Browser进程分发输入事件给Render进程的节奏。

       有三种类型的输入事件是需要控制分发节奏的:鼠标移动、鼠标中键滚动和触摸事件。这三类输入事件都有一个特点,它们会连续大量地输入。每一次输入Render进程都需要对它们作出响应,也就是对网页进行处理。如果它们发生得太过频繁,那么就会造成Render进程的负担很重。因此,对于这三类输入事件,RenderWidget类的成员函数OnHandleInputEvent并不会都马上对它们进行ACK。这里有一点需要注意,对于触摸事件,只有WebKit没有对它进行处理,那么RenderWidget类的成员函数OnHandleInputEvent会马上对它进行ACK。这是因为这个触摸事件和接下来发生的触摸事件,可能会触发手势操作,例如滑动手势和捏合手势。这些手势操作必须要迅速进行处理,否则的话,用户就会觉得浏览器没有反应了。

       什么情况下不会马上进行ACK呢?如果网页的当前帧请求执行了一次Commit操作,也就是请求了重新绘制网页的CC Layer Tree,但是网页的CC Layer Tree还没有绘制完成,并且也没有同步到CC Pending Layer Tree,那么就不会马上进行ACK。这个ACK会被缓存在RenderWidget类的成员变量pending_input_event_ack_中。等到网页的CC Layer Tree重新绘制完成并且同步到CC Pending Layer Tree之后,被缓存的ACK才会发送给Browser进程。之所以要这样做,是因为Main线程在重新绘制网页的CC Layer Tree的时候,任务是相当重的,这时候不宜再分发新的输入事件给它处理。

        此外,如果平台实现了高精度时钟,那么RenderWidget类的成员函数OnHandleInputEvent也不一定要等到网页的CC Layer Tree重新绘制完成并且同步到CC Pending Layer Tree之后,才将缓存的ACK才会发送给Browser进程。如果已经过去了一段时间,网页的CC Layer Tree还没有绘制完成,也没有同步到CC Pending Layer Tree,那么RenderWidget类的成员函数OnHandleInputEvent就会马上给Browser进程发送一个ACK。这个时间被设置为kInputHandlingTimeThrottlingThresholdMicroseconds(4166)微秒,大约等于1/4帧时间(假设一帧时间为16毫秒)。

        还有一种特殊情况,造成RenderWidget类的成员函数OnHandleInputEvent不会缓存输入事件的ACK,那就是网页当前不可见。对于不可见的网页,Main线程不需要对它们进行处理,因为处理了也是白处理(用户看不到)。因此,可以认为此时分发给网页的任何输入都在瞬间完成,于是就可以安全地将它们ACK给Browser进程,不用担心Main线程的负载问题。

       最后,我们还看到,如果浏览器连上了Debugger,并且Debugger希望不要对鼠标移动事件进行ACK,那么鼠标移动事件在任何情况下,都不会进行ACK。Browser进程是通过类型为InputHostMsg_HandleInputEvent的消息向Render进程分发输入事件的。相应地,Render进程通过向Browser进程发送类型为InputHostMsg_HandleInputEvent_ACK的消息对输入事件进行ACK。

       接下来,我们就重点分析Chromium分发输入事件给WebKit处理的过程。从前面Chromium网页Frame Tree创建过程分析一文可以知道, RenderWidget类的成员变量webwidget_指向的是一个WebViewImpl对象。RenderWidget类的成员函数OnHandleInputEvent就是通过调用这个WebViewImpl对象的成员函数handleInputEvent将参数input_event描述的输入事件分发给WebKit处理的。前面我们已经假设这是一个Touch事件。

       WebViewImpl类的成员函数handleInputEvent的实现如下所示:

bool WebViewImpl::handleInputEvent(const WebInputEvent& inputEvent)
{
    ......

    return PageWidgetDelegate::handleInputEvent(m_page.get(), *this, inputEvent);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       对于Touch事件,WebViewImpl类的成员函数handleInputEvent将会调用PageWidgetDelegate类的静态成员函数handleInputEvent对它进行处理,如下所示:

bool PageWidgetDelegate::handleInputEvent(Page* page, PageWidgetEventHandler& handler, const WebInputEvent& event)
{
    LocalFrame* frame = page && page->mainFrame()->isLocalFrame() ? page->deprecatedLocalMainFrame() : 0;
    switch (event.type) {
    ......

    case WebInputEvent::TouchStart:
    case WebInputEvent::TouchMove:
    case WebInputEvent::TouchEnd:
    case WebInputEvent::TouchCancel:
        if (!frame || !frame->view())
            return false;
        return handler.handleTouchEvent(*frame, *static_cast<const WebTouchEvent*>(&event));

    ......

    default:
        return false;
    }
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/PageWidgetDelegate.cpp中。

       从前面的调用过程可以知道,参数page是从WebViewImpl类的成员变量m_page传递过来的,它指向的是一个Page对象。PageWidgetDelegate类的静态成员函数handleInputEvent通过这个Page对象可以获得一个LocalFrame对象。这个LocalFrame对象用来在WebKit层描述在当前进程中加载的网页,它的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。

       另外一个参数handler描述的是一个WebViewImpl对象。当第三个参数event描述的是一个Touch相关的事件时,PageWidgetDelegate类的静态成员函数handleInputEvent就会调用参数handler描述的WebViewImpl对象的成员函数handleTouchEvent对它进行处理。

       WebViewImpl类的成员函数handleTouchEvent是从父类PageWidgetEventHandler继承下来的,它的实现如下所示:

bool PageWidgetEventHandler::handleTouchEvent(LocalFrame& mainFrame, const WebTouchEvent& event)
{
    return mainFrame.eventHandler().handleTouchEvent(PlatformTouchEventBuilder(mainFrame.view(), event));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/PageWidgetDelegate.cpp中。

       PageWidgetEventHandler类的成员函数handleTouchEvent首先调用参数mainFrame描述的一个LocalFrame对象的成员函数eventHandler获得一个EventHandler对象,接着再调用这个EventHandler对象的成员函数handleTouchEvent将处参数event描述的Touch事件,如下所示:

bool EventHandler::handleTouchEvent(const PlatformTouchEvent& event)
{
    ......

    const Vector<PlatformTouchPoint>& points = event.touchPoints();  

    unsigned i;
    ......
    bool allTouchReleased = true;
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
            allTouchReleased = false;
    }

    ......

    // First do hit tests for any new touch points.
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        
        if (point.state() == PlatformTouchPoint::TouchPressed) {
            HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
            LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->windowToContents(point.pos()));
            HitTestResult result;
            if (!m_touchSequenceDocument) {
                result = hitTestResultAtPoint(pagePoint, hitType);
            } else if (m_touchSequenceDocument->frame()) {
                LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->windowToContents(point.pos()));
                result = hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
            } else
                continue;

            Node* node = result.innerNode();
            ......

            if (!m_touchSequenceDocument) {
                // Keep track of which document should receive all touch events
                // in the active sequence. This must be a single document to
                // ensure we don't leak Nodes between documents.
                m_touchSequenceDocument = &(result.innerNode()->document());
                ......
            }

            ......

            m_targetForTouchID.set(point.id(), node);

            ......
        }
    }

    ......

    // Holds the complete set of touches on the screen.
    RefPtrWillBeRawPtr<TouchList> touches = TouchList::create();

    // A different view on the 'touches' list above, filtered and grouped by
    // event target. Used for the 'targetTouches' list in the JS event.
    typedef WillBeHeapHashMap<EventTarget*, RefPtrWillBeMember<TouchList> > TargetTouchesHeapMap;
    TargetTouchesHeapMap touchesByTarget;

    // Array of touches per state, used to assemble the 'changedTouches' list.
    typedef WillBeHeapHashSet<RefPtrWillBeMember<EventTarget> > EventTargetSet;
    struct {
        // The touches corresponding to the particular change state this struct
        // instance represents.
        RefPtrWillBeMember<TouchList> m_touches;
        // Set of targets involved in m_touches.
        EventTargetSet m_targets;
    } changedTouches[PlatformTouchPoint::TouchStateEnd];

    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        PlatformTouchPoint::State pointState = point.state();
        RefPtrWillBeRawPtr<EventTarget> touchTarget = nullptr;

        if (pointState == PlatformTouchPoint::TouchReleased || pointState == PlatformTouchPoint::TouchCancelled) {
            // The target should be the original target for this touch, so get
            // it from the hashmap. As it's a release or cancel we also remove
            // it from the map.
            touchTarget = m_targetForTouchID.take(point.id());
        } else {
            // No hittest is performed on move or stationary, since the target
            // is not allowed to change anyway.
            touchTarget = m_targetForTouchID.get(point.id());
        }

        LocalFrame* targetFrame = 0;
        bool knownTarget = false;
        if (touchTarget) {
            Document& doc = touchTarget->toNode()->document();
            // If the target node has moved to a new document while it was being touched,
            // we can't send events to the new document because that could leak nodes
            // from one document to another. See http://crbug.com/394339.
            if (&doc == m_touchSequenceDocument.get()) {
                targetFrame = doc.frame();
                knownTarget = true;
            }
        }

        ......

        RefPtrWillBeRawPtr<Touch> touch = Touch::create(
            targetFrame, touchTarget.get(), point.id(), point.screenPos(), adjustedPagePoint, adjustedRadius, point.rotationAngle(), point.force());

        ......

        // Ensure this target's touch list exists, even if it ends up empty, so
        // it can always be passed to TouchEvent::Create below.
        TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        if (targetTouchesIterator == touchesByTarget.end()) {
            touchesByTarget.set(touchTarget.get(), TouchList::create());
            targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        }

        // touches and targetTouches should only contain information about
        // touches still on the screen, so if this point is released or
        // cancelled it will only appear in the changedTouches list.
        if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
            touches->append(touch);
            targetTouchesIterator->value->append(touch);
        }

        // Now build up the correct list for changedTouches.
        // Note that  any touches that are in the TouchStationary state (e.g. if
        // the user had several points touched but did not move them all) should
        // never be in the changedTouches list so we do not handle them
        // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
        // for further discussion about the TouchStationary state.
        if (pointState != PlatformTouchPoint::TouchStationary && knownTarget) {
            ......
            if (!changedTouches[pointState].m_touches)
                changedTouches[pointState].m_touches = TouchList::create();
            changedTouches[pointState].m_touches->append(touch);
            changedTouches[pointState].m_targets.add(touchTarget);
        }
    }
    if (allTouchReleased) {
        m_touchSequenceDocument.clear();
        ......
    }

    ......

    // Now iterate the changedTouches list and m_targets within it, sending
    // events to the targets as required.
    bool swallowedEvent = false;
    for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
        ......

        const AtomicString& stateName(eventNameForTouchPointState(static_cast<PlatformTouchPoint::State>(state)));
        const EventTargetSet& targetsForState = changedTouches[state].m_targets;
        for (EventTargetSet::const_iterator it = targetsForState.begin(); it != targetsForState.end(); ++it) {
            EventTarget* touchEventTarget = it->get();
            RefPtrWillBeRawPtr<TouchEvent> touchEvent = TouchEvent::create(
                touches.get(), touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
                stateName, touchEventTarget->toNode()->document().domWindow(),
                event.ctrlKey(), event.altKey(), event.shiftKey(), event.metaKey(), event.cancelable());
            touchEventTarget->toNode()->dispatchTouchEvent(touchEvent.get());
            swallowedEvent = swallowedEvent || touchEvent->defaultPrevented() || touchEvent->defaultHandled();
        }
    }

    return swallowedEvent;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/page/EventHandler.cpp中。

       EventHandler类的成员函数handleTouchEvent主要是做三件事情:

       1. 对当前发生的Touch事件的每一个Touch Point进行Hit Test,分别找到它们的Target Node。

       2. 对Touch Point进行分类。还与屏幕接触是一类,不再与屏幕接触是另一类。与屏幕接触的一类又划分为两个子类。一个子类是静止不动的,另一个子类是正在移动的。另外,目标Node相同的Touch Point也会被组织在同一个Touch List中。

       3. 将Touch事件分发给Target Node处理。

       接下来,我们就将EventHandler类的成员函数handleTouchEvent划分为三段进行分析,每一段对应于上述的一个事件。

       第一段代码如下所示:

bool EventHandler::handleTouchEvent(const PlatformTouchEvent& event)
{
    ......

    const Vector<PlatformTouchPoint>& points = event.touchPoints();  

    unsigned i;
    ......
    bool allTouchReleased = true;
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
            allTouchReleased = false;
    }

    ......

    // First do hit tests for any new touch points.
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        
        if (point.state() == PlatformTouchPoint::TouchPressed) {
            HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
            LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->windowToContents(point.pos()));
            HitTestResult result;
            if (!m_touchSequenceDocument) {
                result = hitTestResultAtPoint(pagePoint, hitType);
            } else if (m_touchSequenceDocument->frame()) {
                LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->windowToContents(point.pos()));
                result = hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
            } else
                continue;

            Node* node = result.innerNode();
            ......

            if (!m_touchSequenceDocument) {
                // Keep track of which document should receive all touch events
                // in the active sequence. This must be a single document to
                // ensure we don't leak Nodes between documents.
                m_touchSequenceDocument = &(result.innerNode()->document());
                ......
            }

            ......

            m_targetForTouchID.set(point.id(), node);

            ......
        }
    }

       这段代码首先获得参数event描述的Touch事件关联的所有Touch Point,保存在本地变量points描述的一个Vector中。

       每一个Touch Point都用一个PlatformTouchPoint对象描述,它有五种状态,如下所示:

class PlatformTouchPoint {
public:
    enum State {
        TouchReleased,
        TouchPressed,
        TouchMoved,
        TouchStationary,
        TouchCancelled,
        TouchStateEnd // Placeholder: must remain the last item.
    };

    ......
};
       这些状态定义在文件external/chromium_org/third_party/WebKit/Source/platform/PlatformTouchPoint.h中。

       一个Touch Point的一般状态变化过程为:TouchPressed->TouchMoved/TouchStationary->TouchReleased/TouchCancelled。

       回到上面第一段代码中,它主要做的事情就是对那些状态为TouchPressed的Touch Point进行Hit Test,目的是找到它们的Target Node,并且将这些Target Node保存在EventHandler类的成员变量m_targetForTouchID描述的一个Hash Map中,键值为Touch Point对应的ID。有了这个Hash Map,当一个Touch Point从TouchPressed状态变为其它状态时,就可以轻松地知道它的Target Node,避免做重复的Hit Test。

       一系列连续的Touch Event只能发生在一个Document上。如果两个Touch Event有两个或者两个以上的Touch Point具有相同的ID,那么它们就是连续的Touch Event。它们所发生在的Document由第一个连续的Touch Event的第一个处于TouchPressed状态的Touch Point确定,也就是这个Touch Point的Target Node所在的Document。这个Document一旦确定,就会维护在EventHandler类的成员变量m_touchSequenceDocument中。

       当一个Touch Event的所有Touch Point的状态都处于TouchReleased或者TouchCancelled时,它就结束一个连续的Touch Event系列。这时候EventHandler类的成员变量m_touchSequenceDocument就会设置为NULL,表示接下来发生的Touch Event属于另外一个连续的系列。

       当一个Touch Event所在的Document所未确定时,EventHandler类的成员函数handleTouchEvent调用成员函数hitTestResultAtPoint对第一个Touch Point做Hit Test;当一个Touch Event所在的Document确定时,则调用另外一个成员函数hitTestResultInFrame做Hit Test。后者会将Hit Test的范围限制在指定的Document中。后面我们将以EventHandler类的成员函数hitTestResultAtPoint为例,分析Hit Test的执行过程。

       第二段代码如下所示:

    // Holds the complete set of touches on the screen.
    RefPtrWillBeRawPtr<TouchList> touches = TouchList::create();

    // A different view on the 'touches' list above, filtered and grouped by
    // event target. Used for the 'targetTouches' list in the JS event.
    typedef WillBeHeapHashMap<EventTarget*, RefPtrWillBeMember<TouchList> > TargetTouchesHeapMap;
    TargetTouchesHeapMap touchesByTarget;

    // Array of touches per state, used to assemble the 'changedTouches' list.
    typedef WillBeHeapHashSet<RefPtrWillBeMember<EventTarget> > EventTargetSet;
    struct {
        // The touches corresponding to the particular change state this struct
        // instance represents.
        RefPtrWillBeMember<TouchList> m_touches;
        // Set of targets involved in m_touches.
        EventTargetSet m_targets;
    } changedTouches[PlatformTouchPoint::TouchStateEnd];

    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        PlatformTouchPoint::State pointState = point.state();
        RefPtrWillBeRawPtr<EventTarget> touchTarget = nullptr;

        if (pointState == PlatformTouchPoint::TouchReleased || pointState == PlatformTouchPoint::TouchCancelled) {
            // The target should be the original target for this touch, so get
            // it from the hashmap. As it's a release or cancel we also remove
            // it from the map.
            touchTarget = m_targetForTouchID.take(point.id());
        } else {
            // No hittest is performed on move or stationary, since the target
            // is not allowed to change anyway.
            touchTarget = m_targetForTouchID.get(point.id());
        }

        LocalFrame* targetFrame = 0;
        bool knownTarget = false;
        if (touchTarget) {
            Document& doc = touchTarget->toNode()->document();
            // If the target node has moved to a new document while it was being touched,
            // we can't send events to the new document because that could leak nodes
            // from one document to another. See http://crbug.com/394339.
            if (&doc == m_touchSequenceDocument.get()) {
                targetFrame = doc.frame();
                knownTarget = true;
            }
        }

        ......

        RefPtrWillBeRawPtr<Touch> touch = Touch::create(
            targetFrame, touchTarget.get(), point.id(), point.screenPos(), adjustedPagePoint, adjustedRadius, point.rotationAngle(), point.force());

        ......

        // Ensure this target's touch list exists, even if it ends up empty, so
        // it can always be passed to TouchEvent::Create below.
        TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        if (targetTouchesIterator == touchesByTarget.end()) {
            touchesByTarget.set(touchTarget.get(), TouchList::create());
            targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        }

        // touches and targetTouches should only contain information about
        // touches still on the screen, so if this point is released or
        // cancelled it will only appear in the changedTouches list.
        if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
            touches->append(touch);
            targetTouchesIterator->value->append(touch);
        }

        // Now build up the correct list for changedTouches.
        // Note that  any touches that are in the TouchStationary state (e.g. if
        // the user had several points touched but did not move them all) should
        // never be in the changedTouches list so we do not handle them
        // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
        // for further discussion about the TouchStationary state.
        if (pointState != PlatformTouchPoint::TouchStationary && knownTarget) {
            ......
            if (!changedTouches[pointState].m_touches)
                changedTouches[pointState].m_touches = TouchList::create();
            changedTouches[pointState].m_touches->append(touch);
            changedTouches[pointState].m_targets.add(touchTarget);
        }
    }
    if (allTouchReleased) {
        m_touchSequenceDocument.clear();
        ......
    }
       这段代码主要是对Touch Point进行分门别类。

       首先,那些还与屏幕有接触的Touch Point将会保存在本地变量touches描述的一个Touch List中。那些状态不等于TouchReleased和TouchCancelled的Touch Point即为还与屏幕有接触的Touch Point。

       其次,具有相同Target Node的Touch Point又会保存在相同的Touch List中。这些Touch List它们关联的Target Node为键值,保存在本地变量touchesByTarget描述的一个Hash Map中。

       第三,那些位置或者状态发生变化,并且有Target Node的Touch Point会按照状态保存在本地变量changedTouches描述的一个数组中。相同状态的Touch Point保存在同一个Touch List中,它们的Target Node也会保存在同一个Hash Set中。位置或者状态发生变化的Touch Point,即为那些状态不等于TouchStationary的Touch Point。另外,如果一个Touch Point的Target Node所在的Document与当前Touch Event所发生在的Document不一致,那么该Touch Point会被认为是没有Target Node。

       这段代码还会做另外两件事情:

       1. 如果一个Touch Point的状态变为TouchReleased或者TouchCancelled,那么它就会从EventHandler类的成员变量m_targetForTouchID描述的一个Hash Map中移除。结合前面对第一段代码的分析,我们就可以知道,一个连续的Touch Event系列,它关联的Touch Point是会动态增加和移除的。

       2. 如果当前发生的Touch Event的所有Touch Point的状态均为TouchReleased或者TouchCancelled,那么当前连续的Touch Event系列就会结束。这时候EventHandler类的成员变量m_touchSequenceDocument将被设置为NULL。

       第三段代码如下所示:

   // Now iterate the changedTouches list and m_targets within it, sending
    // events to the targets as required.
    bool swallowedEvent = false;
    for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
        ......

        const AtomicString& stateName(eventNameForTouchPointState(static_cast<PlatformTouchPoint::State>(state)));
        const EventTargetSet& targetsForState = changedTouches[state].m_targets;
        for (EventTargetSet::const_iterator it = targetsForState.begin(); it != targetsForState.end(); ++it) {
            EventTarget* touchEventTarget = it->get();
            RefPtrWillBeRawPtr<TouchEvent> touchEvent = TouchEvent::create(
                touches.get(), touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
                stateName, touchEventTarget->toNode()->document().domWindow(),
                event.ctrlKey(), event.altKey(), event.shiftKey(), event.metaKey(), event.cancelable());
            touchEventTarget->toNode()->dispatchTouchEvent(touchEvent.get());
            swallowedEvent = swallowedEvent || touchEvent->defaultPrevented() || touchEvent->defaultHandled();
        }
    }

    return swallowedEvent;
}
       这段代码将Touch Event分发给Target Node处理。注意,并不是所有的Target Node都会被分发Touch Event。只有那些Touch Point位置或者状态发生了变化的Target Node才会获得Touch Event。

       这段代码按照Touch Point的状态分发Touch Event给Target Node处理,顺序为TouchReleased->TouchPressed->TouchMoved->TouchCancelled。如果具有相同状态的Touch Point关联了不同的Target Node,那么每一个Target Node都会获得一个Touch Event。

       每一个Target Node获得的Touch Event是不同的TouchEvent对象,每一个TouchEvent对象包含了以下三种信息:

       1. 所有与屏幕接触的Touch Point。这些Touch Point有的位于Target Node的范围内,有的可能位于Target Node的范围外。

       2. 位于Target Node的范围内的Touch Point。

       3. 具有相同状态的Touch Point。

       注意,这些Touch Point限定在当前发生的Touch Event关联的Touch Point中,也就是限定在参数event描述的Touch Event关联的Touch Point中。

       EventHandler类的成员函数是通过Target Node的成员函数handleTouchEvent给它们分发Touch Event,也就是通过调用Node类的成员函数handleTouchEvent将Touch Event分发给Target Node处理。

       接下来,我们就继续分析EventHandler类的成员函数hitTestResultAtPoint和Node类的成员函数handleTouchEvent的实现,以便了解WebKit根据Touch Point找到Target Node和将Touch Event分发给Target Node处理的过程。

       EventHandler类的成员函数hitTestResultAtPoint的实现如下所示:

HitTestResult EventHandler::hitTestResultAtPoint(const LayoutPoint& point, HitTestRequest::HitTestRequestType hitType, const LayoutSize& padding)
{
    ......

    // We always send hitTestResultAtPoint to the main frame if we have one,
    // otherwise we might hit areas that are obscured by higher frames.
    if (Page* page = m_frame->page()) {
        LocalFrame* mainFrame = page->mainFrame()->isLocalFrame() ? page->deprecatedLocalMainFrame() : 0;
        if (mainFrame && m_frame != mainFrame) {
            FrameView* frameView = m_frame->view();
            FrameView* mainView = mainFrame->view();
            if (frameView && mainView) {
                IntPoint mainFramePoint = mainView->rootViewToContents(frameView->contentsToRootView(roundedIntPoint(point)));
                return mainFrame->eventHandler().hitTestResultAtPoint(mainFramePoint, hitType, padding);
            }
        }
    }

    HitTestResult result(point, padding.height(), padding.width(), padding.height(), padding.width());
    ......

    // hitTestResultAtPoint is specifically used to hitTest into all frames, thus it always allows child frame content.
    HitTestRequest request(hitType | HitTestRequest::AllowChildFrameContent);
    m_frame->contentRenderer()->hitTest(request, result);
    ......

    return result;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/page/EventHandler.cpp中。

       EventHandler类的成员函数hitTestResultAtPoint首先会检查当前正在处理的网页的Main Frame是否是一个Local Frame。如果是的话,那么就会在它上面做Hit Test。否则的话,就在当前正在处理的网页所加载在的Frame上做Hit Test,也就是EventHandler类的成员变量m_frame描述的Frame。这个Frame是一个Local Frame。

       前面Chromium网页加载过程简要介绍和学习计划一文提到,在WebKit中,一个Frame有可能是Local的,也可能是Remote的。Local Frame就在当前进程加载一个网页,而Remote Frame在另外一个进程加载一个网页。Frame与Frame之间会形成一个Frame Tree。Frame Tree的根节点就是一个Main Frame。结合前面分析的EventHandler类的成员函数handleTouchEvent,我们就可以知道,如果当前正在处理的网页不是加载在一个Main Frame上,并且这个网页的Main Frame是一个Local Frame,那么发生在这个网页上的Touch Event将会在它的Main Frame上做Hit Test。在其余情况下,发生在当前正在处理的网页上的Touch Event,将会在该网页所加载在的Frame上做Hit Test。

       一旦确定在哪个Frame上做Hit Test之后,EventHandler类的成员函数hitTestResultAtPoint就会调用这个Frame的成员函数contentRenderer获得一个RenderView对象。从前面Chromium网页Render Object Tree创建过程分析一文可以知道,这个RenderView对象描述的就是当前正在处理的网页的Render Object Tree的根节点。有了这个RenderView对象之后,EventHandler类的成员函数hitTestResultAtPoint就会调用它的成员函数hitTest对参数point描述的Touch Point进行Hit Test。

       RenderView类的成员函数hitTest的实现如下所示:

bool RenderView::hitTest(const HitTestRequest& request, HitTestResult& result)
{
    return hitTest(request, result.hitTestLocation(), result);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。

       RenderView类的成员函数hitTest调用另外一个重载版本的成员函数hitTest对参数result描述的Touch Point进行Hit Test,如下所示:

bool RenderView::hitTest(const HitTestRequest& request, const HitTestLocation& location, HitTestResult& result)
{
    ......

    return layer()->hitTest(request, location, result);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。

       RenderView类重载版本的成员函数hitTest首先会调用另外一个成员函数layer获得一个RenderLayer对象。从前面Chromium网页Render Layer Tree创建过程分析一文可以知道,这个RenderLayer对象描述的就是网页的Render Layer Tree的根节点。有了这个RenderLayer对象之后,RenderView类重载版本的成员函数hitTest就会调用它的成员函数hitTest对象参数result描述的Touch Point进行Hit Test,如下所示:

bool RenderLayer::hitTest(const HitTestRequest& request, const HitTestLocation& hitTestLocation, HitTestResult& result)
{
    ......

    LayoutRect hitTestArea = renderer()->view()->documentRect();
    ......

    RenderLayer* insideLayer = hitTestLayer(this, 0, request, result, hitTestArea, hitTestLocation, false);
    ......

    return insideLayer;
}

      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

      RenderLayer类的成员函数hitTest首先是获得网页的Document对象占据的区域hitTestArea,然后再调用另外一个成员函数hitTestLayer在该区域上对参数hitTestLocation描述的Touch Point进行Hit Test,如下所示:

RenderLayer* RenderLayer::hitTestLayer(RenderLayer* rootLayer, RenderLayer* containerLayer, const HitTestRequest& request, HitTestResult& result,
                                       const LayoutRect& hitTestRect, const HitTestLocation& hitTestLocation, bool appliedTransform,
                                       const HitTestingTransformState* transformState, double* zOffset)
{
    ......

    // Ensure our lists and 3d status are up-to-date.
    m_stackingNode->updateLayerListsIfNeeded();
    update3DTransformedDescendantStatus();
    ......

    // The following are used for keeping track of the z-depth of the hit point of 3d-transformed
    // descendants.
    double localZOffset = -numeric_limits<double>::infinity();
    double* zOffsetForDescendantsPtr = 0;
    double* zOffsetForContentsPtr = 0;

    bool depthSortDescendants = false;
    if (preserves3D()) {
        depthSortDescendants = true;
        // Our layers can depth-test with our container, so share the z depth pointer with the container, if it passed one down.
        zOffsetForDescendantsPtr = zOffset ? zOffset : &localZOffset;
        zOffsetForContentsPtr = zOffset ? zOffset : &localZOffset;
    } else if (m_has3DTransformedDescendant) {
        // Flattening layer with 3d children; use a local zOffset pointer to depth-test children and foreground.
        depthSortDescendants = true;
        zOffsetForDescendantsPtr = zOffset ? zOffset : &localZOffset;
        zOffsetForContentsPtr = zOffset ? zOffset : &localZOffset;
    } else if (zOffset) {
        zOffsetForDescendantsPtr = 0;
        // Container needs us to give back a z offset for the hit layer.
        zOffsetForContentsPtr = zOffset;
    }

    ......

    // This variable tracks which layer the mouse ends up being inside.
    RenderLayer* candidateLayer = 0;

    // Begin by walking our list of positive layers from highest z-index down to the lowest z-index.
    RenderLayer* hitLayer = hitTestChildren(PositiveZOrderChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
                                        localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
    if (hitLayer) {
        if (!depthSortDescendants)
            return hitLayer;
        candidateLayer = hitLayer;
    }

    // Now check our overflow objects.
    hitLayer = hitTestChildren(NormalFlowChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
                           localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
    if (hitLayer) {
        if (!depthSortDescendants)
            return hitLayer;
        candidateLayer = hitLayer;
    }

    ......

    // Next we want to see if the mouse pos is inside the child RenderObjects of the layer. Check
    // every fragment in reverse order.
    if (isSelfPaintingLayer()) {
        // Hit test with a temporary HitTestResult, because we only want to commit to 'result' if we know we're frontmost.
        HitTestResult tempResult(result.hitTestLocation());
        bool insideFragmentForegroundRect = false;
        if (hitTestContentsForFragments(layerFragments, request, tempResult, hitTestLocation, HitTestDescendants, insideFragmentForegroundRect)
            && isHitCandidate(this, false, zOffsetForContentsPtr, unflattenedTransformState.get())) {
            ......
            if (!depthSortDescendants)
                return this;
            // Foreground can depth-sort with descendant layers, so keep this as a candidate.
            candidateLayer = this;
        } 
    }

    // Now check our negative z-index children.
    hitLayer = hitTestChildren(NegativeZOrderChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
        localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
    if (hitLayer) {
        if (!depthSortDescendants)
            return hitLayer;
        candidateLayer = hitLayer;
    }

    ......

    // If we found a layer, return. Child layers, and foreground always render in front of background.
    if (candidateLayer)
        return candidateLayer;

    if (isSelfPaintingLayer()) {
        HitTestResult tempResult(result.hitTestLocation());
        bool insideFragmentBackgroundRect = false;
        if (hitTestContentsForFragments(layerFragments, request, tempResult, hitTestLocation, HitTestSelf, insideFragmentBackgroundRect)
            && isHitCandidate(this, false, zOffsetForContentsPtr, unflattenedTransformState.get())) {
            ......
            return this;
        }
        ......
    }

    return 0;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

       RenderLayer类的成员变量m_stackingNode指向的是一个RenderLayerStackingNode对象。这个RenderLayerStackingNode对象描述了当前正在处理的Render Layer所在的Stacking Context。RenderLayer类的成员函数hitTestLayer首先调用这个RenderLayerStackingNode对象的成员函数updateLayerListsIfNeeded更新它所描述的Stacking Context所包含的Render Layer的层次关系,以便接下来可以按照它们的Z轴位置进行Hit Test。

       RenderLayer类的成员函数hitTestLayer同时还会调用另外一个成员函数update3DTransformedDescendantStatus检查当前正在处理的Render Layer的子Render Layer是否设置了3D。如果设置了,那么RenderLayer类的成员变量m_has3DTransformedDescendant就会被设置为true。3D变换使得Hit Test不能简单地按照原来Z-Index的大小进行Hit Test。

       RenderLayer类的成员函数hitTestLayer接下来根据两种不同的情况,采取两种不同的Hit Test方法:

       1. 当前正在处理的Render Layer将CSS属性tranform-type设置为“preserve-3d”,或者它的子Render Layer设置了3D变换。在这种情况下,本地变量depthSortDescendants的值会被设置为true,并且另外两个本地变量zOffsetForDescendantsPtr和zOffsetForContentsPtr指向了一个类型为double的地址。这个地址包含的double值描述的是上一个被Hit的Render Layer在Touch Point处的Z轴位置。其中,本地变量zOffsetForContentsPtr描述的Z轴位置是给当前正在处理的Render Layer使用的,而本地变量zOffsetForDescendantsPtr描述的Z轴位置是给当前正在处理的Render Layer的子Render Layer使用的。在一个设置了3D变换的环境中,Z-Index值大的Render Layer不一定位于Z-Index值小的Render Layer的上面,需要进一步结合它们的3D变换情况进行判断。因此,就需要将上一个被Hit的Render Layer在Touch Point处的Z轴位置保存下来,用来与下一个也被Hit的Render Layer进行比较,以便得出正确的被Hit的Render Layer。

       2. 当前正在处理的Render Layer没有将CSS属性tranform-type设置为“preserve-3d”,以及它的子Render Layer也没有设置3D变换。在这种情况下,本地变量depthSortDescendants的值会被设置为false。另外两个本地变量zOffsetForDescendantsPtr和zOffsetForContentsPtr,前者被设置为NULL,后者设置为参数zOffset的值。将本地变量zOffsetForDescendantsPtr设置为NULL,是因为当前正在处理的Render Layer的子Render Layer在做Hit Test时,不需要与其它的子Render Layer在Touch Point处进行Z轴位置。将zOffsetForContentsPtr的值指定为参数zOffset的值,是因为调用者可能会指定一个Z轴位置,要求当前正在处理的Render Layer在Touch Point处与其进行比较。

       对第二种情况的处理比较简单,流程如下所示: 

       1. 按照Z-Index从大到小的顺序对Z-Index值大于等于0的子Render Layer进行Hit Test。如果发生了Hit,那么停止Hit Test流程。

       2. 对当前正在处理的Render Layer的Foreground层进行Hit Test。如果发生了Hit,那么停止Hit Test流程。

       3. 按照Z-Index从大到小的顺序对Z-Index值小于0的子Render Layer进行Hit Test。如果发生了Hit,那么停止Hit Test流程。

       4. 对当前正在处理的Render Layer关联的Render Object的Background层进行Hit Test。

       对第一种情况的处理相对就会复杂一些,如下所示:

       1. 对当前正在处理的Render Layer的所有子Render Layer,以及当前正在处理的Render Layer的Foreground层,都会一一进行Hit Test。在这个Hit Test过程中,所有被Hit的Render Layer,都会根据它们3D变换情况,检查它们在Touch Point处的Z轴位置。Z轴位置最大的Render Layer或者Render Object,将会选择用来接收Touch Event。

       2. 如果所有子Render Layer和当前正在处理的Render Layer的Foreground都没有发生Hit,那么就会再对当前正在处理的Render Layer的Background层进行Hit Test。

       注意,对于每一个子Render Layer,RenderLayer类的成员函数hitTestLayer就会调用另外一个成员函数hitTestChildren对分别对它们进行Hit Test。RenderLayer类的成员函数hitTestChildren又会调用hitTestLayer对每一个子Render Layer执行具体的Hit Test。

       这意味着,RenderLayer类的成员函数hitTestLayer会被递归调用来对Render Layer Tree中的每一个Render Layer进行Hit Test。当前正在处理的Render Layer是否被Hit,RenderLayer类的成员函数hitTestLayer是通过调用两次成员函数hitTestContentsForFragments进行检查。第一次调用是确定当前正在处理的Render Layer的Foreground层是否发生了Hit Test。第二次调用是确定当前正在处理的Render Layer的Background层是否发生了Hit Test。

        一旦检查当前正在处理的Render Layer发生了Hit,那么RenderLayer类的成员函数hitTestLayer还需要调用另外一个成员函数isHitCandidate将它在Touch Point处的Z轴位置与本地变量zOffsetForContentsPtr描述的Z轴位置(上一个被Hit的Render Layer在Touch Point的Z轴位置)进行比较。比较后如果发现当前正在处理的Render Layer在Touch Point处的Z轴位置较大,那么才会认为它是被Hit的Render Layer。

       接下来我们就继续分析RenderLayer类的成员函数hitTestContentsForFragments的实现,以便可以了解一个Render Layer在什么情况会被认为是发生了Hit,如下所示:

bool RenderLayer::hitTestContentsForFragments(const LayerFragments& layerFragments, const HitTestRequest& request, HitTestResult& result,
    const HitTestLocation& hitTestLocation, HitTestFilter hitTestFilter, bool& insideClipRect) const
{
    ......

    for (int i = layerFragments.size() - 1; i >= 0; --i) {
        const LayerFragment& fragment = layerFragments.at(i);
        ......
        if (hitTestContents(request, result, fragment.layerBounds, hitTestLocation, hitTestFilter))
            return true;
    }

    return false;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

       前面Chromium网页Layer Tree绘制过程分析一文提到,Render Layer是按Fragment进行划分的。因此,RenderLayer类的成员函数hitTestContentsForFragments分别调用另外一个成员函数hitTestContents对每一个Fragment进行Hit Test。只要其中一个Fragment发生了Hit,那么就会认为它所在的Render Layer发生了Hit。

       RenderLayer类的成员函数hitTestContents的实现如下所示:

bool RenderLayer::hitTestContents(const HitTestRequest& request, HitTestResult& result, const LayoutRect& layerBounds, const HitTestLocation& hitTestLocation, HitTestFilter hitTestFilter) const
{
    ......

    if (!renderer()->hitTest(request, result, hitTestLocation, toLayoutPoint(layerBounds.location() - renderBoxLocation()), hitTestFilter)) {
        ......
        return false;
    }

    ......

    return true;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

       RenderLayer类的成员函数hitTestContents首先调用成员函数renderer获得一个Render Object。这个Render Object是当前正在处理的Render Layer的宿主Render Object。也就是说,我们在为网页创建Render Layer Tree时,为上述Render Object创建了一个Render Layer。该Render Object的子Render Object如果没有自己的Render Layer,那么就会与该Render Object共享同一个Render Layer。这一点可以参考前面Chromium网页Render Layer Tree创建过程分析一文。 

       获得了当前正在处理的Render Layer的宿主Render Object之后,RenderLayer类的成员函数hitTestContents就调用它的成员函数hitTest检查它在参数layerBounds描述的区域内是否发生了Hit。如果发生了Hit,那么RenderLayer类的成员函数hitTestContents就直接返回一个true值给调用者。否则的话,就会返回一个false值给调用者。

       这一步执行完成之后,就从Render Layer Tree转移到Render Object Tree进行Hit Test,也就是调用RenderObject类的成员函数hitTest进行Hit Test,如下所示:

bool RenderObject::hitTest(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestFilter hitTestFilter)
{
    bool inside = false;
    if (hitTestFilter != HitTestSelf) {
        // First test the foreground layer (lines and inlines).
        inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestForeground);

        // Test floats next.
        if (!inside)
            inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestFloat);

        // Finally test to see if the mouse is in the background (within a child block's background).
        if (!inside)
            inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestChildBlockBackgrounds);
    }

    // See if the mouse is inside us but not any of our descendants
    if (hitTestFilter != HitTestDescendants && !inside)
        inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestBlockBackground);

    return inside;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderObject.cpp中。

       从前面分析的RenderLayer类的成员函数hitTestLayer可以知道,一个Reder Layer关联的Render Object会进行两次Hit Test。第一次是针对该Render Object的Foreground进行Hit Test,这时候参数hitTestFilter的值等于HitTestDescendants。第二次是针对该Render Object的Background层进行Hit Test,这时候参数hitTestFilter的值等于HitTestSelf。

       无论是Foreground层,还是Background层,RenderObject类的成员函数hitTest都是通过调用另外一个成员函数nodeAtPoint进行Hit Test的。RenderObject类的成员函数nodeAtPoint是从父类RenderBox继承下来的,它的实现如下所示:

bool RenderBox::nodeAtPoint(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestAction action)
{
    LayoutPoint adjustedLocation = accumulatedOffset + location();

    // Check kids first.
    for (RenderObject* child = slowLastChild(); child; child = child->previousSibling()) {
        if ((!child->hasLayer() || !toRenderLayerModelObject(child)->layer()->isSelfPaintingLayer()) && child->nodeAtPoint(request, result, locationInContainer, adjustedLocation, action)) {
            updateHitTestResult(result, locationInContainer.point() - toLayoutSize(adjustedLocation));
            return true;
        }
    }

    // Check our bounds next. For this purpose always assume that we can only be hit in the
    // foreground phase (which is true for replaced elements like images).
    LayoutRect boundsRect = borderBoxRect();
    boundsRect.moveBy(adjustedLocation);
    if (visibleToHitTestRequest(request) && action == HitTestForeground && locationInContainer.intersects(boundsRect)) {
        updateHitTestResult(result, locationInContainer.point() - toLayoutSize(adjustedLocation));
        if (!result.addNodeToRectBasedTestResult(node(), request, locationInContainer, boundsRect))
            return true;
    }

    return false;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderBox.cpp中。

       RenderBox类的成员函数nodeAtPoint首先对当前正在处理的Render Object的子Render Object进行Hit Test。这是通过递归调用RenderBox类的成员函数nodeAtPoint实现的。如果子Render Object没有被Hit,那么RenderBox类的成员函数才会判断当前正在处理的Render Object是否发生Hit,也就是判断参数locationInContainer描述的Touch Point是否落在当前正在处理的Render Object的区域boundsRect内。

       这里有一点需要注意,当前正在处理的Render Object的每一个子Render Objec并不是都会被递归Hit Test。只有那些没有创建Render Layer的子Render Object才会进行递归Hit Test。这些没有创建自己的Render Layer的子Render Object将会与当前正在处理的Render Object共享同一个Render Layer。对于那些有自己的Render Layer的子Render Object,它们的Hit Test将由前面分析的RenderLayer类的成员函数hitTestLayer发起。

       当一个Render Object发生Hit时,RenderBox类的成员函数nodeAtPoint就会调用另外一个成员函数updateHitTestResult将Hit信息记录在参数result描述的一个HitTestResult对象中。RenderBox类的成员函数updateHitTestResult由子类RenderObject实现,如下所示:

void RenderObject::updateHitTestResult(HitTestResult& result, const LayoutPoint& point)
{
    ......

    Node* node = this->node();

    // If we hit the anonymous renderers inside generated content we should
    // actually hit the generated content so walk up to the PseudoElement.
    if (!node && parent() && parent()->isBeforeOrAfterContent()) {
        for (RenderObject* renderer = parent(); renderer && !node; renderer = renderer->parent())
            node = renderer->node();
    }

    if (node) {
        result.setInnerNode(node);
        ......
    }
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderObject.cpp中。

       RenderObject类的成员函数updateHitTestResult首先调用成员函数node获得与当前正在处理的Render Object关联的HTML元素,也就是位于网页的DOM Tree中的一个Node,作为当前发生的Touch Event的Target Node。

       如果当前正在处理的Render Object没有关联一个HTML元素,那么就说明当前正在处理的Render Object是一个匿名的Render Object。这时候需要在网页的Render Object Tree中找到一个负责生成它的、非匿名的父Render Object,然后再获得与这个父Render Object关联的HTML元素,作为当前发生的Touch Event的Target Node。

       一旦找到了Target Node,RenderObject类的成员函数updateHitTestResult就会将它保存在参数result描述的一个HitTestResult对象中。这是通过调用HitTestResult类的成员函数setInnerNode实现的,如下所示:

void HitTestResult::setInnerNode(Node* n)
{
    ......
    m_innerNode = n;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/HitTestResult.cpp中。

       HitTestResult类的成员函数setInnerNode主要是将参数n描述的一个HTML元素保存在成员变量m_innerNode中。这个HTML元素可以通过调用HitTestResult类的成员函数innerNode获得,如下所示:

class HitTestResult {
public:
    ......

    Node* innerNode() const { return m_innerNode.get(); }

    ......
};
      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/HitTestResult.h中。

      这一步执行完成后,WebKit就通过网页的Render Layer Tree和Render Object Tree,最终在DOM Tree中找到了Target Node。这个Target Node负责接收和处理当前发生的Touch Event。

      回到前面分析的EventHandler类的成员函数handleTouchEvent中,接下来它就会将当前发生的Touch Event分发给前面找到的Target Node处理,这是通过调用它的成员函数dispatchTouchEvent实现的,如下所示:

bool Node::dispatchTouchEvent(PassRefPtrWillBeRawPtr<TouchEvent> event)
{
    return EventDispatcher::dispatchEvent(this, TouchEventDispatchMediator::create(event));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Node.cpp中。

       Node类的成员函数dispatchTouchEvent首先调用TouchEventDispatchMediator类的静态成员函数create将参数描述的Touch Event封装在一个TouchEventDispatchMediator对象中,然的再调用EventDispatcher类的静态成员函数dispatchEvent对该Touch Event进行处理,如下所示:

bool EventDispatcher::dispatchEvent(Node* node, PassRefPtrWillBeRawPtr<EventDispatchMediator> mediator)
{
    ......
    EventDispatcher dispatcher(node, mediator->event());
    return mediator->dispatchEvent(&dispatcher);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。

       EventDispatcher类的静态成员函数dispatchEvent首先从参数mediator描述的TouchEventDispatchMediator对象中取出它所封装的Touch Event,并且将该Touch Event封装在一个EventDispatcher对象中,最后调用参数mediator描述的TouchEventDispatchMediator对象的成员函数dispatchEvent对上述Touch Event进行分发处理,如下所示:

bool TouchEventDispatchMediator::dispatchEvent(EventDispatcher* dispatcher) const
{
    event()->eventPath().adjustForTouchEvent(dispatcher->node(), *event());
    return dispatcher->dispatch();
}

       这个函数定义在文件/external/chromium_org/third_party/WebKit/Source/core/events/TouchEvent.cpp中。

       TouchEventDispatchMediator类的成员函数dispatchEvent首先调用成员函数event获得一个TouchEvent对象。这个TouchEvent描述的就是当前发生的Touch Event。调用这个TouchEvent对象的成员函数eventPath可以获得一个EventPath对象。这个EventPath对象描述的是当前发生的Touch Event的分发路径。关于WebKit中的Event分发路径,下面我们再描述。有了这个EventPath对象之后,TouchEventDispatchMediator类的成员函数dispatchEvent调用它的成员函数adjustForTouchEvent调整Touch Event分发路径中的Shadow DOM的Touch List。关于Shadow DOM的更详细描述,可以参考What the Heck is Shadow DOM这篇文章。

        TouchEventDispatchMediator类的成员函数dispatchEvent最后调用参数dispatcher描述的EventDispatcher对象的成员函数dispatch处理它所封装的Touch Event,如下所示:

bool EventDispatcher::dispatch()
{
    .......

    void* preDispatchEventHandlerResult;
    if (dispatchEventPreProcess(preDispatchEventHandlerResult) == ContinueDispatching)
        if (dispatchEventAtCapturing(windowEventContext) == ContinueDispatching)
            if (dispatchEventAtTarget() == ContinueDispatching)
                dispatchEventAtBubbling(windowEventContext);
    dispatchEventPostProcess(preDispatchEventHandlerResult);

    ......

    return !m_event->defaultPrevented();
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。

       在网页发生的一个Event,分为五个阶段进行分发处理:

       1. Pre Process

       2. Capturing

       3. Target

       4. Bubbling

       5. Post Process

       我们通过一个例子说明这五个阶段的处理过程,如下所示:

<html>
    <body>
        <div id="div1"> 
            <div id="div2">
                <div id="div3">
                </div>
            </div>
        </div>
    </body>
</html>
       假设在div3上发生了一个Touch Event。

       在Pre Process阶段,WebKit会将Touch Event分发给div3的Pre Dispatch Event Handler处理,让div3有机会在DOM Event Handler处理Touch Event之前做一些事情,用来实现自己的行为。

       在Capturing阶段,WebKit会将Touch Event依次分发给html -> body -> div1 -> div2的DOM Event Handler处理。

       在Target阶段,WebKit会将Touch Event依次分发给div3的DOM Event Handler处理。。

       在Bubbling阶段, WebKit会将Touch Event依次分发给div2 -> div1 -> body -> html的DOM Event Handler处理。

       在Post Process阶段,WebKit会将Touch Event分发给div3的Post Dispatch Event Handler处理,让div3有机会在DOM Event Handler处理Touch Event之后做一些事情,与它的Pre Dispatch Event Handler相呼应。

       此外,如果在前面4个阶段,Touch Event的preventDefault函数没有被调用,那么WebKit会将它依次分发给div3 -> div2 -> div1 -> body -> html的Default Event Handler处理。在这个过程中,如果某一个Node的Default Event Handler处理了该Touch Event,那么该Touch Event的分发过程就会中止。

       其中,Pre Process和Post Process这两个阶段是一定会执行的。在Capturing、Target和Bubbling这三个阶段,如果某一个Node的DOM Event Handler调用了Touch Event的stopPropagation函数,那么它就会提前中止,后面的阶段也不会被执行。

       Pre Dispatch Event Handler、Post Dispatch Event Handler和Default Event Handler是由WebKit实现的,DOM Event Handler可以通过JavaScript进行注册。在注册的时候,可以指定DOM Event Handler在Capturing阶段还是Bubbling阶段接收Event,但是不能同时在这两个阶段都接收。此外,注册Target Node上的DOM Event Handler没有Capturing阶段还是Bubbling阶段之分,如果Event在Capturing阶段没有被中止,那么它将在Target阶段接收。

       明白了WebKit中的Event处理流程之后,接下来我们主要分析Target Node在Target阶段处理Touch Event的过程,也就是EventDispatcher类的成员函数dispatchEventAtTarget的实现,如下所示:

inline EventDispatchContinuation EventDispatcher::dispatchEventAtTarget()
{
    m_event->setEventPhase(Event::AT_TARGET);
    m_event->eventPath()[0].handleLocalEvents(m_event.get());
    return m_event->propagationStopped() ? DoneDispatching : ContinueDispatching;
}

      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。

      EventDispatcher类的成员变量m_event描述的就是当前发生的Touch Event。这个Touch Event的Event Path是一个Node Event Context列表。列表中的第一个Node Event Context描述的就是当前发生的Touch Event的Target Node的上下文信息。有了这个Node Event Context之后,就可以调用它的成员函数handleLocalEvents将当前发生的Touch Event分发给Target Node的DOM Event Handler处理,如下所示:

void NodeEventContext::handleLocalEvents(Event* event) const
{
    ......

    event->setTarget(target());
    event->setCurrentTarget(m_currentTarget.get());
    m_node->handleLocalEvents(event);
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/NodeEventContext.cpp中。

       这时候,NodeEventContext类的成员变量m_node描述的就是Touch Event的Target Node。NodeEventContext类的成员函数handleLocalEvents调用这个Target Node的成员函数handleLocalEvents,用来将当前发生的Touch Event分发给它的DOM Event Handler处理,如下所示:

void Node::handleLocalEvents(Event* event)
{
    ......

    fireEventListeners(event);
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Node.cpp中。

       Node类的成员函数handleLocalEvents主要是调用另外一个成员函数fireEventListeners将当前发生的Touch Event分发给Target Node的DOM Event Handler处理。

       Node类的成员函数fireEventListeners是从父类EventTarget继承下来的,它的实现如下所示:

bool EventTarget::fireEventListeners(Event* event)
{
    ......

    EventTargetData* d = eventTargetData();
    ......

    EventListenerVector* legacyListenersVector = 0;
    AtomicString legacyTypeName = legacyType(event);
    if (!legacyTypeName.isEmpty())
        legacyListenersVector = d->eventListenerMap.find(legacyTypeName);

    EventListenerVector* listenersVector = d->eventListenerMap.find(event->type());
    ......

    if (listenersVector) {
        fireEventListeners(event, d, *listenersVector);
    } else if (legacyListenersVector) {
        ......
        fireEventListeners(event, d, *legacyListenersVector);
        ......
    }

    ......
    return !event->defaultPrevented();
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventTarget.cpp中。

       Node类的成员函数fireEventListeners主要根据Touch Event的类型在Target Node注册的DOM Event Handler中找到对应的DOM Event Handler。例如,如果当前发生的是类型为MOVE的Touch Event,那么Node类的成员函数fireEventListeners就会找到注册在Target Node上的类型为TouchMove的DOM Event Handler。

       找到了对应的DOM Event Handler之后,Node类的成员函数fireEventListeners再调用另外一个重载版本的成员函数fireEventListeners将当前发生的Touch Event分发给它们处理,如下所示:

void EventTarget::fireEventListeners(Event* event, EventTargetData* d, EventListenerVector& entry)
{
    ......

    size_t i = 0;
    size_t size = entry.size();
    ......

    for ( ; i < size; ++i) {
        RegisteredEventListener& registeredListener = entry[i];
        if (event->eventPhase() == Event::CAPTURING_PHASE && !registeredListener.useCapture)
            continue;
        if (event->eventPhase() == Event::BUBBLING_PHASE && registeredListener.useCapture)
            continue;

        // If stopImmediatePropagation has been called, we just break out immediately, without
        // handling any more events on this target.
        if (event->immediatePropagationStopped())
            break;

        ExecutionContext* context = executionContext();
        if (!context)
            break;

        ......
        registeredListener.listener->handleEvent(context, event);
        ......
    }
    ......
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventTarget.cpp中。

       在Capturing和Bubbling阶段,WebKit也是通过Node类的成员函数fireEventListeners将当前发生的Event分发给Event Path上的Node处理。从这里我们就可以看到,在Capturing阶段,如果一个Node在注册了一个在Capturing阶段接收Event的DOM Event Handler,那么此时该DOM Event Handler就会获得当前发生的Event。同样,在Bubbling阶段,如果一个Node在注册了一个在Bubbling阶段接收Event的DOM Event Handler,那么此时该DOM Event Handler就会获得当前发生的Event。对于Target Node来说,它注册的DOM Event Handler则会在Target阶段获得当前发生的Event。

       这些DOM Event Handler在注册的时候,会被JavaScript引擎V8封装在一个V8AbstractEventListener对象中。Node类的成员函数fireEventListeners通过调用这些V8AbstractEventListener对象的成员函数handleEvent将当前发生的Event分发给它们所封装的DOM Event Handler处理。这就会进入到JavaScript引擎V8里面去执行了。以后我们分析JavaScript引擎V8时,再回过头来看它对Event的处理流程。

       至此,我们就分析完成Chromium分发输入事件给WebKit,以及WebKit进行处理的过程了。这个过程是在Render进程的Main线程中执行的。回忆前面Chromium网页滑动和捏合手势处理过程分析一文,滑动和捏合手势这两种特殊的输入事件,是在Render进程的Compositor线程中处理的。

       WebKit在处理输入事件的过程中,需要通过Render Layer Tree和Render Object Tree,在DOM Tree中找到输入事件的Target Node(Hit Test)。找到了输入事件的Target Node之后,分为Pre Process、Capturing、Target、Bubbling和Post Process五个阶段对输入事件进行处理。其中,Capturing、Target和Bubbling这三个阶段是将输入事件分发给Target Node的DOM Event Handler处理,也就是我们通过JavaScript注册的Event Handler。DOM Event Handler最终是在JavaScript引擎V8执行的。

       至此,Chromium的网页输入事件处理机制我们就全部分析完成了。重新学习可以参考前面Chromium网页输入事件处理机制简要介绍和学习计划这篇文章。更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/7/25 1:00:35 原文链接
阅读:39664 评论:7 查看评论

Chromium视频标签简要介绍和学习计划

$
0
0

       随着互联网的发展,在网页上观看视频变得越来越流行,尤其是泛娱乐(手机直播)大行其道的今天。在HTML5之前,在网页上播放视频需要插件支持,例如Flash插件。有了HTML5之后,标签<video>使得浏览器有了播放视频的功能。与插件相比,浏览器的视频播放功能不仅在产品上体验更好,在技术上也更加稳定。本文接下来就简要介绍Chromium是如何实现<video>标签的视频播放功能的,以及制定学习计划。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       本文以及接下来的文章,我们主要关注Chromium在Android平台上是如何实现<video>标签的视频播放功能的。我们知道,Android平台提供播放视频的API接口,也就是MediaPlayer接口。这个接口不仅可以用来播放本地媒体文件,也就用来播放网络上的流媒体文件。这可以大大简化Chromium在Android平台上支持<video>标签的工作,因为前者可以直接利用后者提供的MediaPlayer接口实现视频播放功能,如图1所示:


图1 <video>标签的实现

       从前面Chromium多进程架构简要介绍和学习计划这个系列的文章可以知道,Chromium是一个多进程架构。其中,Render进程负责加载、解析和渲染网页,Browser进程负责将Render进程渲染出来的网页内容合成到屏幕上显示。Render进程又是通过WebKit来加载和解析网页内容的。

       WebKit在解析网页内容时,每当遇到<video>标签,就会在DOM Tree中创建一个类型为HTMLMediaElement的节点。这个HTMLMediaElement节点又会在内部创建一个WebMediaPlayerClientImpl对象。这个WebMediaPlayerClientImpl对象在WebKit内部就描述为一个播放器,用来为<video>标签提供视频播放功能。

       WebMediaPlayerClientImpl类是由WebKit提供的,它本身不实现视频播放功能,因为视频播放是一个平台相关的功能。我们知道,WebKit是平台无关的,所有平台相关的功能都需要由它的使用者实现。在Chromium中,WebKit的使用者即为运行在Render进程中的Content模块。Content模块提供了一个WebMediaPlayerAndroid类,用来向WebKit提供视频播放功能。

       WebKit层的每一个WebMediaPlayerClientImpl对象在Content层都有一个对应的WebMediaPlayerAndroid对象。这些WebMediaPlayerAndroid对象就相当于是在Render进程内部实现的播放器。每一个播放器都关联有一个ID,它们被另外一个称为RendererMediaPlayerManager的对象管理。通过这种方式,就可以在一个网页上同时支持多个<video>标签,也就是可以同时播放多个视频。

       我们知道,Render进程运行在一个沙箱中,也就是它是一个受限进程。播放网络上的视频需要访问网络,以及使用系统的解码器等资源。因此,Render进程也没有实现视频播放功能,而是通过Browser进程进行播放。因此,对于Render进程中的每一个WebMediaPlayerAndroid对象,在Browser进程中都会有一个对应的WebMediaPlayerBridge对象。这些WebMediaPlayerBridge对象就相当于在Browser进程内部实现的播放器。这些播放器被另外一个称为BrowserMediaPlayerManager的对象管理,使得Browser进程可以同时创建多个播放器,以支持在一个网页上同时播放多个视频。

       WebMediaPlayerBridge类本身也不实现播放器功能。在Android平台上,WebMediaPlayerBridge类将通过SDK提供的MediaPlayer接口来实现视频播放功能。SDK是在Java层提供MediaPlayer接口的,而WebMediaPlayerBridge类是实现在C++层的,因此后者在使用前者时,需要通过JNI使用。

       总结来说,在Android平台上,Chromium会通过SDK接口MediaPlayer为网页中的每一个<video>标签创建一个播放器。播放器负责从网络上下载视频内容,并且进行解码。解码后得到的视频画面需要作为网页的一部分进行显示。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,在Chromium中,网页是Render进程进行渲染的,并且当前需要渲染的内容来自于网页的CC Active Layer Tree。这意味着要将播放器解码出来的视频画面交给网页的CC Active Layer Tree处理。

       播放器解码出来的视频画面是通过SurfaceTexture接口交给网页的CC Active Layer Tree处理的,如图2所示:


图2 <video>标签的视频画面渲染方式

       在Android平台上,SurfaceTexture是一个完美的接口。一方面它支持跨进程传输数据,另一方面传输的数据可以作为纹理使用。在我们这个情景中,MediaPlayer运行在Browser进程中,CC Active Layer Tree运行在Render进程中,并且是通过OpenGL进行渲染的。因此,SurfaceTexture非常适合将前者的输出作为后者的输入,并且通过OpenGL以纹理的方式渲染出来。

       具体来说,就是CC Active Layer Tree会为每一个<video>标签创建一个类型为VideoLayerImpl的节点。这个节点的内容就来自于播放器解码出来的视频画面。这些视频画面最终又是通过纹理来描述的。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,CC Active Layer Tree其它节点的内容,最终也是描述为纹理进行渲染的。因此,Chromium可以轻松地将播放器解码出来的视频画面作为网页的一部分进行显示。

       在Android平台上,Chromium还为<video>标签提供了全屏播放功能。全屏播放与非全屏播放可以进行无缝切换,它是怎么实现的呢?我们通过图3来说明,如下所示:


图3 <video>标签的全屏播放功能实现

       在<video>标签全屏播放的情况下,我们是看不到网页的其它内容的。这使得Chromium可以使用一种简单的方式实现<video>标签的全屏播放功能。当<video>标签全屏播放的时候,Browser进程会在浏览器窗口上创建一个全屏的SurfaceView,然后将这个SurfaceView底层的Surface取出来,设置为MediaPlayer的解码输出。这样就可以将MediaPlayer的解码输出全屏显示在屏幕上了。这时候由于网页是不可见的,Render进程不需要对它进行渲染。

       接下来,我们结合源码,按照三个情景深入分析Chromium对<video>标签的支持:

       1. 为<video>标签创建播放器的过程

       2. 渲染<video>标签视频画面的过程

       3. 全屏播放<video>标签视频的过程

       学习了这三个情景之后 ,我们就会对HTML5中的<video>标签有更深刻的了解,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/8/8 1:00:30 原文链接
阅读:39004 评论:6 查看评论

Chromium为视频标签创建播放器的过程分析

$
0
0

       Chromium是通过WebKit解析网页内容的。当WebKit遇到<video>标签时,就会创建一个播放器实例。WebKit是平台无关的,而播放器实现是平台相关的。因此,WebKit并没有自己实现播放器,而仅仅是创建一个播放器接口。通过这个播放器接口,可以使用平台提供的播放器来播放视频的内容。这就简化了Chromium对视频标签的支持。本文接下来就分析Chromium为视频标签创建播放器的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       以Android平台为例,它的SDK提供了一个MediaPlayer接口,用来播放视频。Chromium的目标就是为网页中的每一个<video>标签创建一个MediaPlayer实例,如图1所示:


图1 Chromium为<video>标签创建MediaPlayer的过程

       首先,WebKit会为网页中的每一个<video>标签创建一个类型为HTMLMediaElement的DOM节点。HTMLMediaElement类内部有一个WebMediaPlayerClientImpl接口。这个WebMediaPlayerClientImpl接口指向的是一个运行在Render进程的Content模块中的一个WebMediaPlayerAndroid对象。这些WebMediaPlayerAndroid对象归一个称为RendererMediaPayerManager的对象管理。

       Render进程中的每一个WebMediaPlayerAndroid对象,在Browser进程中都有一个对应的WebMediaPlayerBridge对象。这些WebMediaPlayerBridge对象归一个称为BrowserMediaPayerManager的对象管理。每一个WebMediaPlayerBridge对象在Java层中又都对应有一个MediaPlayer对象。这些MediaPlayer对象描述的就是Android平台提供的播放器。

       接下来,我们就从WebKit解析<video>标签的属性开始,分析Chromium为它们创建MediaPlayer的过程,如下所示:

void HTMLMediaElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
{
    if (name == srcAttr) {
        // Trigger a reload, as long as the 'src' attribute is present.
        if (!value.isNull()) {
            ......
            scheduleDelayedAction(LoadMediaResource);
        }
    } 

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       WebKit为网页的每一个标签创建了相应的DOM节点之后,就会调用这个DOM节点的成员函数parseAtrribute对它的属性进行解析。从前面Chromium网页DOM Tree创建过程分析一文可以容易知道,WebKit为<video>标签创建的DOM节点的实际类型为HTMLVideoElement。HTMLVideoElement类是从HTMLMediaElement类继承下来的,WebKit是调用后者的成同员函数parseAttribute来解析<video>的属性。

       我们假设<video>标签通过src属性设置了要播放的视频文件的URL。这时候HTMLMediaElement类的成员函数parseAttribute就会调用另外一个成员函数scheduleDelayedAction为<video>标签创建播放器,如下所示:

void HTMLMediaElement::scheduleDelayedAction(DelayedActionType actionType)
{
    .....

    if ((actionType & LoadMediaResource) && !(m_pendingActionFlags & LoadMediaResource)) {
        prepareForLoad();
        m_pendingActionFlags |= LoadMediaResource;
    }

    ......

    if (!m_loadTimer.isActive())
        m_loadTimer.startOneShot(0, FROM_HERE);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       从前面的调用过程可以知道,参数actionType的值等于LoadMediaResource。这时候如果HTMLMediaElement类的成员变量m_pendingActionFlags的LoadMediaResource位等于0,那么就说明WebKit还没有为当前正在解析的<video>标签创建过播放器接口。于是接下来就会做两件事情:

       1. 调用另外一个成员函数prepareForLoad开始为当前正在解析的<video>标签创建图1所示的WebMediaPlayerClientImpl接口;

       2. 将成员变量m_pendingActionFlags的LoadMediaResource位设置为1,表示WebKit正在为当前正在解析的<video>标签创建过播放器接口,避免接下来出现重复创建的情况。

       HTMLMediaElement类的另外一个成员变量m_loadTimer描述的是一个定时器。如果这个定时器还没有被启动,那么HTMLMediaElement类的成员函数scheduleDelayedAction就会调用它的成员函数startOneShot马上进行启动。指定的启动时间单位为0,这意味着这个定时器会马上超时。超时后它将会调用HTMLMediaElement类的成员函数loadTimerFired。HTMLMediaElement类的成员函数loadTimerFired将会继续创建图1所示的WebMediaPlayerAndroid、WebMediaPlayerBridge和MediaPlayer接口。

       接下来我们就先分析HTMLMediaElement类的成员函数prepareForLoad的实现,如下所示:

void HTMLMediaElement::prepareForLoad()
{
    ......

    createMediaPlayer();

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数prepareForLoad主要是调用另外一个成员函数createMediaPlayer为当前正在解析的<video>标签创建一个WebMediaPlayerClientImpl接口,如下所示:

void HTMLMediaElement::createMediaPlayer()
{
    ......

    m_player = MediaPlayer::create(this);

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数createMediaPlayer调用MediaPlayer类的静态成员函数create创建了一个WebMediaPlayerClientImpl接口,并且保存在HTMLMediaElement类的成员变量m_player中。

       MediaPlayer类的静态成员函数create的实现如下所示:

static CreateMediaEnginePlayer createMediaEngineFunction = 0;

void MediaPlayer::setMediaEngineCreateFunction(CreateMediaEnginePlayer createFunction)
{
    ASSERT(createFunction);
    ASSERT(!createMediaEngineFunction);
    createMediaEngineFunction = createFunction;
}

PassOwnPtr<MediaPlayer> MediaPlayer::create(MediaPlayerClient* client)
{
    ASSERT(createMediaEngineFunction);
    return createMediaEngineFunction(client);
}
       这两个函数定义在文件external/chromium_org/third_party/WebKit/Source/platform/graphics/media/MediaPlayer.cpp中。

       MediaPlayer类的静态成员函数create调用全局变量createMediaEngineFunction描述的一个函数创建一个播放器接口返回给调用者。全局变量createMediaEngineFunction描述的函数实际上是WebMediaPlayerClientImpl类的静态成员函数create,它是通过调用MediaPlayer类的静态成员函数setMediaEngineCreateFunction设置的。

       WebMediaPlayerClientImpl类的静态成员函数create的实现如下所示:

PassOwnPtr<MediaPlayer> WebMediaPlayerClientImpl::create(MediaPlayerClient* client)
{
    return adoptPtr(new WebMediaPlayerClientImpl(client));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       从这里可以看到,WebMediaPlayerClientImpl类的静态成员函数create返回的是一个WebMediaPlayerClientImpl对象。这个WebMediaPlayerClientImpl对象描述的就是WebKit层的播放器接口。

       这一步执行完成之后,WebKit就为当前正在解析的<video>标签创建了一个类型为WebMediaPlayerClientImpl的播放器接口。回到前面分析的HTMLMediaElement类的成员函数scheduleDelayedAction中,接下来它启动的定时器就会马上执行,也就HTMLMediaElement类的成员函数loadTimerFired会马上被调用。

       HTMLMediaElement类的成员函数loadTimerFired的实现如下所示:

void HTMLMediaElement::loadTimerFired(Timer<HTMLMediaElement>*)
{
    ......

    if (m_pendingActionFlags & LoadMediaResource) {
        if (m_loadState == LoadingFromSourceElement)
            loadNextSourceChild();
        else
            loadInternal();
    }

    m_pendingActionFlags = 0;
} 
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       前面分析的HTMLMediaElement类的成员函数scheduleDelayedAction已经将成员变量m_pendingActionFlags的LoadMediaResource位设置为1。这时候HTMLMediaElement类的成员函数loadTimerFired就会检查HTMLMediaElement类的另外一个成员变量m_loadState的值是否等于LoadingFromSourceElement。如果等于,那么就说明当前正在解析的<video>标签通过子元素<source>指定要播放的视频的URL。否则的话,就通过属性src指定要播放的视频的URL。

       前面我们假定了当前正在解析的<video>标签通过属性src指定要播放的视频的URL,因此HTMLMediaElement类的成员函数loadTimerFired接下来就会调用成员函数loadInternal继续为其创建其它的播放器接口,如下所示:

void HTMLMediaElement::loadInternal()
{
    ......

    selectMediaResource();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数loadInternal调用另外一个成员函数selectMediaResource为当前正在解析的<video>标签选择当前要播放的视频的URL。确定了当前要播放的视频的URL之后,就会加载视频元数据。有了这些元数据之后,就可以为其创建真正的播放器。

       HTMLMediaElement类的成员函数selectMediaResource的实现如下所示:

void HTMLMediaElement::selectMediaResource()
{
    ......

    enum Mode { attribute, children };

    .......
    Mode mode = attribute;
    if (!fastHasAttribute(srcAttr)) {
        // Otherwise, if the media element does not have a src attribute but has a source
        // element child, then let mode be children and let candidate be the first such
        // source element child in tree order.
        if (HTMLSourceElement* element = Traversal<HTMLSourceElement>::firstChild(*this)) {
            mode = children;
            ......
        } 
        .....
    }

    ......

    if (mode == attribute) {
        ......

        // If the src attribute's value is the empty string ... jump down to the failed step below
        KURL mediaURL = getNonEmptyURLAttribute(srcAttr);
        ......

        loadResource(mediaURL, contentType, String());
        .....
        return;
    }

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数selectMediaResource所做的事情就要确定是从当前正在解析的<video>标签的src属性获得要加载的视频的URL,还是从它的子元素<source>获得要加载的视频的URL。

       如果当前正在解析的<video>标签设置了src属性,那么就会优先从这个属性获得要加载的视频的URL。有了这个URL之后,HTMLMediaElement类的成员函数selectMediaResource就会调用另外一个成员函数loadResource加载它所描述的视频的元数据。

       在我们这个情景中,当前正在解析的<video>标签设置了src属性。因此,接下来我们就继续分析HTMLMediaElement类的成员函数loadResource的实现,如下所示:

void HTMLMediaElement::loadResource(const KURL& url, ContentType& contentType, const String& keySystem)
{
    ......

    m_currentSrc = url;
    ......
    bool attemptLoad = true;

    if (url.protocolIs(mediaSourceBlobProtocol)) {
        if (isMediaStreamURL(url.string())) {
            m_userGestureRequiredForPlay = false;
        } else {
            m_mediaSource = HTMLMediaSource::lookup(url.string());

            if (m_mediaSource) {
                if (!m_mediaSource->attachToElement(this)) {
                    // Forget our reference to the MediaSource, so we leave it alone
                    // while processing remainder of load failure.
                    m_mediaSource = nullptr;
                    attemptLoad = false;
                }
            }
        }
    }

    if (attemptLoad && canLoadURL(url, contentType, keySystem)) {
        .......

        if (!m_havePreparedToPlay && !autoplay() && m_preload == MediaPlayer::None) {
            ......
            deferLoad();
        } else {
            startPlayerLoad();
        }
    } 

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数loadResource首先将当前要播放的视频的URL保存在成员变量m_currentSrc中,接下来判断该URL的协议部分是否为“blob”。协议“blob”是“Binary Large OBject”的缩写,表示一组二进制数据。例如,我们手头上有一组表示一个Image的二进制数据,这时候可以调用URL.createObjectURL函数为这组二进制数据创建一个blob协议地址,然后再将该地址设置为一个<img>标签的src,这样就可以将图像显示出来。

       通过blob协议,还可以描述媒体数据。在WebKit中,媒体数据可以通过Media Source或者Media Stream API描述。Media Source API的设计初衷是让JavaScript能动态产生媒体流,然后交给<video>标签播放。Media Stream API是为WebRTC设计的,不仅可以使用<video>标签播放从本地摄像头采集的图像,还可以播放从网络发送过来的实时媒体流。关于Media Source 和Media Steam API的更详细信息,可以参考Media Source ExtensionsMedia Capture and Streams这两篇文档。

       如果当前要播放的视频的URL的协议部分为“blob”,HTMLMediaElement类的成员函数loadResource首先会检查它描述的是否是一个Media Stream。如果是的话,那么就会将HTMLMediaElement类的成员变量m_userGestureRequiredForPlay设置为false,表示后台Tab网页的视频可以自动播放。Render进程有一个“disable-gesture-requirement-for-media-playback”选项。当这个选项的值设置为false时,HTMLMediaElement类的成员变量m_userGestureRequiredForPlay就会被设置为true,表示后台Tab网页的视频不可以自动播放。不过,如果要播放的视频是一个Media Stream,那么不会受到此限制。关于Render进程的“disable-gesture-requirement-for-media-playback”启动选项的更多信息,可以参考Chrome 47 offers a flag to disable defer media playback in background tabs一文。

       如果当前要播放的视频的URL的协议部分为“blob”,但是它描述的不是一个Media Stream API,那么HTMLMediaElement类的成员函数loadResource再检查它是否是一个Media Source。如果是的话,就会将该Media Source作为当前正在解析的<video>标签的播放源。在这种情况下,WebKit不需要从网络上下载媒体数据回来,只需要从指定的Media Source读取回来就可以了。这时候本地变量attemptLoad的值会被设置为false。在其余情况下,本地变量attemptLoad的值保持为初始值true。

       在本地变量attemptLoad的值为true的情况下,HTMLMediaElement类的成员函数loadResource会调用另外一个成员函数canLoadURL继续检查当前要播放的视频的URL描述的内容是否为多媒体数据,以及该多媒体使用的编码方式是否被支持。如果检查通过,HTMLMediaElement类的成员函数loadResource再判断当前解析的<video>标签的是否需要preload和autoplay。如果不需要,那么就会调用成员函数deferLoad延迟加载要播放的视频的内容。否则的话,就会调用成员函数startPlayerLoad马上加载要播放的视频的内容回来。

       我们假设当前解析的<video>标签设置了autoplay,因此接下来我们就继续分析HTMLMediaElement类的成员函数startPlayerLoad的实现,如下所示:

void HTMLMediaElement::startPlayerLoad()
{
    .......

    KURL requestURL = m_currentSrc;
    ......

    m_player->load(loadType(), requestURL, corsMode());
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       从前面的分析可以知道,HTMLMediaElement类的成员变量m_player指向的是一个WebMediaPlayerClientImpl对象。HTMLMediaElement类的成员函数startPlayerLoad主要是调用这个WebMediaPlayerClientImpl对象的成员函数load加载当前要播放的视频的内容。

       WebMediaPlayerClientImpl类的成员函数load的实现如下所示:

void WebMediaPlayerClientImpl::load(WebMediaPlayer::LoadType loadType, const WTF::String& url, WebMediaPlayer::CORSMode corsMode)
{
    ......

    KURL kurl(ParsedURLString, url);
    m_webMediaPlayer = createWebMediaPlayer(this, kurl, frame);
    ...... 
   
    m_webMediaPlayer->load(loadType, kurl, corsMode);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       WebMediaPlayerClientImpl类的成员函数load首先调用函数createWebMediaPlayer请求运行在当前进程(Render进程)中的Content模块创建一个播放器接口。这个Content模块的播放器接口会保存在WebMediaPlayerClientImpl类的成员变量m_webMediaPlayer中。有了Content模块的播放器接口之后,WebMediaPlayerClientImpl类的成员函数load再调用它的成员函数load请求加载当前要播放的视频的内容。

       接下来我们先分析Content模块的播放器接口的创建过程,也就是函数createWebMediaPlayer的实现,如下所示:

static PassOwnPtr<WebMediaPlayer> createWebMediaPlayer(WebMediaPlayerClient* client, const WebURL& url, LocalFrame* frame)
{
    WebLocalFrameImpl* webFrame = WebLocalFrameImpl::fromFrame(frame);
    ......
    return adoptPtr(webFrame->client()->createMediaPlayer(webFrame, url, client));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       函数createWebMediaPlayer首先获得一个类型为blink::WebFrameClient的接口。这个接口是由WebKit的使用者设置给WebKit的,以便WebKit可以通过它执行一些平台相关的功能。在我们这个情景中,WebKit的使用者即为Render进程中的Content模块。Content模块中的RenderFrameImpl类实现了该接口,并且会设置给WebKit。因此,函数createWebMediaPlayer接下来就会调用它的成员函数createMediaPlayer创建一个播放器接口。

       RenderFrameImpl类的成员函数createMediaPlayer的实现如下所示:

blink::WebMediaPlayer* RenderFrameImpl::createMediaPlayer(
    blink::WebLocalFrame* frame,
    const blink::WebURL& url,
    blink::WebMediaPlayerClient* client) {
  blink::WebMediaStream web_stream(
      blink::WebMediaStreamRegistry::lookupMediaStreamDescriptor(url));
  if (!web_stream.isNull())
    return CreateWebMediaPlayerForMediaStream(url, client);

#if defined(OS_ANDROID)
  return CreateAndroidWebMediaPlayer(url, client);
#else
  ......
#endif  // defined(OS_ANDROID)
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       RenderFrameImpl类的成员函数createMediaPlayer首先判断当前要播放的视频的URL描述的是否是一个Media Stream。如果是的话,那么就会调用成员函数CreateWebMediaPlayerForMediaStream创建一个类型为WebMediaPlayerMS的播放器。这个类型为WebMediaPlayerMS的播放器是在WebRTC中实现的。我们不考虑这种情况。

       如果当前要播放的视频的URL描述的不是一个Media Stream,那么RenderFrameImpl类的成员函数createMediaPlayer就会调用另外一个成员函数CreateAndroidWebMediaPlayer创建另外一种类型的播放器,如下所示:

WebMediaPlayer* RenderFrameImpl::CreateAndroidWebMediaPlayer(
      const blink::WebURL& url,
      WebMediaPlayerClient* client) {
  GpuChannelHost* gpu_channel_host =
      RenderThreadImpl::current()->EstablishGpuChannelSync(
          CAUSE_FOR_GPU_LAUNCH_VIDEODECODEACCELERATOR_INITIALIZE);
  ......

  scoped_refptr<StreamTextureFactory> stream_texture_factory;
  if (SynchronousCompositorFactory* factory =
          SynchronousCompositorFactory::GetInstance()) {
    stream_texture_factory = factory->CreateStreamTextureFactory(routing_id_);
  } else {
    scoped_refptr<webkit::gpu::ContextProviderWebContext> context_provider =
        RenderThreadImpl::current()->SharedMainThreadContextProvider();
    ......

    stream_texture_factory = StreamTextureFactoryImpl::Create(
        context_provider, gpu_channel_host, routing_id_);
  }

  return new WebMediaPlayerAndroid(
      frame_,
      client,
      weak_factory_.GetWeakPtr(),
      GetMediaPlayerManager(),
      GetCdmManager(),
      stream_texture_factory,
      RenderThreadImpl::current()->GetMediaThreadMessageLoopProxy(),
      new RenderMediaLog());
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer首先是调用RenderThreadImpl类的成员函数EstablishGpuChannelSync为接下来要创建的播放器创建一个到GPU进程的GPU通道。之所以要创建这个GPU通道,是因为Render要在GPU进程中创建一个纹理,然后将这个纹理封装为一个SurfaceTexture,作为Android平台的MediaPlayer的解码输出。也就是说,Render进程会通过这个SurfaceTexture接收Android平台的MediaPlayer的解码输出,然后再以纹理的形式渲染在网页上。GPU通道的创建过程,也就是RenderThreadImpl类的成员函数EstablishGpuChannelSync的实现,可以参考前面Chromium的GPU进程启动过程分析一文。

       RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer最终创建的是一个类型为WebMediaPlayerAndroid的播放器。创建这个类型为WebMediaPlayerAndroid的播放器需要用到两个重要的对象。一个是StreamTextureFactory对象,另一个是RendererMediaPlayerManager对象。前者用来创建前面所述的纹理。后者用来管理在Render进程中创建的播放器实例。

       在WebView的情况下,调用SynchronousCompositorFactory类的静态成员函数GetInstance会获得一个SynchronousCompositorFactory对象。在这种情况下,上述StreamTextureFactory对象通过调用这个SynchronousCompositorFactory对象的成员函数CreateStreamTextureFactory获得。我们不考虑这一种情况。

       在独立App的情况下,上述StreamTextureFactory对象实际上是一个StreamTextureFactoryImpl对象,它是通过调用StreamTextureFactoryImpl类的静态成员函数Create创建的,如下所示:

scoped_refptr<StreamTextureFactoryImpl> StreamTextureFactoryImpl::Create(
    const scoped_refptr<cc::ContextProvider>& context_provider,
    GpuChannelHost* channel,
    int frame_id) {
  return new StreamTextureFactoryImpl(context_provider, channel, frame_id);
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       从这里可以看到,StreamTextureFactoryImpl类的静态成员函数Create返回的是一个StreamTextureFactoryImpl对象。

       回到前面分析的RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer中,它创建了一个StreamTextureFactoryImpl对象之后,接下来又会调用另外一个成员函数GetMediaPlayerManager获得一个RendererMediaPlayerManager对象,如下所示:

RendererMediaPlayerManager* RenderFrameImpl::GetMediaPlayerManager() {
  if (!media_player_manager_) {
    media_player_manager_ = new RendererMediaPlayerManager(this);
    ......
  }
  return media_player_manager_;
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       从这里可以看到,RenderFrameImpl类的成员函数GetMediaPlayerManager返回的是成员变量media_player_manager_指向的是一个RendererMediaPlayerManager对象。如果这个RendererMediaPlayerManager对象还没有创建,那么就会先进行创建。

       再回到前面分析的RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer中,有了一个StreamTextureFactoryImpl对象和一个RendererMediaPlayerManager对象之后,它就会创建一个类型为WebMediaPlayerAndroid的播放器。

       类型为WebMediaPlayerAndroid的播放器的创建过程,也就是WebMediaPlayerAndroid类的构造函数的实现,如下所示:

WebMediaPlayerAndroid::WebMediaPlayerAndroid(
    blink::WebFrame* frame,
    blink::WebMediaPlayerClient* client,
    base::WeakPtr<WebMediaPlayerDelegate> delegate,
    RendererMediaPlayerManager* player_manager,
    RendererCdmManager* cdm_manager,
    scoped_refptr<StreamTextureFactory> factory,
    const scoped_refptr<base::MessageLoopProxy>& media_loop,
    media::MediaLog* media_log)
    : ......,
      player_manager_(player_manager),
      ......,
      stream_texture_factory_(factory),
      ...... {

  ......

  player_id_ = player_manager_->RegisterMediaPlayer(this);
  ......

  TryCreateStreamTextureProxyIfNeeded();
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的构造函数首先是将参数player_manager和factory描述的StreamTextureFactoryImpl对象和RendererMediaPlayerManager对象分别保存在成员变量player_manager_和stream_texture_factory_中。

       WebMediaPlayerAndroid类的构造函数接下来又会做两件事情:

       1. 调用上述RendererMediaPlayerManager对象的成员函数RegisterMediaPlayer将当前正在创建的WebMediaPlayerAndroid对象保存在其内部,并且为其分配一个ID。以后通过这个ID就可以在该RendererMediaPlayerManager对象中找到当前正在创建的WebMediaPlayerAndroid对象。

       2. 调用另外一个成员函数TryCreateStreamTextureProxyIfNeeded创建一个SurfaceTexture,以便后面可以用来接收Android平台的MediaPlayer的解码输出。

       关于WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded创建SurfaceTexture的过程,我们在接下来一篇文章中分析<video>标签的视频渲染过程时再分析。

       这一步执行完成之后,运行在Render进程中的Content模块就创建了一个类型为WebMediaPlayerAndroid的播放器接口。这个播放器接口会返回给WebKit层的WebMediaPlayerClientImpl类的成员函数load。WebMediaPlayerClientImpl类的成员函数load获得了这个播放器接口之后,就会调用它的成员函数load加载当前要播放的视频的内容,如下所示:

void WebMediaPlayerAndroid::load(LoadType load_type,
                                 const blink::WebURL& url,
                                 CORSMode cors_mode) {
  ......

  switch (load_type) {
    case LoadTypeURL:
      player_type_ = MEDIA_PLAYER_TYPE_URL;
      break;

    case LoadTypeMediaSource:
      player_type_ = MEDIA_PLAYER_TYPE_MEDIA_SOURCE;
      break;

    case LoadTypeMediaStream:
      CHECK(false) << "WebMediaPlayerAndroid doesn't support MediaStream on "
                      "this platform";
      return;
  }

  url_ = url;
  int demuxer_client_id = 0;
  if (player_type_ != MEDIA_PLAYER_TYPE_URL) {
    RendererDemuxerAndroid* demuxer =
        RenderThreadImpl::current()->renderer_demuxer();
    demuxer_client_id = demuxer->GetNextDemuxerClientID();

    media_source_delegate_.reset(new MediaSourceDelegate(
        demuxer, demuxer_client_id, media_loop_, media_log_));

    if (player_type_ == MEDIA_PLAYER_TYPE_MEDIA_SOURCE) {
      media::SetDecryptorReadyCB set_decryptor_ready_cb =
          media::BindToCurrentLoop(
              base::Bind(&WebMediaPlayerAndroid::SetDecryptorReadyCB,
                         weak_factory_.GetWeakPtr()));

      media_source_delegate_->InitializeMediaSource(
          base::Bind(&WebMediaPlayerAndroid::OnMediaSourceOpened,
                     weak_factory_.GetWeakPtr()),
          base::Bind(&WebMediaPlayerAndroid::OnNeedKey,
                     weak_factory_.GetWeakPtr()),
          set_decryptor_ready_cb,
          base::Bind(&WebMediaPlayerAndroid::UpdateNetworkState,
                     weak_factory_.GetWeakPtr()),
          base::Bind(&WebMediaPlayerAndroid::OnDurationChanged,
                     weak_factory_.GetWeakPtr()));
      InitializePlayer(url_, frame_->document().firstPartyForCookies(),
                       true, demuxer_client_id);
    }
  } else {
    info_loader_.reset(
        new MediaInfoLoader(
            url,
            cors_mode,
            base::Bind(&WebMediaPlayerAndroid::DidLoadMediaInfo,
                       weak_factory_.GetWeakPtr())));
    info_loader_->Start(frame_);
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       类型为WebMediaPlayerAndroid的播放器只能处理普通URL描述的媒体流,以及BLOB协议描述的类型为Media Source的媒体流,不能处理类型为Media Stream的媒体流(需要由WebRTC处理)。

       类型为Media Source的媒体流数据直接从指定的Media Source获得。WebMediaPlayerAndroid类的成员函数load将该Media Source封装在一个MediaSourceDelegate对象中,然后通过这个MediaSourceDelegate对象来获得要播放的视频的元数据。有了要播放的视频的元数据之后,就可以调用WebMediaPlayerAndroid类的成员函数InitializePlayer初始化当前正在处理的播放器。

       我们假设当前要播放的视频的URL是一个普通的URL,它描述的媒体流的元数据要通过一个MediaInfoLoader对象从网络上下载回来。下载完成后,WebMediaPlayerAndroid类的成员函数DidLoadMediaInfo会被调用,它的实现如下所示:

void WebMediaPlayerAndroid::DidLoadMediaInfo(
    MediaInfoLoader::Status status,
    const GURL& redirected_url,
    const GURL& first_party_for_cookies,
    bool allow_stored_credentials) {
  ......

  InitializePlayer(
      redirected_url, first_party_for_cookies, allow_stored_credentials, 0);

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数DidLoadMediaInfo会用下载回来的视频元数据初始化当前正在处理的播放器。这同样是通过调用WebMediaPlayerAndroid类的成员函数InitializePlayer进行的,如下所示:

void WebMediaPlayerAndroid::InitializePlayer(
    const GURL& url,
    const GURL& first_party_for_cookies,
    bool allow_stored_credentials,
    int demuxer_client_id) {
  ......
  player_manager_->Initialize(
      player_type_, player_id_, url, first_party_for_cookies, demuxer_client_id,
      frame_->document().url(), allow_stored_credentials);
  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       从前面的分析可以知道,WebMediaPlayerAndroid类的成员变量player_manager_指向的是一个RendererMediaPlayerManager对象。WebMediaPlayerAndroid类的成员函数InitializePlayer调用这个RendererMediaPlayerManager对象的成员函数Initialize对当前正在处理的播放器进行初始化,如下所示:

void RendererMediaPlayerManager::Initialize(
    MediaPlayerHostMsg_Initialize_Type type,
    int player_id,
    const GURL& url,
    const GURL& first_party_for_cookies,
    int demuxer_client_id,
    const GURL& frame_url,
    bool allow_credentials) {
  MediaPlayerHostMsg_Initialize_Params media_player_params;
  media_player_params.type = type;
  media_player_params.player_id = player_id;
  media_player_params.demuxer_client_id = demuxer_client_id;
  media_player_params.url = url;
  media_player_params.first_party_for_cookies = first_party_for_cookies;
  media_player_params.frame_url = frame_url;
  media_player_params.allow_credentials = allow_credentials;

  Send(new MediaPlayerHostMsg_Initialize(routing_id(), media_player_params));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager对象的成员函数Initialize向Browser进程发送一个类型为MediaPlayerHostMsg_Initialize的IPC消息,用来请求Browser进程为当前正在处理的类型为WebMediaPlayerAndroid的播放器创建一个由Android平台实现的播放器。

       Browser进程是通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_Initialize的IPC消息的,如下所示:

bool MediaWebContentsObserver::OnMediaPlayerMessageReceived(
    const IPC::Message& msg,
    RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg)
    ......
    IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_Initialize,
                        GetMediaPlayerManager(render_frame_host),
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。

       MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived会将类型为MediaPlayerHostMsg_Initialize的IPC消息分发给运行在Browser进程中的一个BrowserMediaPlayerManager对象的成员函数OnInitialize处理。这个BrowserMediaPlayerManager对象负责管理在Browser进程中创建的播放器实例,它与运行在Render进程中的RendererMediaPlayerManager对象是对应的,不过后者用来管理在Render进程中创建的播放器实例。

       BrowserMediaPlayerManager类的成员函数OnInitialize的实现如下所示:

void BrowserMediaPlayerManager::OnInitialize(
    const MediaPlayerHostMsg_Initialize_Params& media_player_params) {
  ......

  MediaPlayerAndroid* player = CreateMediaPlayer(
      media_player_params,

      host->GetBrowserContext()->IsOffTheRecord(), this,
      host->browser_demuxer_android());

  ......

  AddPlayer(player);
}

       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数OnInitialize主要是调用成员函数CreateMediaPlayer创建一个类型为MediaPlayerBridge的播放器实例,并且调用另外一个成员函数AddPlayer将这个播放器实例保存在内部。

       BrowserMediaPlayerManager类的成员函数CreateMediaPlayer的实现如下所示:

MediaPlayerAndroid* BrowserMediaPlayerManager::CreateMediaPlayer(
    const MediaPlayerHostMsg_Initialize_Params& media_player_params,
    bool hide_url_log,
    MediaPlayerManager* manager,
    BrowserDemuxerAndroid* demuxer) {
  switch (media_player_params.type) {
    case MEDIA_PLAYER_TYPE_URL: {
      const std::string user_agent = GetContentClient()->GetUserAgent();
      MediaPlayerBridge* media_player_bridge = new MediaPlayerBridge(
          media_player_params.player_id,
          media_player_params.url,
          media_player_params.first_party_for_cookies,
          user_agent,
          hide_url_log,
          manager,
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesRequested,
                     weak_ptr_factory_.GetWeakPtr()),
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesReleased,
                     weak_ptr_factory_.GetWeakPtr()),
          media_player_params.frame_url,
          media_player_params.allow_credentials);
      BrowserMediaPlayerManager* browser_media_player_manager =
          static_cast<BrowserMediaPlayerManager*>(manager);
      ContentViewCoreImpl* content_view_core_impl =
          static_cast<ContentViewCoreImpl*>(ContentViewCore::FromWebContents(
              browser_media_player_manager->web_contents_));
      if (!content_view_core_impl) {
        // May reach here due to prerendering. Don't extract the metadata
        // since it is expensive.
        // TODO(qinmin): extract the metadata once the user decided to load
        // the page.
        browser_media_player_manager->OnMediaMetadataChanged(
            media_player_params.player_id, base::TimeDelta(), 0, 0, false);
      } else if (!content_view_core_impl->ShouldBlockMediaRequest(
            media_player_params.url)) {
        media_player_bridge->Initialize();
      }
      return media_player_bridge;
    }

    case MEDIA_PLAYER_TYPE_MEDIA_SOURCE: {
      return new MediaSourcePlayer(
          media_player_params.player_id,
          manager,
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesRequested,
                     weak_ptr_factory_.GetWeakPtr()),
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesReleased,
                     weak_ptr_factory_.GetWeakPtr()),
          demuxer->CreateDemuxer(media_player_params.demuxer_client_id),
          media_player_params.frame_url);
    }
  }

  NOTREACHED();
  return NULL;
}

       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数CreateMediaPlayer同样是只会为普通URL描述的视频以及类型为Media Source的视频创建播放器。这里我们只考虑普通URL描述的视频的情况。这时候BrowserMediaPlayerManager类的成员函数CreateMediaPlayer会创建一个类型为MediaPlayerBridge的播放器,并且在视频所加载在的网页可见的情况下,调用它的成员函数Initialize对它进行初始化,也就是获取视频元数据。如果视频所加载在的网页当前不可见,那么获取视频元数据的操作可以延后执行,也就是等到下次可见时再执行。

       我们假设视频所加载在的网页当前是可见的。接下来我们就继续分析类型为MediaPlayerBridge的播放器的初始化过程,也就是MediaPlayerBridge类的成员函数Initialize的实现,如下所示:

void MediaPlayerBridge::Initialize() {
  ......

  media::MediaResourceGetter* resource_getter =
      manager()->GetMediaResourceGetter();
  ......

  // Start extracting the metadata immediately if the request is anonymous.
  // Otherwise, wait for user credentials to be retrieved first.
  if (!allow_credentials_) {
    ExtractMediaMetadata(url_.spec());
    return;
  }

  resource_getter->GetCookies(url_,
                              first_party_for_cookies_,
                              base::Bind(&MediaPlayerBridge::OnCookiesRetrieved,
                                         weak_factory_.GetWeakPtr()));
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员变量allow_credentials_是一个布尔变量。当它的值等于false的时候,表示视频元数据可以通过匿名方式获取。在这种情况下,MediaPlayerBridge类的成员函数Initialize就会直接调用另外一个成员函数ExtractMediaMetadata获取视频元数据。

       当MediaPlayerBridge类的成员变量allow_credentials_的值等于true的时候,表示视频元数据要有凭证才可以获取。在这种情况下,MediaPlayerBridge类的成员函数Initialize先通过调用一个MediaResourceGetter对象的成员函数GetCookies获取该凭证。获得了这个凭证之后,MediaPlayerBridge类的成员函数OnCookiesRetrieved会被回调。

       MediaPlayerBridge类的成员函数OnCookiesRetrieved被回调的时候,它同样是会调用成员函数ExtractMediaMetadata去获取视频元数据。为简单起见,我们假设视频元数据可以通过匿名方式获取。因此,接下来我们就继续分析MediaPlayerBridge类的成员函数ExtractMediaMetadata的实现,以及了解视频元数据获取的过程,如下所示:

void MediaPlayerBridge::ExtractMediaMetadata(const std::string& url) {
  int fd;
  int64 offset;
  int64 size;
  if (InterceptMediaUrl(url, &fd, &offset, &size)) {
    manager()->GetMediaResourceGetter()->ExtractMediaMetadata(
        fd, offset, size,
        base::Bind(&MediaPlayerBridge::OnMediaMetadataExtracted,
                   weak_factory_.GetWeakPtr()));
  } else {
    manager()->GetMediaResourceGetter()->ExtractMediaMetadata(
        url, cookies_, user_agent_,
        base::Bind(&MediaPlayerBridge::OnMediaMetadataExtracted,
                   weak_factory_.GetWeakPtr()));
  }
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员函数ExtractMediaMetadata首先是调用成员函数InterceptMediaUrl检查当前要播放的视频是否已经存在本地,也就是之前下载过。如果是的话,就会直接从该文件读取视频元数据回来。否则的话,就会通过网络请求视频元数据。无论是哪一种方式,一旦得到视频元数据之后,就会回调用MediaPlayerBridge类的成员函数OnMediaMetadataExtracted进行后续处理。

       MediaPlayerBridge类的成员函数OnMediaMetadataExtracted的实现如下所示:

void MediaPlayerBridge::OnMediaMetadataExtracted(
    base::TimeDelta duration, int width, int height, bool success) {
  if (success) {
    duration_ = duration;
    width_ = width;
    height_ = height;
  }
  manager()->OnMediaMetadataChanged(
      player_id(), duration_, width_, height_, success);
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       获取的视频元数据包括视频持续时间、宽度以及高度。这些数据会分别保存在MediaPlayerBridge类的成员变量duration_、width_和height_中。

       MediaPlayerBridge类的成员函数OnMediaMetadataExtracted最后还会通知Browser进程中的BrowserMediaPlayerManager对象,当前正在处理的播放器已经获得了视频元数据。这个BrowserMediaPlayerManager对象是通过调用MediaPlayerBridge类的成员函数manager获得的,并且是通过调用它的成员函数OnMediaMetadataChanged对它进行通知的。

       BrowserMediaPlayerManager类的成员函数OnMediaMetadataChanged的实现如下所示:

void BrowserMediaPlayerManager::OnMediaMetadataChanged(
    int player_id, base::TimeDelta duration, int width, int height,
    bool success) {
  Send(new MediaPlayerMsg_MediaMetadataChanged(
      RoutingID(), player_id, duration, width, height, success));
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数OnMediaMetadataChanged主要是向Render进程发送一个类型为MediaPlayerMsg_MediaMetadataChanged的IPC消息,通知ID为player_id的播放器已经获得了要播放的视频的元数据。

       Render进程是通过RendererMediaPlayerManager类的成员函数OnMessageReceived接收类型为MediaPlayerMsg_MediaMetadataChanged的IPC消息的,如下所示:

bool RendererMediaPlayerManager::OnMessageReceived(const IPC::Message& msg) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RendererMediaPlayerManager, msg)
    IPC_MESSAGE_HANDLER(MediaPlayerMsg_MediaMetadataChanged,
                        OnMediaMetadataChanged)
    ......
  IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数OnMessageReceived将类型为MediaPlayerMsg_MediaMetadataChanged的IPC消息分发给另外一个成员函数OnMediaMetadataChanged处理,如下所示:

void RendererMediaPlayerManager::OnMediaMetadataChanged(
    int player_id,
    base::TimeDelta duration,
    int width,
    int height,
    bool success) {
  WebMediaPlayerAndroid* player = GetMediaPlayer(player_id);
  if (player)
    player->OnMediaMetadataChanged(duration, width, height, success);
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数OnMediaMetadataChanged首先根据参数player_id获得之前创建的一个类型为WebMediaPlayerAndroid的播放器,然后调用这个播放器的成员函数OnMediaMetadataChanged通知它,当前要播放的视频元数据已经获取回来了。

       WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged的实现如下所示:

void WebMediaPlayerAndroid::OnMediaMetadataChanged(
    const base::TimeDelta& duration, int width, int height, bool success) {
  ......

  if (ready_state_ != WebMediaPlayer::ReadyStateHaveEnoughData) {
    UpdateReadyState(WebMediaPlayer::ReadyStateHaveMetadata);
    UpdateReadyState(WebMediaPlayer::ReadyStateHaveEnoughData);
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员变量read_state_描述的播放器的状态。如果它的状态不等于WebMediaPlayer::ReadyStateHaveEnoughData,即还没有足够的数据开始播放视频,那么WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged此时就会先将状态更改为WebMediaPlayer::ReadyStateHaveMetadata,然后再更改为WebMediaPlayer::ReadyStateHaveEnoughData。这都是通过调用WebMediaPlayerAndroid类的成员函数UpdateReadyState实现的。

       WebMediaPlayerAndroid类的成员函数UpdateReadyState的实现如下所示:

void WebMediaPlayerAndroid::UpdateReadyState(
    WebMediaPlayer::ReadyState state) {
  ready_state_ = state;
  client_->readyStateChanged();
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数UpdateReadyState除了将Content模块中的播放器的状态设置为参数state描述的值之外,还会通知WebKit层中的播放器,也就是一个WebMediaPlayerClientImpl对象,它的状态发生了变化。

       上述WebMediaPlayerClientImpl对象保存在WebMediaPlayerAndroid类的成员变量client_中,通过调用它的成员函数readyStateChanged可以通知它,播放器状态发生了变化,如下所示:

void WebMediaPlayerClientImpl::readyStateChanged()
{
    m_client->mediaPlayerReadyStateChanged();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       从前面的分析可以知道,WebMediaPlayerClientImpl类的成员变量m_client指向的是一个HTMLMediaElement对象。这个HTMLMediaElement对象描述的就是要播放视频的<video>标签。WebMediaPlayerClientImpl类的成员函数readyStateChanged调用这个HTMLMediaElement对象的成员函数mediaPlayerReadyStateChanged通知它,播放器状态发生了变化,如下所示:

void HTMLMediaElement::mediaPlayerReadyStateChanged()
{
    setReadyState(static_cast<ReadyState>(webMediaPlayer()->readyState()));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数mediaPlayerReadyStateChanged首先获得Content模块中的播放器的当前状态,然后再调用另外一个成员函数setReadyState将这个状态保存在内部。从前面的分析可以知道,Content模块中的播放器的当前状态为WebMediaPlayer::ReadyStateHaveEnoughData,对应于在WebKit中定义的状态HAVE_ENOUGH_DATA。

       HTMLMediaElement类的成员函数setReadyState的实现如下所示:

void HTMLMediaElement::setReadyState(ReadyState state)
{
    ......

    bool tracksAreReady = textTracksAreReady();
    ......

    if (tracksAreReady)
        m_readyState = newState;
    else {
        // If a media file has text tracks the readyState may not progress beyond HAVE_FUTURE_DATA until
        // the text tracks are ready, regardless of the state of the media file.
        if (newState <= HAVE_METADATA)
            m_readyState = newState;
        else
            m_readyState = HAVE_CURRENT_DATA;
    }

    ......

    updatePlayState();
    
    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员变量m_readyState用来描述WebKit层中的播放器的状态。HTMLMediaElement类的成员函数setReadyState首先调用成员函数textTracksAreReady检查当前要播放的视频是否存在类型为Text的Track,也就是字幕。如果存在,并且这些Track都已经Ready,那么它的返回值就会等于true。另一方面,如果不存在类型为Text的Track,那么调用HTMLMediaElement类的成员函数textTracksAreReady得到的返回值也会等于true。

       在HTMLMediaElement类的成员函数textTracksAreReady的返回值等于true的情况下,HTMLMediaElement类的成员函数setReadyState才会将WebKit层中的播放器的状态设置为参数state的值,也就是将它的状态保持与Content模块中的播放器一致。否则的话,至多会将WebKit层中的播放器的状态设置为HAVE_CURRENT_DATA。这个HAVE_CURRENT_DATA状态不能让播放器开始播放视频。

       我们假设当前要播放的视频不存在类型为Text的Track。因此,这时候WebKit层中的播放器的状态将会被设置为HAVE_ENOUGH_DATA,也就是HTMLMediaElement类的成员变量m_readyState会被设置为HAVE_ENOUGH_DATA。

       HTMLMediaElement类的成员函数setReadyState最后调用成员函数updatePlayState开始播放视频,如下所示:

void HTMLMediaElement::updatePlayState()
{
    ......

    bool shouldBePlaying = potentiallyPlaying();
    bool playerPaused = m_player->paused();

    .....

    if (shouldBePlaying) {
        ......

        if (playerPaused) {
            ......

            m_player->play();
        }

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数updatePlayState首先调用另外一个成员函数potentiallyPlaying检查WebKit层中的播放器的状态。如果WebKit层中的播放器的状态表明它已经获得了足够的视频数据,并且视频还没有播放结束,以及没有被用户中止,也没有出现错误等,那么HTMLMediaElement类的成员函数potentiallyPlaying的返回值就会等于true。在这种情况下,如果Content模块中的播放器目前处于暂停状态,那么就可以通知它开始播放视频了。这是通过调用HTMLMediaElement类的成员变量m_player指向的一个WebMediaPlayerClientImpl对象的成员函数play实现的。

       从前面的分析可以知道,在我们这个情景中,通知Content模块中的播放器开始播放视频的条件是满足的,因此接下来我们就继续分析WebMediaPlayerClientImpl类的成员函数play的实现,如下所示:

void WebMediaPlayerClientImpl::play()
{
    if (m_webMediaPlayer)
        m_webMediaPlayer->play();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       从前面的分析可以知道,WebMediaPlayerClientImpl类的成员变量m_webMediaPlayer指向的是一个WebMediaPlayerAndroid对象。这个WebMediaPlayerAndroid对象描述的就是Content模块中的播放器实例。WebMediaPlayerClientImpl类的成员函数play调用这个WebMediaPlayerAndroid对象的成员函数play通知它开始播放视频。

       WebMediaPlayerAndroid类的成员函数play的实现如下所示:

void WebMediaPlayerAndroid::play() {
  ......

  TryCreateStreamTextureProxyIfNeeded();
  // There is no need to establish the surface texture peer for fullscreen
  // video.
  if (hasVideo() && needs_establish_peer_ &&
      !player_manager_->IsInFullscreen(frame_)) {
    EstablishSurfaceTexturePeer();
  }

  if (paused())
    player_manager_->Start(player_id_);
  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数play首先调用成员函数TryCreateStreamTextureProxyIfNeeded检查当前正在处理的WebMediaPlayerAndroid对象是否已经创建过一个纹理对象。如果还没有创建,那么就会请求GPU进程进行创建。

       WebMediaPlayerAndroid类的成员函数play接下来又检查是否需要将上述纹理对象封装为一个SurfaceTexture,然后再将该SurfaceTexture设置为Android平台的MediaPlayer的解码输出。在满足以下两个条件时,就需要进行封装和设置,也就是调用另外一个成员函数EstablishSurfaceTexturePeer:

       1. 要播放的媒体包含有视频,即调用成员函数hasVideo得到的返回值等于true。

       2. 要播放的视频不是全屏模式,这时候成员变量needs_establish_peer_的值等于true,以及调用调用成员变量player_manager_指向的RendererMediaPlayerManager对象的成员函数IsInFullscreen得到的返回值等于false。

       在我们这个情景中,上述两个条件都是满足的。不过,WebMediaPlayerAndroid类的TryCreateStreamTextureProxyIfNeeded和EstablishSurfaceTexturePeer的实现我们在接下来的一篇文章中再详细分析。

       WebMediaPlayerAndroid类的成员函数play最后检查当前正在处理的WebMediaPlayerAndroid对象描述的播放器是否处于暂停状态。如果是的话,那么就会调用成员变量player_manager_指向的RendererMediaPlayerManager对象的成员函数Start启动该播放器。

       接下来我们就继续分析RendererMediaPlayerManager类的成员函数Start的实现,以便了解类型为WebMediaPlayerAndroid的播放器的启动过程,如下所示:

void RendererMediaPlayerManager::Start(int player_id) {
  Send(new MediaPlayerHostMsg_Start(routing_id(), player_id));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数Start向Browser进程发送一个类型为MediaPlayerHostMsg_Start的IPC消息,用来请求启动ID值为player_id的播放器。

       Browser进程通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_Start的IPC消息,如下所示:

bool MediaWebContentsObserver::OnMediaPlayerMessageReceived(
    const IPC::Message& msg,
    RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg)
    ......
    IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_Start,
                        GetMediaPlayerManager(render_frame_host),
                        BrowserMediaPlayerManager::OnStart)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。

       MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived会将类型为MediaPlayerHostMsg_Start的IPC消息分发给运行在Browser进程中的一个BrowserMediaPlayerManager对象的成员函数OnStart处理,如下所示:

void BrowserMediaPlayerManager::OnStart(int player_id) {
  MediaPlayerAndroid* player = GetPlayer(player_id);
  ......
  player->Start();
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数OnStart首先调用成员函数GetPlayer获得与参数player_id对应的一个MediaPlayerBridge对象,然后调用这个MediaPlayerBridge对象的成员函数Start启动它所描述的播放器。注意,这里获得的MediaPlayerBridge对象使用一个类型为MediaPlayerAndroid的指针引用,这是因为MediaPlayerBridge类是从类MediaPlayerAndroid继承下来的。

       接下来我们就继续分析MediaPlayerBridge类的成员函数Start的实现,如下所示:

void MediaPlayerBridge::Start() {
  if (j_media_player_bridge_.is_null()) {
    pending_play_ = true;
    Prepare();
  } else {
    if (prepared_)
      StartInternal();
    else
      pending_play_ = true;
  }
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员变量j_media_player_bridge_是一个类型为jobject的引用,它是用来指向在Java层创建的一个MediaPlayerBridge对象的。如果这个MediaPlayerBridge对象还没有创建,那么MediaPlayerBridge类的成员变量j_media_player_bridge_的值就会等于NULL。这种情况说明我们还没有为<video>标签创建一个真正的播放器。%

作者:Luoshengyang 发表于2016/8/15 0:59:33 原文链接
阅读:43302 评论:18 查看评论

Chromium为视频标签渲染视频画面的过程分析

$
0
0

       在浏览器中,<video>标签与普通标签有一个显著不同点,它们的内容不是由浏览器自己绘制出来,而是由第三方组件提供的。例如,在Android平台上,<video>标签的内容来自于系统播放器MediaPlayer的输出。然而在非全屏模式下,<video>标签的内容又需要像普通标签一样,嵌入在HTML页面中显示,也就是由浏览器进行渲染。本文接下来就分析Chromium渲染<video>标签内容的原理。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       浏览器是否能够无缝地渲染播放器的输出,取决于播放器是否有良好的设计。一个有良好设计的播放器要有独立的输入和输出。输入就是一个URL或者一个本地文件路径,输出即为一帧一帧的视频画面。播放器都能接受URL或者本地文件路径作为输入,也就是输入这一点都能满足要求。在输出上,它的设计就很有讲究了,有上中下三种策略。

      下策是让使用者提供一个窗口作为播放器的输出。这显然是不合适的,因为一般来说,播放器的使用者除了要在窗口显示视频内容之外,还需要显示其它内容,也就是需要在窗口上放其它控件。当然,如果系统支持将一个窗口作为一个控件嵌入在另外一个窗口中显示,这种设计也未尝不可,不过这种设计太不通用了。

       中策是让使用者提供一个控件作为播放器的输出。这种方式可以解决下策中提出的问题。然而,有一类特殊的使用者,它们的主UI不是通过系统提供控件设计出来的,而是用自己的方式绘制出来的。例如,在浏览器中,网页中的元素就不是通过系统提供的控件显示出来的,而是用自己的图形渲染引擎绘制出来的。

       上策是让使用者提供一个缓冲区作为播放器的输出。这种输出使得使用者以非常灵活的方式将视频画面显示出来。不过缺点就是使用者要多做一些工作,也就是将缓冲区的内容渲染出来的。

       将播放器的输出设计为缓冲区时,有一个细节,是非常值得注意的。一般来说,播放器的输出最终要显示在屏幕上。现在流行的系统,渲染基本上都是通过GPU进行的。如果我们提供给播放器的缓冲区,是普通的缓冲区,也就是只有CPU才可以访问的缓冲区,那么使用者在使用GPU渲染的情况下,需要将缓冲区内容上传到GPU去。这就相当于是执行一个纹理上传操作。我们知道,纹理上传是一个非常慢的操作,而视频的数据又很大,分辨率通常达到1080p。因此,理想的设计是让播放器将输出写入到GPU缓冲区中去。不过,这需要系统提供支持。

       好消息是Android平台提供了这样的支持。在Android系统上,SurfaceTexture描述的就是GPU缓冲区,并且以纹理的形式进行渲染。SurfaceTexture可以进一步封装在Surface中。Android系统的MediaPlayer提供了一个setSurface接口,参数是一个Surface,用来接收解码输出,也就是视频画面。这意味着Android系统的MediaPlayer支持将解码输出写入在GPU缓冲区中。这是上上策,得益于Android系统本身的良好的设计。

      Chromium正是利用了SurfaceTexture作为MediaPlayer的解码输出,如图1所示:


图1 以SurfaceTexture作为MediaPlayer的解码输出

       从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,在Chromium的Content层,一个网页被抽象为三个Tree:CC Layer Tree、CC Pending Layer Tree和CC Active Layer Tree。其中,CC Layer Tree由Render进程中的Main线程管理,CC Pending Layer Tree和CC Active Layer Tree由Render进程中的Compositor线程管理。CC Pending Layer Tree由CC Layer Tree同步得到,CC Active Layer Tree由CC Pending Layer Tree激活得到。

       Chromium为每一个<video>标签在CC Layer Tree创建一个VideoLayer。这个VideoLayer在CC Active Layer Tree中有一个对应的VideoLayerImpl。由于网页的UI最终是通过渲染CC Active Layer Tree得到的,因此Chromium通过VideoLayerImpl接收MediaPayer的解码输出。

       接下来,我们就先分析Chromium为<video>标签在CC Layer Tree和CC Active Layer Tree中创建VideoLayer和VideoLayerImpl的过程,然后再分析MediaPlayer将解码输出交给VideoLayerImpl渲染的过程。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,当Browser进程获得要播放的视频的元数据之后,会调用WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged通知Render进程,如下所示:

void WebMediaPlayerAndroid::OnMediaMetadataChanged(
    const base::TimeDelta& duration, int width, int height, bool success) {
  ......

  if (success)
    OnVideoSizeChanged(width, height);

  ......
}
      这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

      当参数success的值等于true的时候,表示成功获取了要播放的视频的元数据,也就是长、宽和持续时间等数据。在这种情况下,WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged就会调用另外一个成员函数OnVideoSizeChanged通知要播放的视频大小发生了变化,如下所示:

void WebMediaPlayerAndroid::OnVideoSizeChanged(int width, int height) {
  ......

  // Lazily allocate compositing layer.
  if (!video_weblayer_) {
    video_weblayer_.reset(new WebLayerImpl(cc::VideoLayer::Create(this)));
    ......
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员变量video_weblayer_描述的是<video>标签在CC Layer Tree对应的一个Layer。如果这时候这个Layer还没有创建,那么WebMediaPlayerAndroid类的成员函数OnVideoSizeChanged就会进行创建,也就是创建一个VideoLayer对象。这个VideoLayer对象会进一步封装在一个WebLayerImpl对象,并且保存在WebMediaPlayerAndroid类的成员变量video_weblayer_中。关于WebLayerImpl,可以参考前面Chromium网页Layer Tree创建过程分析一文。它主要是用来连接WebKit层的Graphic Layer Tree和Content层的CC Layer Tree。

       接下来,我们主要关注VideoLayer对象的创建过程,也就是VideoLayer类的静态成员函数Create的实现,如下所示:

scoped_refptr<VideoLayer> VideoLayer::Create(VideoFrameProvider* provider) {
  return make_scoped_refptr(new VideoLayer(provider));
}
       这个函数定义在文件external/chromium_org/cc/layers/video_layer.cc中。

       从前面的调用过程可以知道,参数provider指向的是一个WebMediaPlayerAndroid对象。这个WebMediaPlayerAndroid对象描述的是Render进程提供的播放器接口。VideoLayer类的静态成员函数Create使用这个WebMediaPlayerAndroid对象创建了一个VideoPlayer对象,并且返回给调用者。

       VideoPlayer对象的创建过程,也就是VideoPlayer类的构造函数的实现,如下所示:

VideoLayer::VideoLayer(VideoFrameProvider* provider) : provider_(provider) {
  DCHECK(provider_);
}
       这个函数定义在文件external/chromium_org/cc/layers/video_layer.cc中。

       VideoPlayer类的构造函数将参数provider指向的一个WebMediaPlayerAndroid对象保存在成员变量provider_,表示当前正在创建的VideoPlayer对象要渲染的内容由它提供。

       从前面Chromium网页Layer Tree同步为Pending Layer Tree的过程分析一文可以知道,当CC Layer Tree同步为CC Pending Layer Tree的时候,CC Layer Tree中的每一个XXXLayer对象都会在CC Pending Layer Tree中有一个对应的XXXLayerImpl对象。对于<video>标签来说,它在CC Layer Tree中对应的是一个VideoLayer对象,这个VideoLayer对象在CC Pending Layer Tree中对应的是一个VideoLayerImpl对象。这个VideoLayerImpl对象是通过调用VideoLayer类的成员函数CreateLayerImpl创建的,如下所示:

scoped_ptr<LayerImpl> VideoLayer::CreateLayerImpl(LayerTreeImpl* tree_impl) {
  return VideoLayerImpl::Create(tree_impl, id(), provider_).PassAs<LayerImpl>();
}
       这个函数定义在文件external/chromium_org/cc/layers/video_layer.cc中。

       参数tree_impl指向的是一个LayerTreeImpl对象。这个LayerTreeImpl对象描述的就是CC Pending Layer Tree。VideoLayer类的成员函数CreateLayerImpl使用这个LayerTreeImpl对象,以及当前正在处理的VideoLayer对象的成员变量provider_指向的一个WebMediaPlayerAndroid对象,创建一个VideoLayerImpl对象,也就是在CC Pending Layer Tree中为<video>标签创建了一个类型为VideoLayerImpl的Layer。这是通过调用VideoLayerImpl类的静态成员函数Create实现的,如下所示:

scoped_ptr<VideoLayerImpl> VideoLayerImpl::Create(
    LayerTreeImpl* tree_impl,
    int id,
    VideoFrameProvider* provider) {
  scoped_ptr<VideoLayerImpl> layer(new VideoLayerImpl(tree_impl, id));
  layer->SetProviderClientImpl(VideoFrameProviderClientImpl::Create(provider));
  ......
  return layer.Pass();
}
      这个函数定义在文件external/chromium_org/cc/layers/video_layer_impl.cc中。

      VideoPlayerImpl类的静态成员函数Create首先创建了一个VideoLayerImpl对象,接着又调用VideoFrameProviderClientImpl类的静态成员函数Create创建了一个VideoFrameProviderClientImpl对象,如下所示:

scoped_refptr<VideoFrameProviderClientImpl>
    VideoFrameProviderClientImpl::Create(
        VideoFrameProvider* provider) {
  return make_scoped_refptr(
      new VideoFrameProviderClientImpl(provider));
}
      这个函数定义在文件external/chromium_org/cc/layers/video_frame_provider_client_impl.cc中。

      VideoFrameProviderClientImpl类的静态成员函数Create使用参数provider指向的一个WebMediaPlayerAndroid对象创建了一个VideoFrameProviderClientImpl对象,如下所示:

VideoFrameProviderClientImpl::VideoFrameProviderClientImpl(
    VideoFrameProvider* provider)
    : active_video_layer_(NULL), provider_(provider) {
  ......

  provider_->SetVideoFrameProviderClient(this);

  ......
}
       这个函数定义在文件external/chromium_org/cc/layers/video_frame_provider_client_impl.cc中。

       VideoFrameProviderClientImpl类的构造函数除了将参数provider指向的WebMediaPlayerAndroid对象保存在成员变量provider_中,还会调用这个WebMediaPlayerAndroid对象的成员函数SetVideoFrameProviderClient,将当前正在创建的VideoFrameProviderClientImpl对象作为它的Client。这个Client将会负责接收播放器的解码输出。

       WebMediaPlayerAndroid类的成员函数SetVideoFrameProviderClient的实现如下所示:

void WebMediaPlayerAndroid::SetVideoFrameProviderClient(
    cc::VideoFrameProvider::Client* client) {
  ......
  video_frame_provider_client_ = client;
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数SetVideoFrameProviderClient主要是将参数client指向的一个VideoFrameProviderClientImpl对象保存在成员变量video_frame_provider_client_中。

       回到前面分析的VideoLayerImpl类的静态成员函数Create中,它创建了一个VideoFrameProviderClientImpl对象之后,接下来会将这个VideoFrameProviderClientImpl对象设置给前面创建的VideoLayerImpl对象。这是通过调用VideoLayerImpl类的成员函数SetProviderClientImpl实现的,如下所示:

void VideoLayerImpl::SetProviderClientImpl(
    scoped_refptr<VideoFrameProviderClientImpl> provider_client_impl) {
  provider_client_impl_ = provider_client_impl;
}
       这个函数定义在文件external/chromium_org/cc/layers/video_layer_impl.cc中。

       VideoLayerImpl类的成员函数SetProviderClientImpl将参数provider_client_impl指向的一个VideoFrameProviderClientImpl对象保存在成员变量provider_client_impl_中。

       这一步执行完成之后,Chromium就为<video>标签在CC Pending Layer Tree中创建了一个VideoLayerImpl对象。从前面Chromium网页Pending Layer Tree激活为Active Layer Tree的过程分析一文可以知道,这个VideoLayerImpl对象在CC Pending Layer Tree激活为CC Active Layer Tree的时候,会变成CC Active Layer Tree中的一个节点。

       这样,Chromium就为<video>标签在CC Active Layer Tree中创建了一个类型为VideoLayerImpl的Layer。接下来我们继续分析Chromium创建SurfaceTexture接收MediaPlayer的解码输出的过程。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,获得了<video>标签要播放的视频的元数据之后,WebKit层就会请求Content层启动它的播放器对视频进行播放,也就是调用WebMediaPlayerAndroid类的成员函数play对视频进行播放,它的实现如下所示:

void WebMediaPlayerAndroid::play() {
  ......

  TryCreateStreamTextureProxyIfNeeded();
  ......

  if (hasVideo() && needs_establish_peer_ &&
      !player_manager_->IsInFullscreen(frame_)) {
    EstablishSurfaceTexturePeer();
  }

  if (paused())
    player_manager_->Start(player_id_);
  ......
}
      这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

      WebMediaPlayerAndroid类的成员函数play的详细分析可以参考前面Chromium为视频标签<video>创建播放器的过程分析一文。这里我们主要它关注它调用的另外两个成员函数TryCreateStreamTextureProxyIfNeeded和EstablishSurfaceTexturePeer的实现。其中,前者用来创建一个SurfaceTexture,后者用来将创建出来的SurfaceTexture封装成一个Surface,并且设置为MediaPlayer的解码输出。

      WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded的实现如下所示:

void WebMediaPlayerAndroid::TryCreateStreamTextureProxyIfNeeded() {
  // Already created.
  if (stream_texture_proxy_)
    return;

  ......

  stream_texture_proxy_.reset(stream_texture_factory_->CreateProxy());
  if (stream_texture_proxy_) {
    DoCreateStreamTexture();
    ReallocateVideoFrame();
    if (video_frame_provider_client_) {
      stream_texture_proxy_->BindToLoop(
          stream_id_, video_frame_provider_client_, compositor_loop_);
    }
  }
}

       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员变量stream_texture_proxy_指向的是一个StreamTextureProxyImpl对象。这个StreamTextureProxyImpl对象用来在Compositor线程中接收MediaPlayer的解码输出。

       WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded首先检查成员变量stream_texture_proxy_是否已经指向了一个StreamTextureProxyImpl对象。如果已经指向,那么就说明Chromium已经为当前正在处理的WebMediaPlayerAndroid创建过了一个SurfaceTexture。在这种情况下,WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded就什么也不做就返回。

       另一方面,如果WebMediaPlayerAndroid类的成员变量stream_texture_proxy_还没有指向一个StreamTextureProxyImpl对象,那么WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded就会调用另外一个成员变量stream_texture_factory_指向的一个StreamTextureFactoryImpl对象的成员函数CreateProxy创建一个StreamTextureProxyImpl对象,并且保存在成员变量stream_texture_proxy_中。

       WebMediaPlayerAndroid类的成员变量stream_texture_factory_指向的StreamTextureFactoryImpl对象的创建过程可以参考前面Chromium为视频标签<video>创建播放器的过程分析一文,它内部包含有一个GpuChannelHost对象。这个GpuChannelHost对象描述的是一个连接到GPU进程的GPU通道。

       StreamTextureFactoryImpl类的成员函数CreateProxy的实现如下所示:

StreamTextureProxy* StreamTextureFactoryImpl::CreateProxy() {
  DCHECK(channel_.get());
  StreamTextureHost* host = new StreamTextureHost(channel_.get());
  return new StreamTextureProxyImpl(host);
}

       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       StreamTextureFactoryImpl类的成员变量channel_描述的就是上述的GPU通道。StreamTextureFactoryImpl类的成员函数CreateProxy首先使用这个GPU通道创建一个StreamTextureHost对象,然后再将这个StreamTextureHost对象封装在一个StreamTextureProxyImpl对象中返回给调用者。这个StreamTextureHost对象描述的实际上就是Render进程接下来要求GPU进程创建的SurfaceTexture。

       回到WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded中,它创建了一个StreamTextureProxyImpl对象之后,紧接着又会调用另外一个成员函数DoCreateStreamTexture请求GPU进程创建一个SurfaceTexture,如下所示:

void WebMediaPlayerAndroid::DoCreateStreamTexture() {
  ......
  stream_id_ = stream_texture_factory_->CreateStreamTexture(
      kGLTextureExternalOES, &texture_id_, &texture_mailbox_);
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数DoCreateStreamTexture调用成员变量stream_texture_factory_指向的StreamTextureFactoryImpl对象的成员函数CreateStreamTexture请求GPU进程创建一个SurfaceTexture对象,如下所示:

unsigned StreamTextureFactoryImpl::CreateStreamTexture(
    unsigned texture_target,
    unsigned* texture_id,
    gpu::Mailbox* texture_mailbox) {
  GLuint stream_id = 0;
  gpu::gles2::GLES2Interface* gl = context_provider_->ContextGL();
  gl->GenTextures(1, texture_id);

  stream_id = gl->CreateStreamTextureCHROMIUM(*texture_id);

  gl->GenMailboxCHROMIUM(texture_mailbox->name);
  gl->BindTexture(texture_target, *texture_id);
  gl->ProduceTextureCHROMIUM(texture_target, texture_mailbox->name);
  return stream_id;
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       在Android中,创建一个SurfaceTexture对象,需要指定一个纹理ID。因此,StreamTextureFactoryImpl类的成员函数CreateStreamTexture在请求GPU进程创建SurfaceTexture对象之前,首先会创建一个纹理。这个纹理可以通过调用Chromium为Render进程提供的Command Buffer OpenGL接口的成员函数GenTextures创建。

       StreamTextureFactoryImpl类的成员变量context_provider_指向的是一个ContextProviderCommandBuffer对象。调用这个ContextProviderCommandBuffer对象的成员函数ContextGL可以获得一个Command Buffer GL接口。当我们调用这个Command Buffer GL接口的成员函数执行GPU命令时,它实际上是通过Command Buffer将GPU命令发送给GPU进程执行。关于Command Buffer GL的更多知识,可以参考前面Chromium硬件加速渲染机制基础知识简要介绍和学习计划这个系列的文章。

       获得了一个纹理ID之后,StreamTextureFactoryImpl类的成员函数CreateStreamTexture就继续调用上述Command Buffer GL接口的成员函数CreateStreamTextureCHROMIUM请求GPU进程创建一个SurfaceTexutre对象。

       创建了SurfaceTexutre对象之后,StreamTextureFactoryImpl类的成员函数CreateStreamTexture还会为前面创建出来的纹理创建一个Mailbox。这个Mailbox的作用是将与它关联的纹理发送给Browser进程进行合成,以便显示在浏览器窗口中。这个纹理描述的实际上就是MediaPlayer的解码输出,因此,上述Mailbox是用来将MediaPlayer的解码输出交给Browser进程合成显示在浏览器窗口中。关于Mailbox的更多知识,可以参考前面Chromium硬件加速渲染的UI合成过程分析一文。

       接下来我们继续分析Render进程请求GPU进程创建SurfaceTexture对象的过程,也就是Command Buffer GL接口的成员函数CreateStreamTextureCHROMIUM的实现。

       从前面Chromium硬件加速渲染机制基础知识简要介绍和学习计划这个系列的文章可以知道,Chromium是通过GLES2Implementation类来描述Command Buffer GL接口的,因此接下来我们分析GLES2Implementation类的成员函数CreateStreamTextureCHROMIUM的实现,如下所示:

GLuint GLES2Implementation::CreateStreamTextureCHROMIUM(GLuint texture) {
  ......
  return gpu_control_->CreateStreamTexture(texture);
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/client/gles2_implementation.cc中。

       从前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文可以知道,GLES2Implementation类的成员变量gpu_control_指向的是一个CommandBufferProxyImpl对象。GLES2Implementation类的成员函数CreateStreamTextureCHROMIUM调用这个CommandBufferProxyImpl对象的成员函数CreateStreamTexture请求GPU进程创建一个SurfaceTexture对象,如下所示:

uint32 CommandBufferProxyImpl::CreateStreamTexture(uint32 texture_id) {
  ......

  int32 stream_id = channel_->GenerateRouteID();
  bool succeeded;
  Send(new GpuCommandBufferMsg_CreateStreamTexture(
      route_id_, texture_id, stream_id, &succeeded));
  ......

  return stream_id;
}
       这个函数定义在文件external/chromium_org/content/common/gpu/client/command_buffer_proxy_impl.cc中。

       CommandBufferProxyImpl类的成员函数CreateStreamTexture向GPU进程发送一个类型为GpuCommandBufferMsg_CreateStreamTexture的IPC消息,用来请求创建一个SurfaceTexture对象。

       GPU进程通过GpuCommandBufferStub类的成员函数OnMessageReceived接收类型为GpuCommandBufferMsg_CreateStreamTexture的IPC消息,如下所示:

bool GpuCommandBufferStub::OnMessageReceived(const IPC::Message& message) {
  ......

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(GpuCommandBufferStub, message)
    ......
    IPC_MESSAGE_HANDLER(GpuCommandBufferMsg_CreateStreamTexture,
                        OnCreateStreamTexture)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  .....

  return handled;
}
       这个函数定义在文件external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。

       GpuCommandBufferStub类的成员函数OnMessageReceived将类型为GpuCommandBufferMsg_CreateStreamTexture的IPC消息分发给另外一个成员函数OnCreateStreamTexture处理,如下所示:

void GpuCommandBufferStub::OnCreateStreamTexture(
    uint32 texture_id, int32 stream_id, bool* succeeded) {
#if defined(OS_ANDROID)
  *succeeded = StreamTexture::Create(this, texture_id, stream_id);
#else
  *succeeded = false;
#endif
}
       这个函数定义在文件external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。

       SurfaceTexture是Android平台特有的接口,因此GpuCommandBufferStub类的成员函数OnCreateStreamTexture只有在Android平台上才会响应请求创建一个SurfaceTexture对象。

       这个SurfaceTexture对象是通过调用StreamTexture类的静态成员函数Create创建的,如下所示:

bool StreamTexture::Create(
    GpuCommandBufferStub* owner_stub,
    uint32 client_texture_id,
    int stream_id) {
  GLES2Decoder* decoder = owner_stub->decoder();
  TextureManager* texture_manager =
      decoder->GetContextGroup()->texture_manager();
  TextureRef* texture = texture_manager->GetTexture(client_texture_id);

  if (texture && (!texture->texture()->target() ||
                  texture->texture()->target() == GL_TEXTURE_EXTERNAL_OES)) {

    // TODO: Ideally a valid image id was returned to the client so that
    // it could then call glBindTexImage2D() for doing the following.
    scoped_refptr<gfx::GLImage> gl_image(
        new StreamTexture(owner_stub, stream_id, texture->service_id()));
    gfx::Size size = gl_image->GetSize();
    texture_manager->SetTarget(texture, GL_TEXTURE_EXTERNAL_OES);
    texture_manager->SetLevelInfo(texture,
                                  GL_TEXTURE_EXTERNAL_OES,
                                  0,
                                  GL_RGBA,
                                  size.width(),
                                  size.height(),
                                  1,
                                  0,
                                  GL_RGBA,
                                  GL_UNSIGNED_BYTE,
                                  true);
    texture_manager->SetLevelImage(
        texture, GL_TEXTURE_EXTERNAL_OES, 0, gl_image);
    return true;
  }

  return false;
}
      这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

      参数client_texture_id描述的是一个纹理ID,不过这个纹理ID是在Render进程中分配的,称为Client ID。在Chromium中,所有的OpenGL相关的对象都是需要在GPU进程创建的。GPU进程在创建这些对象的时候,会获得一个相应的ID,称为Service ID。Client ID和Service ID是一一对应的。

       例如,Render进程需要一个纹理对象时。它就会先在本进程获得一个Client ID,然后该Client ID发送给GPU进程,让GPU进程调用OpenGL函数为其创建一个纹理对象。这个纹理对象与指定的Client ID关联,并且被封装在一个TextureRef对象中,由GPU进程中的一个TextureManager对象进行管理。调用这个TextureRef对象的成员函数service_id可以获得一个Service ID。这个Service ID实际上就是调用OpenGL函数glGenTextures时获得的纹理ID。

       StreamTexture类的静态成员函数Create在为参数client_texture_id创建SurfaceTexture之前,首先要确保它已经与一个TextureRef对象关联,也就是GPU进程已经为它创建过纹理对象。此外,StreamTexture类的静态成员函数Create还需要确保这个纹理对象的类型为GL_TEXTURE_EXTERNAL_OES,而不是GL_TEXTURE。类型为GL_TEXTURE的纹理是OpenGL提供的标准纹理,而类型为GL_TEXTURE_EXTERNAL_OES的纹理是由厂商扩展的,它有特殊的用途。在Android平台上,它的特殊用途就是用来创建SurfaceTexture。

        一旦上述条件得到满足了,StreamTexture类的静态成员函数Create就会创建一个StreamTexture对象,并且将这个StreamTexture对象交给对应的TextureManager对象管理。这个StreamTexture对象在创建的过程中,同时会创建一个SurfaceTexture对象,如下所示:

StreamTexture::StreamTexture(GpuCommandBufferStub* owner_stub,
                             int32 route_id,
                             uint32 texture_id)
    : surface_texture_(gfx::SurfaceTexture::Create(texture_id)),
      ...... {
  ......
  surface_texture_->SetFrameAvailableCallback(base::Bind(
      &StreamTexture::OnFrameAvailable, weak_factory_.GetWeakPtr()));
}
       这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

       StreamTexture类的构造函数通过调用SurfaceTexture类的静态成员函数Create创建一个SurfaceTexture对象,并且保存在成员变量surface_texture_中。SurfaceTexture类的静态成员函数Create的实现可以参考前面Chromium网页CPU光栅化原理分析一文。它实际上是通过JNI在Java层创建了一个由Android SDK提供的SurfaceTexture对象。这个Java层的SurfaceTexture对象会被封装在C++层中的一个SurfaceTexture对象中。这个C++层的SurfaceTexture对象就保存在StreamTexture类的成员变量surface_texture_中。

       StreamTexture类的构造函数最后还做的一件事情是调用前面创建的C++层的SurfaceTexture对象的成员函数SetFrameAvailableCallback,用来设置一个Frame Available Callback。这个Frame Available Callback绑定了StreamTexture类的成员函数OnFrameAvailable。这意味着当上述设置的Frame Available Callback被执行时,StreamTexture类的成员函数OnFrameAvailable就会被调用。

       C++层的SurfaceTexture类的成员函数SetFrameAvailableCallback又会通过JNI调用Java层的SurfaceTexture类的成员函数setOnFrameAvailableListener给前面在Java层创建的SurfaceTexture对象设置一个Frame Available Listener。一个SurfaceTexture对象描述的实际上是一个GPU缓冲区队列。这是一个生产者/消费者队列。在我们这个情景中,生产者即为Android系统提供的MediaPlayer,消费者即为上述在Java层设置的Frame Available Listener。

       每当MediaPlayer解码出一帧视频之后,它都会从设置给它的SurfaceTexture对象中获得一个GPU缓冲区,然后将解码出来的视频帧数据拷贝到该GPU缓冲区中去,并且通知上述在Java层设置的Frame Available Listener,有新的GPU缓冲区可用。Java层的Frame Available Listener又会进一步通过JNI执行上述在C++层设置的Frame Available Callback。这时候StreamTexture类的成员函数OnFrameAvailable就会被调用。

       StreamTexture类的成员函数OnFrameAvailable的调用过程我们后面再分析。现在回到前面分析的WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded中,这时候它就请求GPU进程创建了一个SurfaceTexture对象。

       WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded接下来调用成员函数ReallocateVideoFrame将前面用来创建SurfaceTexture的纹理封装在一个VideoFrame对象中,如下所示:

void WebMediaPlayerAndroid::ReallocateVideoFrame() {
  if (needs_external_surface_) {
    ......
  } else if (!is_remote_ && texture_id_) {
    ......

    scoped_refptr<VideoFrame> new_frame = VideoFrame::WrapNativeTexture(
        make_scoped_ptr(new gpu::MailboxHolder(
            texture_mailbox_, texture_target, texture_mailbox_sync_point)),
        media::BindToCurrentLoop(base::Bind(
            &OnReleaseTexture, stream_texture_factory_, texture_id_ref)),
        natural_size_,
        gfx::Rect(natural_size_),
        natural_size_,
        base::TimeDelta(),
        VideoFrame::ReadPixelsCB());
    SetCurrentFrameInternal(new_frame);
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       当Chromium用来实现WebView时,WebMediaPlayerAndroid类的成员变量needs_external_surface_的值会等于true。我们不考虑这一种情况。

       当WebMediaPlayerAndroid类的成员变量is_remote_的值等于true时,表示<video>标签的视频在一个远程设备上播放,也就是不在本设备上播放。我们也不考虑这一种情况。

       从前面的调用过程可以知道,WebMediaPlayerAndroid类的成员变量texture_id_描述的是一个纹理ID。这个纹理ID就是前面用来创建SurfaceTexture对象所使用的纹理ID。这个纹理ID同时封装在一个Mailbox中。这个Mailbox由WebMediaPlayerAndroid类的另外一个成员变量texture_mailbox_中。

       排除上述的两种情况之后,如果WebMediaPlayerAndroid类的成员变量texture_id_的值不等于0,也就是前面我们成功创建了一个SurfaceTexture对象,那么WebMediaPlayerAndroid类的成员函数ReallocateVideoFrame就会创建一个VideoFrame对象。这个VideoFrame对象封装了WebMediaPlayerAndroid类的成员变量texture_mailbox_所描述的一个Mailbox。这个Mailbox同时又与前面用来创建SurfaceTexture对象的纹理关联。因此,以后通过这里创建出来的VideoFrame对象将可以访问到前面创建出来的SurfaceTexture对象的内容,也就是MediaPlayer的解码输出。

       WebMediaPlayerAndroid类的成员函数ReallocateVideoFrame最后调用另外一个成员函数SetCurrentFrameInternal将前面创建出来的VideoFrame保存在内部,如下所示:

void WebMediaPlayerAndroid::SetCurrentFrameInternal(
    scoped_refptr<media::VideoFrame>& video_frame) {
  base::AutoLock auto_lock(current_frame_lock_);
  current_frame_ = video_frame;
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数SetCurrentFrameInternal将参数video_frame描述的VideoFrame对象保存在成员变量current_frame_中。后面我们将会看到这个VideoFrame对象是如何使用的。

       这一步执行完成后,再回到WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded中,它最后还要做一件事情,就是指定一个对象处理MediaPlayer的解码输出,这个对象就是它的成员变量video_frame_provider_client_所指向的VideoFrameProviderClientImpl对象。

       为了方便描述,我们重新列出WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded的实现,如下所示:

void WebMediaPlayerAndroid::TryCreateStreamTextureProxyIfNeeded() {
  // Already created.
  if (stream_texture_proxy_)
    return;

  ......

  stream_texture_proxy_.reset(stream_texture_factory_->CreateProxy());
  if (stream_texture_proxy_) {
    DoCreateStreamTexture();
    ReallocateVideoFrame();
    if (video_frame_provider_client_) {
      stream_texture_proxy_->BindToLoop(
          stream_id_, video_frame_provider_client_, compositor_loop_);
    }
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员变量video_frame_provider_client_指向的VideoFrameProviderClientImpl对象的创建过程可以参考前面的分析。WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded通过调用前面创建的StreamTextureProxyImpl对象的成员函数BindToLoop指定成员变量video_frame_provider_client_指向的VideoFrameProviderClientImpl对象在Render进程的Compositor线程中处理MediaPlayer的解码输出,如下所示:

void StreamTextureProxyImpl::BindToLoop(
    int32 stream_id,
    cc::VideoFrameProvider::Client* client,
    scoped_refptr<base::MessageLoopProxy> loop) {
  DCHECK(loop);

  {
    base::AutoLock lock(lock_);
    DCHECK(!loop_ || (loop == loop_));
    loop_ = loop;
    client_ = client;
  }

  if (loop->BelongsToCurrentThread()) {
    BindOnThread(stream_id);
    return;
  }
  // Unretained is safe here only because the object is deleted on |loop_|
  // thread.
  loop->PostTask(FROM_HERE,
                 base::Bind(&StreamTextureProxyImpl::BindOnThread,
                            base::Unretained(this),
                            stream_id));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       StreamTextureProxyImpl类的成员函数BindToLoop首先将参数client指向的VideoFrameProviderClientImpl对象保存在成员变量client_中,以便以后可以将MediaPlayer的解码输出交给它处理。

       另外一个参数loop描述的是Render进程的Compositor线程的消息队列。StreamTextureProxyImpl类的成员函数BindToLoop接下来判断当前正在执行的线程是否就是Compositor线程。如果是的话,那么就直接调用另外一个成员函数BindToThread,用来在Compositor线程中设置一个Route,接收MediaPlayer的解码输出通知。如果不是的话,那么就需要向Compositor线程的消息队列发送一个Task。当这个Task在Compositor线程中执行时,再调用StreamTextureProxyImpl类的成员函数BindToThread中,用来确保在Compositor线程获得MediaPlayer的解码输出通知。

       StreamTextureProxyImpl类的成员函数BindOnThread的实现如下所示:

void StreamTextureProxyImpl::BindOnThread(int32 stream_id) {
  host_->BindToCurrentThread(stream_id, this);
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       StreamTextureProxyImpl类的成员变量host_指向的是一个StreamTextureHost对象。StreamTextureProxyImpl类的成员函数BindOnThread调用这个StreamTextureHost对象的成员函数BindToCurrentThread让其在Compositor线程中接收MediaPlayer的解码输出通知,如下所示:

bool StreamTextureHost::BindToCurrentThread(int32 stream_id,
                                            Listener* listener) {
  listener_ = listener;
  if (channel_.get() && stream_id && !stream_id_) {
    stream_id_ = stream_id;
    channel_->AddRoute(stream_id, weak_ptr_factory_.GetWeakPtr());
    channel_->Send(new GpuStreamTextureMsg_StartListening(stream_id));
    return true;
  }

  return false;
}
       这个函数定义在文件external/chromium_org/content/renderer/gpu/stream_texture_host_android.cc中。

       从前面的调用过程可以知道,参数listener指向的是一个StreamTextureProxyImpl对象。StreamTextureHost类的成员函数BindToCurrentThread首先将这个StreamTextureProxyImpl对象保存在成员变量listener_中。

       StreamTextureHost类的另外一个成员变量channel_描述的是一个GPU通道。这个GPU通道就是前面Render请求GPU进程创建SurfaceTexture所用的GPU通道。StreamTextureHost类的成员函数BindToCurrentThread将当前正在处理的StreamTextureHost对象设置为该GPU通道的一个Route,用来接收从GPU进程发送过来的Routing ID为stream_id的IPC消息,也就是那些与前面创建的SurfaceTexture相关的IPC消息。

       StreamTextureHost类的成员函数最后通过成员变量channel_描述的GPU通道向GPU进程发送一个类型为GpuStreamTextureMsg_StartListening的IPC消息,表示Render进程已经准备就绪接收MediaPlayer的解码输出。

       GPU进程通过StreamTexture类的成员函数OnMessageReceived接收类型为GpuStreamTextureMsg_StartListening的IPC消息,如下所示:

bool StreamTexture::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(StreamTexture, message)
    IPC_MESSAGE_HANDLER(GpuStreamTextureMsg_StartListening, OnStartListening)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  DCHECK(handled);
  return handled;
}
      这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

      StreamTexture类的成员函数OnMessageReceived将类型为StreamTextureMsg_StartListening的IPC消息分发给另外一个成员函数OnStartListening处理,如下所示:

void StreamTexture::OnStartListening() {
  DCHECK(!has_listener_);
  has_listener_ = true;
}
       这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

       StreamTexture类的成员函数OnStartListening将成员变量has_listener_的值设置为true,表示Render进程准备就绪接收MediaPlayer的解码输出。

       这一步执行完成之后,Chromium就为<video>标签创建了一个SurfaceTexture对象,并且在Render进程中准备就绪接收MediaPlayer的解码输出。回到前面分析的WebMediaPlayerAndroid类的成员函数play中,它接下来就会将前面创建的SurfaceTexture对象设置为MediaPlayer的解码输出。这是通过调用WebMediaPlayerAndroid类的另外一个成员函数EstablishSurfaceTexturePeer实现的,如下所示:

void WebMediaPlayerAndroid::EstablishSurfaceTexturePeer() {
  ......

  if (stream_texture_factory_.get() && stream_id_)
    stream_texture_factory_->EstablishPeer(stream_id_, player_id_);
  
  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数EstablishSurfaceTexturePeer调用成员变量stream_texture_factory_指向的StreamTextureFactoryImpl对象的成员函数EstablishPeer将之前创建的SurfaceTexture对象设置为MediaPlayer的解码输出,如下所示:

void StreamTextureFactoryImpl::EstablishPeer(int32 stream_id, int player_id) {
  DCHECK(channel_.get());
  channel_->Send(
      new GpuStreamTextureMsg_EstablishPeer(stream_id, frame_id_, player_id));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       StreamTextureFactoryImpl类的成员函数EstablishPeer通过成员变量channel_描述的GPU通道向GPU进程发送一个类型为GpuStreamTextureMsg_EstablishPeer的IPC消息,请求它将之前创建的SurfaceTexture对象设置为MediaPlayer的解码输出。

       GPU进程通过StreamTexture类的成员函数OnMessageReceived接收类型为GpuStreamTextureMsg_EstablishPeer的IPC消息,如下所示:

bool StreamTexture::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(StreamTexture, message)
    ......
    IPC_MESSAGE_HANDLER(GpuStreamTextureMsg_EstablishPeer, OnEstablishPeer)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  DCHECK(handled);
  return handled;
}
       这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

       StreamTexture类的成员函数OnMessageReceived将类型为GpuStreamTextureMsg_EstablishPeer的IPC消息分发给另外一个成员函数OnEstablishPeer处理,如下所示:

void StreamTexture::OnEstablishPeer(int32 primary_id, int32 secondary_id) {
  ......

  SurfaceTexturePeer::GetInstance()->EstablishSurfaceTexturePeer(
      process, surface_texture_, primary_id, secondary_id);
}
       这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

       从前面的分析可以知道,StreamTexture类的成员变量surface_texture_指向的SurfaceTexture对象就是之前Render进程请求GPU进程创建的SurfaceTexture对象。现在需要将这个SurfaceTexture对象设置为MediaPlayer的解码输出。这是通过调用运行在GPU进程中的一个SurfaceTexturePeer单例对象的成员函数EstablishSurfaceTexturePeer实现的。

       SurfaceTexturePeer类的成员函数EstablishSurfaceTexturePeer的实现如下所示:

void SurfaceTexturePeerBrowserImpl::EstablishSurfaceTexturePeer(
    base::ProcessHandle render_process_handle,
    scoped_refptr<gfx::SurfaceTexture> surface_texture,
    int render_frame_id,
    int player_id) {
  ......

  BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
      &SetSurfacePeer, surface_texture, render_process_handle,
      render_frame_id, player_id));
}
       这个函数定义在文件external/chromium_org/content/browser/android/surface_texture_peer_browser_impl.cc中。

       在Android平台中,Chromium的GPU进程与Browser进程实际上是同一个进程。SurfaceTexturePeer类的成员函数EstablishSurfaceTexturePeer于是就在Browser进程的UI线程中执行函数SetSurfacePeer,目的是将参数surface_texture描述的SurfaceTexture对象设置为MediaPlayer的解码输出。

       函数SetSurfacePeer的实现如下所示:

static void SetSurfacePeer(
    scoped_refptr<gfx::SurfaceTexture> surface_texture,
    base::ProcessHandle render_process_handle,
    int render_frame_id,
    int player_id) {
  int render_process_id = 0;
  RenderProcessHost::iterator it = RenderProcessHost::AllHostsIterator();
  while (!it.IsAtEnd()) {
    if (it.GetCurrentValue()->GetHandle() == render_process_handle) {
      render_process_id = it.GetCurrentValue()->GetID();
      break;
    }
    it.Advance();
  }
  ......

  RenderFrameHostImpl* frame =
      RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
  ......

  RenderViewHostImpl* view =
      static_cast<RenderViewHostImpl*>(frame->GetRenderViewHost());
  BrowserMediaPlayerManager* player_manager =
      view->media_web_contents_observer()->GetMediaPlayerManager(frame);
  ......

  media::MediaPlayerAndroid* player = player_manager->GetPlayer(player_id);
  ......

  if (player != player_manager->GetFullscreenPlayer()) {
    gfx::ScopedJavaSurface scoped_surface(surface_texture);
    player->SetVideoSurface(scoped_surface.Pass());
  }
}

       这个函数定义在文件external/chromium_org/content/browser/android/surface_texture_peer_browser_impl.cc中。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,Render进程在解析当前正在加载的网页的<video>标签时,会请求Browser进程分别为它们创建一个MediaPlayerBridge对象。这些MediaPlayerBridge对象描述的是Browser进程为<video>标签提供的播放器接口。每一个MediaPlayerBridge对象在Java层都会创建一个由Android系统提供的播放器实例,也就是一个MediaPlayer对象。

       函数SetSurfacePeer要做的事情就是在Browser进程找到ID值为player_id的一个MediaPlayerBridge对象,然后将参数surface_texture描述的SurfaceTexture对象设置为它在Java层创建的MediaPlayer的解码输出。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,在Render进程中创建的每一个播放器实例,也就是每一个MediaPlayerAndroid对象,都有一个Player ID。这些Player ID只在当前的Render进程中唯一(Chromium可能存多个Render进程)。因此,函数SetSurfacePeer不能仅仅根据参数player_id就能找到其对应的MediaPlayerBridge对象。

       为了找到目标MediaPlayerBridge对象,函数SetSurfacePeer首先要找到目标MediaPlayerBridge对象所对应的Render进程。这可以通过参数render_process_handle获得。然而,知道目标MediaPlayerBridge对象所对应的Render进程还不足够,因为一个Render进程可能会同时加载多个网页。Player ID在同一个网页内才是唯一的。这时候需要用到另外一个参数render_frame_id。这个参数render_frame_id描述的是目标MediaPlayerBridge对象所对应的网页。

       Browser为同一个网页的<video>标签创建的所有MediaPlayerBridge对象都由同一个BrowserMediaPlayerManager对象管理。知道了目标MediaPlayerBridge对象所对应的Render进程和网页之后,就可以获得这个BrowserMediaPlayerManager对象。有了这个BrowserMediaPlayerManager对象之后,就可以调用它的成员函数GetPlayer获得与参数player_id对应的MediaPlayerBridge对象。

       有了这个MediaPlayerBridge对象之后,就可以调用它的成员函数SetVideoSurface将参数surface_texture描述的SurfaceTexture对象设置为它在Java层创建的MediaPlayer的解码输出了。不过,只有在<video>标签没有启动全屏播放的时候,才可以设置。这是因为在全屏播放模式下,MediaPlayer的解码输出是交给一个全屏模式的SurfaceView渲染的。这一点我们在接下来一篇文章中再详细分析。

       这里还有一点需要注意,MediaPlayerBridge类是从MediaPlayerAndroid类继承下来的,因此,函数SetSurfacePeer可以将找到的目标MediaPlayerBridge对象保存在一个MediaPlayerAndroid指针中。

       在将参数surface_texture描述的SurfaceTexture对象设置为它在Java层创建的MediaPlayer的解码输出之前,函数SetSurfacePeer需要将这个SurfaceTexture对象封装在一个Surface对象。这是通过ScopedJavaSurface类实现的,如下所示:

ScopedJavaSurface::ScopedJavaSurface(
    const SurfaceTexture* surface_texture)
    : auto_release_(true),
      is_protected_(false) {
  JNIEnv* env = base::android::AttachCurrentThread();
  RegisterNativesIfNeeded(env);
  ScopedJavaLocalRef<jobject> tmp(JNI_Surface::Java_Surface_Constructor(
      env, surface_texture->j_surface_texture().obj()));
  DCHECK(!tmp.is_null());
  j_surface_.Reset(tmp);
}
       这个函数定义在文件external/chromium_org/ui/gl/android/scoped_java_surface.cc中。

       ScopedJavaSurface类的构造函数首先是调用参数surface_texture指向的一个C++层的SurfaceTexture对象的成员函数j_surface_texture获得它在Java层对应的SurfaceTexture对象,然后再以这个Java层的SurfaceTexture为参数,调用JNI接口JNI_Surface::Java_Surface_Constructor在Java层创建一个Surface对象。Java层Surface对象的创建接口,可以参考Android SDK文档。创建出来的Java层Surface对象,保存在ScopedJavaSurface类的成员变量j_surface_中。

      以后通过调用ScopedJavaSurface类的成员函数j_surface可以获得保存在成员变量j_surface_中的Java层Surface对象,如下所示:

class GL_EXPORT ScopedJavaSurface {
  ......

 public:
  ......

  const base::android::JavaRef<jobject>& j_surface() const {
    return j_surface_;
  }

 private:
  ......

  base::android::ScopedJavaGlobalRef<jobject> j_surface_;
};
       这个函数定义在文件external/chromium_org/ui/gl/android/scoped_java_surface.h中。

       回到前面分析的函数SetSurfacePeer中,它将参数surface_texture描述的SurfaceTexture对象封装在一个Java层Surface对象之后,接下来就调用前面找到的目标MediaPlayerBridge对象的成员函数SetVideoSurface将其设置为Java层MediaPlayer的解码输出,如下所示:

void MediaPlayerBridge::SetVideoSurface(gfx::ScopedJavaSurface surface) {
  if (j_media_player_bridge_.is_null()) {
    if (surface.IsEmpty())
      return;
    Prepare();
  }

  JNIEnv* env = base::android::AttachCurrentThread();
  CHECK(env);
  is_surface_in_use_ = true;
  Java_MediaPlayerBridge_setSurface(
      env, j_media_player_bridge_.obj(), surface.j_surface().obj());
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,MediaPlayerBridge类的成员变量j_media_player_bridge_指向的是Java层的一个MediaPlayerBridge对象。如果这个MediaPlayerBridge对象还没有创建出来,那么C++层的MediaPlayerBridge类的成员函数SetVideoSurface就会先调用另外一个成员函数Prepare进行创建。

       在我们这个情景中,MediaPlayerBridge类的成员变量j_media_player_bridge_指向的Java层MediaPlayerBridge对象已经创建。这时候C++层的MediaPlayerBridge类的成员函数SetVideoSurface就会通过JNI接口Java_MediaPlayerBridge_setSurface调用这个Java层MediaPlayerBridge对象的成员函数setSurface将参数surface描述的Java层Surface设置为它内部创建的MediaPlayer的解码输出。

       Java层的MediaPlayerBridge类的成员函数setSurface的实现如下所示:

public class MediaPlayerBridge {
    ......

    @CalledByNative
    protected void setSurface(Surface surface) {
        getLocalPlayer().setSurface(surface);
    }

    ......
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       MediaPlayerBridge类的成员函数setSurface首先调用另外一个成员函数getLocalPlayer获得内部创建的一个MediaPlayer对象。这个MediaPlayer对象描述的就是Android系统提供的播放器实例,它的创建过程可以参考前面Chromium为视频标签<video>创建播放器的过程分析一文。有了这个MediaPlayer对象之后,MediaPlayerBridge类的成员函数setSurface就将参数surface描述的Surface对象设置为它的解码输出。

       从前面的分析可以知道,参数surface描述的Surface对象封装了一个SurfaceTexture对象。当MediaPlayer开始播放视频时,它每解码出来的一帧,都会从上述被封装的SurfaceTexture对象中获得一个GPU缓冲区,并且将视频帧数据写入到该GPU缓冲区中,然后再通过设置给上述被封装的SurfaceTexture对象的Frame Available Listener,调用C++层的StreamTexture类的成员函数OnFrameAvailable,用来通知它MediaPlayer有新的解码输出需要渲染。

       C++层的StreamTexture类的成员函数OnFrameAvailable的实现如下所示:

void StreamTexture::OnFrameAvailable() {
  has_pending_frame_ = true;
  if (has_listener_ && owner_stub_) {
    owner_stub_->channel()->Send(
        new GpuStreamTextureMsg_FrameAvailable(route_id_));
  }
}

       这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。

       StreamTexture类的成员函数OnFrameAvailable首先将成员变量has_pending_frame_的值设置为true,表示有一个刚刚解码出来的视频帧待处理。

       从前面的分析可以知道,StreamTexture类的成员变量has_listener_已经被设置为true。在这种情况下,如果StreamTexture类的成员变量owner_stub_指向了一个GpuCommandBufferStub对象,那么就说明当前正在处理的StreamTexture对象是为Render进程创建的。这时候就需要向该Render进程发送一个类型为GpuStreamTextureMsg_FrameAvailable的IPC消息,用来通知它MediaPlayer有一个新的解码输出。

       Render进程通过StreamTextureHost类的成员函数OnMessageReceived接收类型为GpuStreamTextureMsg_FrameAvailable的IPC消息,如下所示:

bool StreamTextureHost::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(StreamTextureHost, message)
    IPC_MESSAGE_HANDLER(GpuStreamTextureMsg_FrameAvailable,
                        OnFrameAvailable);
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  DCHECK(handled);
  return handled;
}
       这个函数定义在文件external/chromium_org/content/renderer/gpu/stream_texture_host_android.cc中。

       StreamTextureHost类的成员函数OnMessageReceived将类型为GpuStreamTextureMsg_FrameAvailable的IPC消息分发给另外一个成员函数OnFrameAvailable处理,如下所示:

void StreamTextureHost::OnFrameAvailable() {
  if (listener_)
    listener_->OnFrameAvailable();
}
       这个函数定义在文件external/chromium_org/content/renderer/gpu/stream_texture_host_android.cc中。

       从前面的分析可以知道,StreamTextureHost类的成员变量listener_指向的是一个StreamTextureProxyImpl对象。 StreamTextureHost类的成员函数OnFrameAvailable调用这个StreamTextureProxyImpl对象的成员函数OnFrameAvailable通知它MediaPlayer有一个新的解码输出,如下所示:

void StreamTextureProxyImpl::OnFrameAvailable() {
  base::AutoLock lock(lock_);
  if (client_)
    client_->DidReceiveFrame();
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       从前面的分析可以知道,StreamTextureProxyImpl类的成员变量client_指向的是一个VideoFrameProviderClientImpl对象。StreamTextureProxyImpl类的成员函数OnFrameAvailable调用这个VideoFrameProviderClientImpl对象的成员函数DidReceiveFrame通知它MediaPlayer有一个新的解码输出,如下所示:

void VideoFrameProviderClientImpl::DidReceiveFrame() {
  ......
  if (active_video_layer_)
    active_video_layer_->SetNeedsRedraw();
}
       这个函数定义在文件external/chromium_org/cc/layers/video_frame_provider_client_impl.cc中。

       VideoFrameProviderClientImpl类的成员变量active_video_layer_指向的是一个VideoLayerImpl对象。这个VideoLayerImpl对象描述的就是<video>标签在网页的CC Active Layer Tree中对应的节点。VideoFrameProviderClientImpl类的成员函数DidReceiveFrame调用这个VideoLayerImpl对象的成员函数SetNeedsRedraw请求调度器对CC Active Layer Tree进行重新渲染,以便将MediaPlayer新的解码输出显示出来。

       从前面Chromium硬件加速渲染的UI合成过程分析一文可以知道,当网页的CC Active Layer Tree被渲染时,它里面的每一个Layer的成员函数WillDraw和AppendQuads都会被调用,用来准备要渲染的材料,以便发送给Browser进程进行合成。对于<video>标签来说,它在CC Active Layer Tree中对应的是类型为VideoLayerImpl的Layer。因此,当CC Active Layer Tree被重新渲染时,VideoLayerImpl类的成员函数WillDraw和AppendQuads就会被调用。接下来我们继续分析VideoLayerImpl类的成员函数WillDraw和AppendQuads的实现,以便了解<video>标签的视频画面渲染过程。

       VideoLayerImpl类的成员函数WillDraw的实现如下所示:

bool VideoLayerImpl::WillDraw(DrawMode draw_mode,
                              ResourceProvider* resource_provider) {
  ......

  frame_ = provider_client_impl_->AcquireLockAndCurrentFrame();
  ......

  if (!updater_) {
    updater_.reset(
      %2
作者:Luoshengyang 发表于2016/8/22 0:59:16 原文链接
阅读:37786 评论:11 查看评论

Chromium为视频标签全屏播放的过程分析

$
0
0

       在Chromium中,<video>标签有全屏和非全屏两种播放模式。在非全屏模式下,<video>标签播放的视频嵌入在网页中显示,也就是视频画面作为网页的一部分显示。在全屏模式下,我们是看不到网页其它内容的,因此<video>标签播放的视频可以在一个独立的全屏窗口中显示。这两种截然不同的播放模式,导致Chromium使用不同的方式渲染视频画面。本文接下来就详细分析<video>标签全屏播放的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       从前面Chromium为视频标签<video>渲染视频画面的过程分析一文可以知道,在Android平台上,<video>标签指定的视频是通过系统提供的MediaPlayer进行播放的。MediaPlayer提供了一个setSurface接口,用来给MediaPlayer设置一个Surface。Surface内部有一个GPU缓冲区队列,以后MediaPlayer会将解码出来的视频画面写入到这个队列中去。

       Surface有两种获取方式。第一种方式是通过SurfaceTexture构造一个新的Surface。第二种方式是从SurfaceView内部获得。在非全屏模式下,Chromium就是通过第一种方式构造一个Surface,然后设置给MediaPlayer的。在全屏模式下,Chromium将会直接创建一个全屏的SurfaceView,然后再从这个SurfaceView内部获得一个Surface,并且设置给MediaPlayer。

       在Android平台上,SurfaceView的本质是一个窗口。既然是窗口,那么它的UI就是由系统(SurfaceFlinger)合成在屏幕上显示的。它的UI就来源于它内部的Surface描述的GPU缓冲区队列。因此,当MediaPlayer将解码出来的视频画面写入到SurfaceView内部的Surface描述的GPU缓冲区队列去时,SurfaceFlinger就会从该GPU缓冲区队列中将新写入的视频画面提取出来,并且合成在屏幕上显示。关于SurfaceView的更多知识,可以参考前面Android视图SurfaceView的实现原理分析一文。

       Surface描述的GPU缓冲区队列,是一个生产者/消息者模型。在我们这个情景中,生产者便是MediaPlayer。如果Surface是通过SurfaceTexture构造的,那么SurfaceTexture的所有者,也就是Chromium,就是消费者。消费者有责任将视频画面从GPU缓冲区队列中提取出来,并且进行渲染。渲染完成后,再交给SurfaceFlinger合成显示在屏幕中。如果Surface是从SurfaceView内部获取的,那么SurfaceView就是消费者,然后再交给SurfaceFlinger合成显示在屏幕中。

       简单来说,在非全屏模式下,<video>标签的视频画面经过MediaPlayer->Chromium->SurfaceFlinger显示在屏幕中,而在全屏模式下,经过MediaPlayer->SurfaceView->SurfaceFlinger显示在屏幕中。

       Chromium支持<video>标签在全屏和非全屏模式之间无缝切换,也就是从一个模式切换到另外一个模式的时候,不需要重新创建MediaPlayer,只需要给原先使用的MediaPlayer设置一个新的Surface即可。图1描述的是<video>标签从非全屏模式切换为全屏模式的示意图,如下所示:


图1 <video>标签从非全屏模式切换为全屏模式

      当<video>标签从非全屏模式切换为全屏模式时,Chromium会为它创建一个全屏的SurfaceView,并且将这个SurfaceView内部的Surface设置给MediaPlayer。以后MediaPlayer就不会再将解码出来的视频画面通过原先设置的SurfaceTexture交给Chromium处理,而是通过后面设置的Surface交给SurfaceView处理。

      接下来,我们就结合源代码,从<video>标签进入全屏模式开始,分析<video>标签全屏播放视频的过程。从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,在WebKit中,<video>标签是通过HTMLMediaElement类描述的。当<video>标签进入全屏模式时,HTMLMediaElement类的成员函数enterFullscreen就会被调用,它的实现如下所示:

void HTMLMediaElement::enterFullscreen()
{
    WTF_LOG(Media, "HTMLMediaElement::enterFullscreen");

    FullscreenElementStack::from(document()).requestFullScreenForElement(this, 0, FullscreenElementStack::ExemptIFrameAllowFullScreenRequirement);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       在WebKit中,网页的每一个标签都可以进入全屏模式。每一个网页都对应有一个FullscreenElementStack对象。这个FullscreenElementStack对象内部有一个栈,用来记录它对应的网页有哪些标签进入了全屏模式。

       HTMLMediaElement类的成员函数enterFullscreen首先调用成员函数document获得当前正在处理的<video>标签所属的网页,然后再通过调用FullscreenElementStack类的静态成员函数from获得这个网页所对应的FullscreenElementStack对象。有了这个FullscreenElementStack对象之后,就可以调用它的成员函数requestFullScreenForElement请求将当前正在处理的<video>标签设置为全屏模式。

       FullscreenElementStack类的成员函数requestFullScreenForElement的实现如下所示:

void FullscreenElementStack::requestFullScreenForElement(Element* element, unsigned short flags, FullScreenCheckType checkType)
{
    ......

    // The Mozilla Full Screen API <https://wiki.mozilla.org/Gecko:FullScreenAPI> has different requirements
    // for full screen mode, and do not have the concept of a full screen element stack.
    bool inLegacyMozillaMode = (flags & Element::LEGACY_MOZILLA_REQUEST);

    do {
        ......

        // 1. If any of the following conditions are true, terminate these steps and queue a task to fire
        // an event named fullscreenerror with its bubbles attribute set to true on the context object's
        // node document:
        ......

        // The context object's node document fullscreen element stack is not empty and its top element
        // is not an ancestor of the context object. (NOTE: Ignore this requirement if the request was
        // made via the legacy Mozilla-style API.)
        if (!m_fullScreenElementStack.isEmpty() && !inLegacyMozillaMode) {
            Element* lastElementOnStack = m_fullScreenElementStack.last().get();
            if (lastElementOnStack == element || !lastElementOnStack->contains(element))
                break;
        }

        // A descendant browsing context's document has a non-empty fullscreen element stack.
        bool descendentHasNonEmptyStack = false;
        for (Frame* descendant = document()->frame() ? document()->frame()->tree().traverseNext() : 0; descendant; descendant = descendant->tree().traverseNext()) {
            ......
            if (fullscreenElementFrom(*toLocalFrame(descendant)->document())) {
                descendentHasNonEmptyStack = true;
                break;
            }
        }
        if (descendentHasNonEmptyStack && !inLegacyMozillaMode)
            break;

        ......


        // 2. Let doc be element's node document. (i.e. "this")
        Document* currentDoc = document();

        // 3. Let docs be all doc's ancestor browsing context's documents (if any) and doc.
        Deque<Document*> docs;

        do {
            docs.prepend(currentDoc);
            currentDoc = currentDoc->ownerElement() ? ¤tDoc->ownerElement()->document() : 0;
        } while (currentDoc);

        // 4. For each document in docs, run these substeps:
        Deque<Document*>::iterator current = docs.begin(), following = docs.begin();

        do {
            ++following;

            // 1. Let following document be the document after document in docs, or null if there is no
            // such document.
            Document* currentDoc = *current;
            Document* followingDoc = following != docs.end() ? *following : 0;

            // 2. If following document is null, push context object on document's fullscreen element
            // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
            // set to true on the document.
            if (!followingDoc) {
                from(*currentDoc).pushFullscreenElementStack(element);
                addDocumentToFullScreenChangeEventQueue(currentDoc);
                continue;
            }

            // 3. Otherwise, if document's fullscreen element stack is either empty or its top element
            // is not following document's browsing context container,
            Element* topElement = fullscreenElementFrom(*currentDoc);
            if (!topElement || topElement != followingDoc->ownerElement()) {
                // ...push following document's browsing context container on document's fullscreen element
                // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
                // set to true on document.
                from(*currentDoc).pushFullscreenElementStack(followingDoc->ownerElement());
                addDocumentToFullScreenChangeEventQueue(currentDoc);
                continue;
            }

            // 4. Otherwise, do nothing for this document. It stays the same.
        } while (++current != docs.end());

        // 5. Return, and run the remaining steps asynchronously.
        // 6. Optionally, perform some animation.
        ......
        document()->frameHost()->chrome().client().enterFullScreenForElement(element);

        // 7. Optionally, display a message indicating how the user can exit displaying the context object fullscreen.
        return;
    } while (0);

    ......
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/FullscreenElementStack.cpp中。

       FullscreenElementStack类的成员函数requestFullScreenForElement主要是用来为网页中的每一个Document建立一个Stack。这个Stack记录了Document中所有请求设置为全屏模式的标签。我们通过图2所示的例子说明FullscreenElementStack类的成员函数requestFullScreenForElement的实现:


图2 Fullscreen Stack for Document

       图2所示的网页包含了两个Document:Doc1和Doc2。其中,Doc1通过<iframe>标签加载了Doc2,后者里面有一个<video>标签。当Doc2里面的<video>标签被设置为全屏模式时,Doc1的Stack会被压入一个<iframe>标签和一个<video>标签,其中,<iframe>标签代表的是Doc1,Doc2的Stack会被压入一个<video>标签。

       注释中提到,Mozilla定义的Fullscreen API没有Fullscreen Element Stack的概念。没有Fullscreen Element Stack,意味着网页的标签在任意情况下都可以设置为全屏模式。不过,非Mozilla定义的Fullscreen API是要求Fullscreen Element Stack的。Fullscreen Element Stack用来限制一个标签是否可以设置为全屏模式:

       1. 当Stack为空时,任意标签均可设置为全屏模式。

       2. 当Stack非空时,栈顶标签的子标签才可以设置为全屏模式。

       FullscreenElementStack类的成员函数requestFullScreenForElement就是根据上述逻辑判断参数element描述的标签是否可以设置为全屏模式的。如果可以,那么就会更新与它相关的Stack,并且在最后调用在WebKit Glue层创建一个WebViewImpl对象的成员函数enterFullScreenForElement,用来通知WebKit Glue层有一个标签要进入全屏模式。这个WebViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。WebKit Glue层的作用,可以参考前面Chromium网页加载过程简要介绍和学习计划一文。

       接下来,我们就继续分析WebViewImpl类的成员函数enterFullScreenForElement的实现,如下所示:

void WebViewImpl::enterFullScreenForElement(WebCore::Element* element)
{
    m_fullscreenController->enterFullScreenForElement(element);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       WebViewImpl类的成员变量m_fullscreenController指向的是一个FullscreenController对象。WebViewImpl类的成员函数enterFullScreenForElement调用这个FullscreenController对象的成员函数enterFullScreenForElement,用来通知它将参数element描述的标签设置为全屏模式。

       FullscreenController类的成员函数enterFullScreenForElement的实现如下所示:

void FullscreenController::enterFullScreenForElement(WebCore::Element* element)
{
    // We are already transitioning to fullscreen for a different element.
    if (m_provisionalFullScreenElement) {
        m_provisionalFullScreenElement = element;
        return;
    }

    // We are already in fullscreen mode.
    if (m_fullScreenFrame) {
        m_provisionalFullScreenElement = element;
        willEnterFullScreen();
        didEnterFullScreen();
        return;
    }

    // We need to transition to fullscreen mode.
    if (WebViewClient* client = m_webViewImpl->client()) {
        if (client->enterFullScreen())
            m_provisionalFullScreenElement = element;
    }
}
      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。

      如果参数element描述的标签所在的网页已经有标签处于全屏模式,那么FullscreenController类的成员变量m_provisionalFullScreenElement就会指向该标签,并且FullscreenController类的另外一个成员变量m_fullScreenFrame会指向一个LocalFrame对象。这个LocalFrame对象描述的就是处于全屏模式的标签所在的网页。

      我们假设参数element描述的标签所在的网页还没有标签被设置为全屏模式。这时候FullscreenController类的成员函数enterFullScreenForElement会调用成员变量m_webViewImpl指向的一个WebViewImpl对象的成员函数client获得一个WebViewClient接口,然后再调用这个WebViewClient接口的成员函数enterFullScreen,用来通知它进入全屏模式。

      上述WebViewClient接口是由WebKit的使用者设置的。在我们这个情景中,WebKit的使用者即Render进程中的Content模块,它设置的WebViewClient接口指向的是一个RenderViewImpl对象。这个RenderViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文,它描述的是当前正在处理的网页所加载在的一个RenderView控件。这一步实际上是通知Chromium的Content层进入全屏模式。

       FullscreenController类的成员函数enterFullScreenForElement通知了Content层进入全屏模式之后,会将引发Content层进入全屏模式的标签记录在成员变量m_provisionalFullScreenElement中。在我们这个情景中,这个标签即为一个<video>标签。

       接下来我们继续分析Chromium的Content层进入全屏模式的过程,也就是RenderViewImpl类的成员函数enterFullScreen的实现,如下所示:

bool RenderViewImpl::enterFullScreen() {
  Send(new ViewHostMsg_ToggleFullscreen(routing_id_, true));
  return true;
}
       这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc。

       RenderViewImpl类的成员函数enterFullScreen向Browser进程发送一个类型为ViewHostMsg_ToggleFullscreen的IPC消息,用来通知它将Routing ID为routing_id_的网页所加载在的Tab设置为全屏模式。

       Browser进程通过RenderViewHostImpl类的成员函数OnMessageReceived接收类型为ViewHostMsg_ToggleFullscreen的IPC消息,如下所示:

bool RenderViewHostImpl::OnMessageReceived(const IPC::Message& msg) {
  ......

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RenderViewHostImpl, msg)
    ......
    IPC_MESSAGE_HANDLER(ViewHostMsg_ToggleFullscreen, OnToggleFullscreen)
    ......
    IPC_MESSAGE_UNHANDLED(
        handled = RenderWidgetHostImpl::OnMessageReceived(msg))
  IPC_END_MESSAGE_MAP()

  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_view_host_impl.cc中。

       RenderViewHostImpl类的成员函数OnMessageReceived将类型为ViewHostMsg_ToggleFullscreen的IPC消息分发给另外一个成员函数OnToggleFullscreen处理,如下所示:

void RenderViewHostImpl::OnToggleFullscreen(bool enter_fullscreen) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  delegate_->ToggleFullscreenMode(enter_fullscreen);
  // We need to notify the contents that its fullscreen state has changed. This
  // is done as part of the resize message.
  WasResized();
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_view_host_impl.cc中。

       RenderViewHostImpl类的成员函数OnToggleFullscreen首先通过成员变量delegate_描述的一个RenderViewHostDelegate委托接口将浏览器窗口设置为全屏模式,然后再调用另外一个成员函数WasResized通知Render进程,浏览器窗口大小已经发生了变化,也就是它进入了全屏模式。

       RenderViewHostImpl类的成员变量delegate_描述的RenderViewHostDelegate委托接口指向的是一个WebContentsImpl对象。这个WebContentsImpl对象是Content层提供给外界的一个接口,用来描述当前正在加载的一个网页。外界通过这个接口就可以访问当前正在加载的网页。

       RenderViewHostImpl类的成员函数WasResized是从父类RenderWidgetHostImpl继承下来的,它的实现如下所示:

void RenderWidgetHostImpl::WasResized() {
  ......

  is_fullscreen_ = IsFullscreen();
  ......

  ViewMsg_Resize_Params params;
  ......
  params.is_fullscreen = is_fullscreen_;
  if (!Send(new ViewMsg_Resize(routing_id_, params))) {
    resize_ack_pending_ = false;
  } else {
    last_requested_size_ = new_size;
  }
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_impl.cc中。

       RenderWidgetHostImpl类的成员函数WasResized主要是向Render进程发送一个类型为ViewMsg_Resize的IPC消息。这个ViewMsg_Resize的IPC消息。携带了一个is_fullscreen信息,用来告知Render进程当前正在处理的网页是否已经进入了全屏模式。

       Render进程通过RenderWidget类的成员函数OnMessageReceived接收类型为ViewMsg_Resize的IPC消息,如下所示:

bool RenderWidget::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RenderWidget, message)
    ......
    IPC_MESSAGE_HANDLER(ViewMsg_Resize, OnResize)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
      这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

      RenderWidget类的成员函数OnMessageReceived将类型为ViewMsg_Resize的IPC消息分发给另外一个成员函数OnResize处理,如下所示:

void RenderWidget::OnResize(const ViewMsg_Resize_Params& params) {
  ......

  Resize(params.new_size, params.physical_backing_size,
         params.overdraw_bottom_height, params.visible_viewport_size,
         params.resizer_rect, params.is_fullscreen, SEND_RESIZE_ACK);

  ......
}
      这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

      RenderWidget类的成员函数OnResize又调用另外一个成员函数Resize处理类型为ViewMsg_Resize的IPC消息,如下所示:

void RenderWidget::Resize(const gfx::Size& new_size,
                          const gfx::Size& physical_backing_size,
                          float overdraw_bottom_height,
                          const gfx::Size& visible_viewport_size,
                          const gfx::Rect& resizer_rect,
                          bool is_fullscreen,
                          ResizeAck resize_ack) {
  ......

  if (compositor_) {
    compositor_->setViewportSize(new_size, physical_backing_size);
    ......
  }

  ......

  // NOTE: We may have entered fullscreen mode without changing our size.
  bool fullscreen_change = is_fullscreen_ != is_fullscreen;
  if (fullscreen_change)
    WillToggleFullscreen();
  is_fullscreen_ = is_fullscreen;

  if (size_ != new_size) {
    size_ = new_size;

    // When resizing, we want to wait to paint before ACK'ing the resize.  This
    // ensures that we only resize as fast as we can paint.  We only need to
    // send an ACK if we are resized to a non-empty rect.
    webwidget_->resize(new_size);
  } 

  ......

  if (fullscreen_change)
    DidToggleFullscreen();

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

       RenderWidget类的成员函数Resize一方面会通知网页的UI合成器,它负责渲染的网页的大小发生了变化,以便它修改网页UI的Viewport大小。这是通过调用成员变量compositor_指向的一个RenderWidgetCompositor对象的成员函数setViewportSize实现的。这个RenderWidgetCompositor对象的创建过程可以参考前面Chromium网页Layer Tree创建过程分析一文。

       另一方面,RenderWidget类的成员函数Resize又会通知WebKit,它当前正在加载网页的大小发生了变化。这是通过调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数resize实现的。这个WebViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。

       此外,RenderWidget类的成员函数Resize还会判断网页是否从全屏模式退出,或者进入全屏模式。如果是的话,那么就在通知WebKit修改网页的大小前后,RenderWidget类的成员函数Resize还会分别调用另外两个成员函数WillToggleFullscreen和DidToggleFullscreen,用来通知WebKit网页进入或者退出了全屏模式。

       在我们这个情景中,网页是进入了全屏模式。接下来我们就先分析RenderWidget类的成员函数WillToggleFullscreen的实现,然后再分析另外一个成员函数DidToggleFullscreen的实现。

       RenderWidget类的成员函数WillToggleFullscreen的实现如下所示:

void RenderWidget::WillToggleFullscreen() {
  ......

  if (is_fullscreen_) {
    webwidget_->willExitFullScreen();
  } else {
    webwidget_->willEnterFullScreen();
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

       从前面的分析可以知道,RenderWidget类的成员函数WillToggleFullscreen是在通知WebKit进入全屏模式之前被调用的。这时候RenderWidget类的成员变量is_fullscreen_的值为false。因此,接下来RenderWidget类的成员函数WillToggleFullscreen会调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数willEnterFullScreen,用来通知WebKit它即将要进入全屏模式。

       WebViewImpl类的成员函数willEnterFullScreen的实现如下所示:

void WebViewImpl::willEnterFullScreen()
{
    m_fullscreenController->willEnterFullScreen();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       WebViewImpl类的成员函数willEnterFullScreen调用成员变量m_fullscreenController指向的一个FullscreenController对象的成员函数willEnterFullScreen,用来通知WebKit即将要进入全屏模式,如下所示:

void FullscreenController::willEnterFullScreen()
{
    if (!m_provisionalFullScreenElement)
        return;

    // Ensure that this element's document is still attached.
    Document& doc = m_provisionalFullScreenElement->document();
    if (doc.frame()) {
        ......
        m_fullScreenFrame = doc.frame();
    }
    m_provisionalFullScreenElement.clear();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。

       从前面的分析可以知道,此时FullscreenController类的成员变量m_provisionalFullScreenElement的值不等于NULL,它指向了一个<video>标签。FullscreenController类的成员函数willEnterFullScreen找到这个<video>标签所在的Document,并且获得与这个Document关联的一个LocalFrame对象,保存在成员变量m_fullScreenFrame中,表示当前处于全屏模式的网页。

       这一步执行完成后,回到前面分析的RenderWidget类的成员函数Resize,它在通知了WebKit修改当前正在加载的网页的大小之后,会调用另外一个成员函数DidToggleFullscreen,用来通知WebKit已经进入了全屏模式,如下所示:

void RenderWidget::DidToggleFullscreen() {
  ......

  if (is_fullscreen_) {
    webwidget_->didEnterFullScreen();
  } else {
    webwidget_->didExitFullScreen();
  }
}
      这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

      从前面的分析可以知道,此时RenderWidget类的成员变量is_fullscreen_的值已经被设置为true。因此,RenderWidget类的成员函数DidToggleFullscreen接下来就会调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数didEnterFullScreen,用来通知WebKit它已经进入全屏模式,如下所示:

void WebViewImpl::didEnterFullScreen()
{
    m_fullscreenController->didEnterFullScreen();
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       WebViewImpl类的成员函数didEnterFullScreen调用成员变量m_fullscreenController指向的一个FullscreenController对象的成员函数didEnterFullScreen,用来通知WebKit已经进入全屏模式,如下所示:

void FullscreenController::didEnterFullScreen()
{
    if (!m_fullScreenFrame)
        return;

    if (Document* doc = m_fullScreenFrame->document()) {
        if (FullscreenElementStack::isFullScreen(*doc)) {
            ......

            if (RuntimeEnabledFeatures::overlayFullscreenVideoEnabled()) {
                Element* element = FullscreenElementStack::currentFullScreenElementFrom(*doc);
                ASSERT(element);
                if (isHTMLMediaElement(*element)) {
                    HTMLMediaElement* mediaElement = toHTMLMediaElement(element);
                    if (mediaElement->webMediaPlayer() && mediaElement->webMediaPlayer()->canEnterFullscreen()
                        // FIXME: There is no embedder-side handling in layout test mode.
                        && !isRunningLayoutTest()) {
                        mediaElement->webMediaPlayer()->enterFullscreen();
                    }
                    .......
                }
            }
        }
    }
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。

       从前面的分析可以知道,FullscreenController类的成员变量m_fullScreenFrame的值不等于NULL。它指向了一个LocalFrame对象。这个LocalFrame对象描述的就是当前被设置为全屏模式的标签所在的网页。通过这个LocalFrame对象可以获得一个Document对象。

       有了这个Document对象之后,FullscreenController类的成员函数didEnterFullScreen就会检查它的Fullscreen Element Stack栈顶标签。如果这是一个<video>标签,并且已经为这个<video>标签创建过MediaPlayer接口,以及这个MediaPlayer接口允许进入全屏模式,那么FullscreenController类的成员函数didEnterFullScreen就会调用这个MediaPlayer接口的成员函数enterFullScreen,让其进入全屏模式。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,WebKit为<video>标签创建的MediaPlayer接口指向的是一个WebMediaPlayerAndroid对象。因此,FullscreenController类的成员函数didEnterFullScreen调用的是WebMediaPlayerAndroid类的成员函数enenterFullScreen让<video>标签的播放器进入全屏模式。

       WebMediaPlayerAndroid类的成员函数enenterFullScreen的实现如下所示:

void WebMediaPlayerAndroid::enterFullscreen() {
  if (player_manager_->CanEnterFullscreen(frame_)) {
    player_manager_->EnterFullscreen(player_id_, frame_);
    ......
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,WebMediaPlayerAndroid类的成员变量player_manager_指向的是一个RendererMediaPlayerManager对象。这个RendererMediaPlayerManager对象负责管理为当前正在处理的网页中的所有<video>标签创建的播放器实例。

       WebMediaPlayerAndroid类的成员函数enenterFullScreen首先调用上述RendererMediaPlayerManager对象的成员函数CanEnterFullscreen检查与当前正在处理的播放器实例关联的<video>标签所在的网页是否已经进入了全屏模式。如果已经进入,那么就会继续调用RendererMediaPlayerManager对象的成员函数EnterFullscreen使得当前正在处理的播放器实例进入全屏模式。

       从前面的分析可以知道,与当前正在处理的播放器实例关联的<video>标签所在的网页已经进入了全屏模式。因此,接下来RendererMediaPlayerManager类的成员函数EnterFullscreen就会被调用,如下所示:

void RendererMediaPlayerManager::EnterFullscreen(int player_id,
                                                 blink::WebFrame* frame) {
  pending_fullscreen_frame_ = frame;
  Send(new MediaPlayerHostMsg_EnterFullscreen(routing_id(), player_id));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数EnterFullscreen主要是向Browser进程发送一个类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,通知它将ID为player_id的播放器设置为全屏模式。

       Browser进程通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,如下所示:

bool MediaWebContentsObserver::OnMediaPlayerMessageReceived(
    const IPC::Message& msg,
    RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg)
    IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_EnterFullscreen,
                        GetMediaPlayerManager(render_frame_host),
                        BrowserMediaPlayerManager::OnEnterFullscreen)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。

       MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived首先调用成员函数GetMediaPlayerManager获得一个BrowserMediaPlayerManager对象,然后调用这个BrowserMediaPlayerManager对象的成员函数OnEnterFullscreen处理类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,如下所示:

void BrowserMediaPlayerManager::OnEnterFullscreen(int player_id) {
  ......

  if (video_view_.get()) {
    fullscreen_player_id_ = player_id;
    video_view_->OpenVideo();
    return;
  } else if (!ContentVideoView::GetInstance()) {
    ......

    video_view_.reset(new ContentVideoView(this));
    base::android::ScopedJavaLocalRef<jobject> j_content_video_view =
        video_view_->GetJavaObject(base::android::AttachCurrentThread());
    if (!j_content_video_view.is_null()) {
      fullscreen_player_id_ = player_id;
      return;
    }
  }

  .....
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       当BrowserMediaPlayerManager类的成员变量video_view_的值不等于NULL时,它指向一个ContentVideoView对象。这个ContentVideoView对象描述的是一个全屏播放器窗口。

       在Browser进程中,一个网页对应有一个BrowserMediaPlayerManager对象。同一个网页中的所有<video>标签在设置为全屏模式时,使用的是同一个全屏播放器窗口。不同的网页的<video>标签在设置为全屏模式时,使用不同的全屏播放器窗口。在同一时刻,Browser进程只能存在一个全屏播放器窗口。

       BrowserMediaPlayerManager类的成员函数OnEnterFullscreen的执行逻辑如下所示:

       1. 如果当前正在处理的BrowserMediaPlayerManager对象的成员变量video_view_的值不等于NULL,也就是它指向了一个ContentVideoView对象,那么就说明Browser进程已经为当前正在处理的BrowserMediaPlayerManager对象创建过全屏播放器窗口了。这时候就会将参数player_id的值保存在另外一个成员变量fullscreen_player_id_中,表示当前正在处理的BrowserMediaPlayerManager对象所管理的全屏播放器窗口正在被ID为player_id的播放器使用。同时,会调用上述ContentVideoView对象的成员函数OpenVideo,让其全屏播放当前被设置为全屏模式的<video>标签的视频。

       2. 如果当前正在处理的BrowserMediaPlayerManager对象的成员变量video_view_的值NULL,并且Browser进程当前没有为其它网页的<video>标签全屏播放视频(这时候调用ContentVideoView类的静态成员函数GetInstance获得的返回值为NULL),那么就会为当前正在处理的BrowserMediaPlayerManager对象创建一个全屏播放窗口,也就是创建一个ContentVideoView对象(这时候调用ContentVideoView类的静态成员函数GetInstance将会返回该ContentVideoView对象),并且保存在其成员变量video_view_中。

       C++层ContentVideoView对象在创建的过程中,又会在Java层创建一个对应的ContentVideoView对象。这个Java层ContentVideoView对象如果能成功创建,那么就可以通过调用其对应的C++层ContentVideoView对象的成员函数GetJavaObject获得。这时候说明Browser进程成功创建了一个全屏播放器窗口。这个全屏播放器窗口实际上就是一个SurfaceView对象。

       一个SurfaceView对象刚创建出来的时候,它描述的窗口没有创建出来。Browser进程需要等它描述的窗口创建出来之后,才使用它全屏播放当前被设置为全屏模式的<video>标签的视频。因此,BrowserMediaPlayerManager类的成员函数OnEnterFullscreen在创建了一个SurfaceView对象之后,只做了一件简单的事情,就是记录当前是哪一个播放器进入了全屏模式,即将参数player_id的值保存在当前正在处理的BrowserMediaPlayerManager对象的成员变量fullscreen_player_id_中。

       接下来,我们就继续分析全屏播放器窗口的创建过程,以及Browser进程使用它来全屏播放当前设置为全屏模式的<video>标签的视频的过程。

       全屏播放器窗口是在C++层ContentVideoView对象的创建过程当中创建出来的,因此,我们就从C++层ContentVideoView类的构造函数开始,分析全屏播放器窗口的创建过程,如下所示:

ContentVideoView::ContentVideoView(
    BrowserMediaPlayerManager* manager)
    : manager_(manager),
      weak_factory_(this) {
  DCHECK(!g_content_video_view);
  j_content_video_view_ = CreateJavaObject();
  g_content_video_view = this;
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       C++层ContentVideoView类的构造函数会调用另外一个成员函数CreateJavaObject在Java层创建一个对应的ContentVideoView对象,并且保存在成员变量j_content_video_view_中。

       与此同时,C++层ContentVideoView类的构造函数会将当前正在创建的ContentVideoView对象保存一个静态成员变量g_content_video_view_中,表示Browser进程当前已经存在一个全屏窗口。根据我们前面的分析,这样就会阻止其它网页全屏播放它们的<video>标签的视频。这个静态成员变量g_content_video_view_的值可以通过调用前面提到的C++层ContentVideoView类的静态成员函数GetInstance获得,如下所示:

ContentVideoView* ContentVideoView::GetInstance() {
  return g_content_video_view;
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       回到C++层的ContentVideoView类的构造函数中,我们主要关注Java层的ContentVideoView对象的创建过程,因此接下来我们继续分析C++层的ContentVideoView类的成员函数CreateJavaObject的实现,如下所示:

JavaObjectWeakGlobalRef ContentVideoView::CreateJavaObject() {
  ContentViewCoreImpl* content_view_core = manager_->GetContentViewCore();
  JNIEnv* env = AttachCurrentThread();
  bool legacyMode = CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kDisableOverlayFullscreenVideoSubtitle);
  return JavaObjectWeakGlobalRef(
      env,
      Java_ContentVideoView_createContentVideoView(
          env,
          content_view_core->GetContext().obj(),
          reinterpret_cast<intptr_t>(this),
          content_view_core->GetContentVideoViewClient().obj(),
          legacyMode).obj());
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       C++层的ContentVideoView类的成员函数CreateJavaObject主要是通过JNI接口Java_ContentVideoView_createContentVideoView调用Java层的ContentVideoView类的静态成员函数createContentVideoView创建一个Java层的ContentVideoView对象,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    private static ContentVideoView createContentVideoView(
            Context context, long nativeContentVideoView, ContentVideoViewClient client,
            boolean legacy) {
        ......

        ContentVideoView videoView = null;
        if (legacy) {
            videoView = new ContentVideoViewLegacy(context, nativeContentVideoView, client);
        } else {
            videoView = new ContentVideoView(context, nativeContentVideoView, client);
        }

        if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
            return videoView;
        }
        return null;
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       从前面的调用过程可以知道,当Browser进程设置了kDisableOverlayFullscreenVideoSubtitle(disable-overlay-fullscreen-video-subtitle)启动选项时,参数legacy的值就会等于true时,表示禁止在全屏播放器窗口上面显示播放控制控件以及字幕。这时候全屏播放器窗口通过一个ContentVideoViewLegacy对象描述。另一方面,如果参数legacy的值等于false,那么全屏播放器窗口将会通过一个ContentVideoView对象描述。

       一个ContentVideoViewLegacy对象或者一个ContentVideoView对象描述的全屏播放器窗口实际上是一个SurfaceView。这个SurfaceView只有添加到浏览器窗口之后,才能显示在屏幕中。为了将该全屏播放器窗口显示出来,ContentVideoView类的静态成员函数createContentVideoView将会通过调用前面创建的ContentVideoViewLegacy对象或者ContentVideoView对象的成员函数getContentVideoViewClient获得一个ContentVideoViewClient接口。有了这个ContentVideoViewClient接口之后,就可以调用它的成员函数onShowCustomView将刚才创建出来的全屏播放器窗口显示出来了。

       我们假设参数legacy的值等于false。这意味着全屏播放器窗口是通过一个ContentVideoView对象描述的。接下来我们就继续分析这个ContentVideoView对象的创建过程,即ContentVideoView类的构造函数的实现,以便了解它内部的SurfaceView的创建过程,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    // This view will contain the video.
    private VideoSurfaceView mVideoSurfaceView;
    ......

    protected ContentVideoView(Context context, long nativeContentVideoView,
            ContentVideoViewClient client) {
        super(context);
        ......
        mVideoSurfaceView = new VideoSurfaceView(context);
        showContentVideoView();
        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的构造函数会创建一个VideoSurfaceView对象,并且保存在成员变量mVideoSurfaceView中。

       VideoSurfaceView类是从SurfaceView类继承下来的,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    private class VideoSurfaceView extends SurfaceView {
        ......
    }

    ......
}
       这个类定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的构造函数在创建了一个VideoSurfaceView对象,接下来会调用另外一个成员函数showContentVideoView将它作为当前正在创建的ContentVideoView对象的子View,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    protected void showContentVideoView() {
        mVideoSurfaceView.getHolder().addCallback(this);
        this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER));

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的成员函数showContentVideoView将前面创建的VideoSurfaceView对象作为当前正在创建的ContentVideoView对象的子View之外,还会使用后者监听前者的surfaceCreated事件,也就是它描述的窗口创建完成事件。

       当前正在创建的ContentVideoView对象实现了SurfaceHolder.Callback接口,一旦前面创建的VideoSurfaceView对象发出surfaceCreated事件通知,那么前者的成员函数surfaceCreated就会被调用,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mSurfaceHolder = holder;
        openVideo();
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的成员函数surfaceCreated会将参数holder描述的一个SurfaceHolder对象保存在成员变量mSurfaceHolder中。后面可以通过这个SurfaceHolder对象获得前面创建的VideoSurfaceView对象内部的一个Surface。这个Surface将会用来接收MediaPlayer的解码输出。

       接下来,ContentVideoView类的成员函数surfaceCreated就会调用另外一个成员函数openVideo将上述Surface设置为MediaPlayer的解码输出,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    protected void openVideo() {
        if (mSurfaceHolder != null) {
            mCurrentState = STATE_IDLE;
            if (mNativeContentVideoView != 0) {
                nativeRequestMediaMetadata(mNativeContentVideoView);
                nativeSetSurface(mNativeContentVideoView,
                        mSurfaceHolder.getSurface());
            }
        }
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的成员变量mNativeContentVideoView描述的是前面在C++层创建的一个ContentVideoView对象。ContentVideoView类的成员函数openVideo主要是做两件事情:

       1. 获取要播放的视频的元数据,以便用来初始化全屏播放器窗口。

       2. 将前面创建的VideoSurfaceView对象内部维护的Surface设置为MediaPlayer的解码输出。

       这两件事情分别是通过调用成员函数nativeRequestMediaMetadata和nativeSetSurface完成的。当它们完成之后,<video>标签的视频可以开始全屏播放了。接下来我们继续分析它们的实现。

       ContentVideoView类的成员函数nativeRequestMediaMetadata是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata实现,如下所示:

void
    Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeContentVideoView) {
  ContentVideoView* native =
      reinterpret_cast<ContentVideoView*>(nativeContentVideoView);
  CHECK_NATIVE_PTR(env, jcaller, native, "RequestMediaMetadata");
  return native->RequestMediaMetadata(env, jcaller);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentVideoView_jni.h中。 

       从前面的调用过程可以知道,参数nativeContentVideoView描述的是一个C++层ContentVideoView对象,函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata调用它的成员函数RequestMediaMetadata获取要播放的视频的元数据,如下所示:

void ContentVideoView::RequestMediaMetadata(JNIEnv* env, jobject obj) {
  base::MessageLoop::current()->PostTask(
      FROM_HERE,
      base::Bind(&ContentVideoView::UpdateMediaMetadata,
                 weak_factory_.GetWeakPtr()));
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       ContentVideoView类的成员函数RequestMediaMetadata向当前线程的消息队列发送一个Task。这个Task绑定了ContentVideoView类的另外一个成员函数UpdateMediaMetadata。这意味着接下来ContentVideoView类的成员函数UpdateMediaMetadata会在当前线程中调用,用来获取要播放的视频的元数据。

       ContentVideoView类的成员函数UpdateMediaMetadata的实现如下所示:

void ContentVideoView::UpdateMediaMetadata() {
  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> content_video_view = GetJavaObject(env);
  if (content_video_view.is_null())
    return;

  media::MediaPlayerAndroid* player = manager_->GetFullscreenPlayer();
  if (player && player->IsPlayerReady()) {
    Java_ContentVideoView_onUpdateMediaMetadata(
        env, content_video_view.obj(), player->GetVideoWidth(),
        player->GetVideoHeight(),
        static_cast<int>(player->GetDuration().InMilliseconds()),
        player->CanPause(),player->CanSeekForward(), player->CanSeekBackward());
  }
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       ContentVideoView类的成员函数UpdateMediaMetadata首先调用另外一个成员函数GetJavaObject获得与当前正在处理的C++层ContentVideoView对象对应的Java层ContentVideoView对象。

       ContentVideoView类的成员函数UpdateMediaMetadata接下来又通过调用成员变量manager_指向的一个BrowserMediaPlayerManager对象的成员函数GetFullscreenPlayer获得当前处于全屏模式的播放器,如下所示:

MediaPlayerAndroid* BrowserMediaPlayerManager::GetFullscreenPlayer() {
  return GetPlayer(fullscreen_player_id_);
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       从前面的分析可以知道,BrowserMediaPlayerManager类的成员变量fullscreen_player_id_记录了当前处于全屏模式的播放器的ID。有了这个ID之后,就可以调用另外一个成员函数GetPlayer获得一个对应的MediaPlayerBridge对象。这个MediaPlayerBridge对象描述的就是当前处于全屏模式的播放器。

       回到ContentVideoView类的成员函数UpdateMediaMetadata中,它获得的全屏播放器之后,实际上就是<video>标签进入全屏模式之前所使用的那个播放器。这意味着<video>标签在全屏和非全屏模式下,使用的是同一个播放器,区别只在于播放器在两种模式下使用的UI不一样。这个播放器之前已经获得了要播放的视频的元数据,并且保存在了内部,因此这里就不需要从网络上重新获取。

       有了要播放的视频的元数据之后,ContentVideoView类的成员函数UpdateMediaMetadata就通过JNI接口Java_ContentVideoView_onUpdateMediaMetadata调用前面获得的Java层ContentVideoView对象的成员函数onUpdateMediaMetadata,让其初始化全屏播放器窗口,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    protected void onUpdateMediaMetadata(
            int videoWidth,
            int videoHeight,
            int duration,
            boolean canPause,
            boolean canSeekBack,
            boolean canSeekForward) {
        mDuration = duration;
        .....
        onVideoSizeChanged(videoWidth, videoHeight);
    }

    ......
}

      这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。 

      参数duration描述的就是要播放的视频的总时长,它会被记录在ContentVideoView类的成员变量mDuration中。

      另外两个参数videoWidth和videoHeight描述的是要播放的视频的宽和高,ContentVideoView类的成员函数onUpdateMediaMetadata将它们传递给另外一个成员函数onVideoSizeChanged,用来初始化全屏播放器窗口,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    private void onVideoSizeChanged(int width, int height) {
        mVideoWidth = width;
        mVideoHeight = height;
        // This will trigger the SurfaceView.onMeasure() call.
        mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。 

       ContentVideoView类的成员函数onVideoSizeChanged将视频的宽度和高度分别记录在成员变量mVideoWidth和mVideoHeight中,然后再将它们设置为用来显示视频画面的VideoSurfaceView的大小。

       这一步执行完成后,全屏播放器窗口就初始化完毕。回到前面分析的ContentVideoView类的成员函数openVideo中,它接下来将上述VideoSurfaceView内部使用的Surface设置为MediaPlayer的解码输出。这是通过调用ContentVideoView类的成员函数nativeSetSurface实现的。

       ContentVideoView类的成员函数nativeSetSurface是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface实现,如下所示:

void
    Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeContentVideoView,
    jobject surface) {
  ContentVideoView* native =
      reinterpret_cast<ContentVideoView*>(nativeContentVideoView);
  CHECK_NATIVE_PTR(env, jcaller, native, "SetSurface");
  return native->SetSurface(env, jcaller, surface);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentVideoView_jni.h中。

       参数nativeContentVideoView描述的是一个C++层ContentVideoView对象,另外一个参数surface描述的就是前面提到的用来显示视频画面的VideoSurfaceView内部使用的Surface。

       函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface参数nativeContentVideoView描述的C++层ContentVideoView对象的成员函数SetSurface重新给当前处于全屏模式的播放器设置一个Surface,如下所示:

void ContentVideoView::SetSurface(JNIEnv* env, jobject obj,
                                  jobject surface) {
  manager_->SetVideoSurface(
      gfx::ScopedJavaSurface::AcquireExternalSurface(surface));
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       ContentVideoView类的成员函数SetSurface调用成员变量manager_指向的一个BrowserMediaPlayerManager对象的成员函数SetVideoSurface重新给当前处于全屏模式的播放器设置一个Surface,如下所示:

void BrowserMediaPlayerManager::SetVideoSurface(
    gfx::ScopedJavaSurface surface) {
  MediaPlayerAndroid* player = GetFullscreenPlayer();
  ......

  player->SetVideoSurface(surface.Pass());
  
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数SetVideoSurface首先调用另外一个成员函数GetFullscreenPlayer获得一个MediaPlayerBridge对象。这个MediaPlayerBridge对象被一个MediaPlayerAndroid指针引用(MediaPlayerAndroid是MediaPlayerBridge的父类),它描述的便是当前处于全屏模式的播放器。

       获得了当前处于全屏模式的播放器之后,就可以调用它的成员函数SetVideoSurface将参数surface描述的一个Surface设置为它的解码输出。这个设置过程,也就是MediaPlayerBridge类的成员函数SetVideoSurface的实现,可以参考前面Chromium为视频标签<video>渲染视频画面的过程分析一文。它实际上就是给在Java层创建的一个MediaPlayer重新设置一个Surface作为解码输出。这个Surface是由一个VideoSurfaceView提供的。这个VideoSurfaceView实际上就是一个SurfaceView。SurfaceView会自己将MediaPlayer的解码输出交给系统渲染。因此就不再需要Chromium参与这个渲染过程。

       回忆<video>标签在进入全屏模式之前,Chromium会为它会创建一个纹理,然后用这个纹理创建一个SurfaceTexture。这个SurfaceTexture最终又会封装在一个Surface中。这个Surface就设置为MediaPlayer的解码输出。这时候MediaPlayer的解码输出需要由Chromium来处理,也就是渲染在浏览器窗口中。当浏览器窗口被系统渲染在屏幕上时,我们就可以看到MediaPlayer输出的视频画面了。这个过程可以参考前面Chromium为视频标签<video>渲染视频画面的过程分析一文。

       这样,我们就分析完成<video>标签全屏播放视频的过程了。从这个分析过程我们就可以知道,<video>标签在全屏模式和非全屏模式下使用的都是相同的MediaPlayer,区别只在于这个MediaPlayer将视频画面渲染在不同的Surface之上。因此,<video>标签可以在全屏模式和非全屏模式之间进行无缝的播放切换。

       至此,我们也分析完成了video标签在Chromium中的实现。视频在互联网中将扮演着越来越重要的角色。以前浏览器主要是通过Flash插件来支持视频播放。Flash插件有着臭名昭著的安全问题和Crash问题。因此,随着HTML5的出现,浏览器逐步转向使用<video>标签来支持视频播放。这不仅在产品上带来更好的体验(无需用户安装插件),而且在技术上也更加稳定。基于上述理由,理解<video>标签的实现原理就显得尤为重要。重新学习可以参考前面Chromium视频标签<video>简要介绍和学习计划一文。更多信息,可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/8/29 1:00:17 原文链接
阅读:38628 评论:15 查看评论

Chromium扩展(Extension)机制简要介绍和学习计划

$
0
0

       Chromium提供了一种Extension机制,用来增强浏览器功能。我们可以将Extension看作是一种运行在Chromium中的应用。这种应用的开发语言是JavaScript,并且UI通过HTML描述。通过使用Chromium提供的API,Extension可以访问网络,修改浏览器行为,以及操作网页的内容等。本文接下来对Chromium的Extension机制进行简要介绍,以及制定学习计划。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       在Chrome(基于Chromium实现,以下我们将交替使用Chrome和Chromium)的地址栏中输入"chrome://extensions",就可以看到当前安装的所有Extension,如图1所示:


图1 Chrome中的Extension列表

       图1显示了两个Extension。一个是基于Browser Action实现的,另一个是基于Page Action实现的。它们在地址栏的右边分别对应有一个Button。点击这两个Button,可以弹出一个窗口。这一点后面我们会看到。

       接下来,我们就通过上述两个Extension例子,对Chromium的Extension机制进行介绍。

       每一个Extension都包含有一个清单文件manifest.json,类似于Android应用程序的AndroidManifest.xml文件。前者是json格式的,后者是xml格式的。清单文件描述了Extension的内容。以图1所示的Browser action example为例,它的manifest.json如下所示:

{
  "manifest_version": 2,

  "name": "Browser action example",
  "description": "This extension show a image and changes a web page's background",
  "version": "1.0",

  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },

  "content_scripts": [
    {
      "matches": ["http://*/*"],
      "js": ["content.js"],
      "run_at": "document_start",
      "all_frames": true
    }
  ]
}
       前面几行是一些描述性信息。后面的browser_action和content_scripts指定了一个Browser Action和一个Content Script。

       Browser Action对在浏览器中加载的所有网页都生效。后面我们可以看到,Extension还有一种Page Action,它针对特定的网页生效。一个Extension最多可以有一个Browser Action或者Page Action。不管是Browser Action,还是Page Action,都可以指定一个icon文件和一个popup文件。前者是一个图片,后者是一个html文件。指定的icon将会以Button的形式展现在地址栏的右边。当这个Button被点击的时候,就会弹出一个窗口,窗口会加载在清单文件中指定的popup.html文件。注意,这里指定的文件路径都是相对路径,相对Extension的根目录的。

       上述popup.html的内容如下所示:

<html>
  <head>
    <title>Getting Started Extension's Popup</title>
    <style>
      body {
        font-family: "Segoe UI", "Lucida Grande", Tahoma, sans-serif;
        font-size: 100%;
      }
      #status {
        /* avoid an excessively wide status text */
        white-space: pre;
        text-overflow: ellipsis;
        overflow: hidden;
        max-width: 400px;
      }
    </style>

    <!--
      - JavaScript and HTML must be in separate files: see our Content Security
      - Policy documentation[1] for details and explanation.
      -
      - [1]: https://developer.chrome.com/extensions/contentSecurityPolicy
     -->
    <script src="popup.js"></script>
  </head>
  <body>
    <div id="status"></div>
    <img id="image-result" hidden>
  </body>
</html>
      这个html文件的内容很简单,由以下内容组成:

      1. 一个popup.js脚本

      2. 一个div标签

      3. 一个img标签

      其中,div标签用来显示状态信息,img标签用来显示图片。它们的内容都是popup.js指定的,如下所示:

function getImageUrl(callback, errorCallback) {
  callback("https://images-cn-8.ssl-images-amazon.com/images/I/61vnPRDVoeL.jpg", 200, 250);
}

function renderStatus(statusText) {
  document.getElementById('status').textContent = statusText;
}

document.addEventListener('DOMContentLoaded', function() {
    getImageUrl(function(imageUrl, width, height) {
      var imageResult = document.getElementById('image-result');
      imageResult.width = width;
      imageResult.height = height;
      imageResult.src = imageUrl;
      imageResult.hidden = false;

    }, function(errorMessage) {
      renderStatus('Cannot display image. ' + errorMessage);
    });
});

       这个popup.js所做的事情非常简单,就是为popup.html中的img标签指定一个src。安装了这个Entension之后,就可以在浏览器地址栏的右边出现一个Button。点击这个Button,就可以弹出一个窗口,它的内容如图2所示:

 

图2 Browser Action的popup.html

       Browser action example的清单文件还指定了一个Content Script,即content.js。这个content.js通过matches字段指定对任何网页生效。这里所说的生效,是指将content.js注入到网页中去执行的。

       上述content.js的内容如下所示:

document.body.style.backgroundColor="red"
       它做了一件非常简单的事情,就是将网页的背景设置为红色。这件事情虽然非常简单,但是它告诉了我们,Extension通过Content Script可以操作在Chromium中加载的任何一个网页的内容!

       我们再来看另一个Page action example。它的清单文件如下所示:

{
  "manifest_version": 2,

  "name": "Page action example",
  "description": "This extension show a image and changes a web page's background",
  "version": "1.0",

  "background": {
    "scripts": ["background.js"]
  },

  "page_action": {
    "default_icon": "icon.png", 
    "default_popup": "popup.html" 
  },

  "permissions": [
     "tabs"
   ],

  "content_scripts": [
    {
      "matches": ["https://fast.com/"],
      "js": ["content.js"],
      "run_at": "document_start",
      "all_frames": true
    }
  ]
}
       这个清单文件没有指定Browser Action,但是指定了Page Action,以及Content Script和Background。Content Script和Background描述的都是一个JavaScript脚本,前者称为content.js,后者称为background.js。

       Page Action与Browser Action类似,它也对应有一个icon和一个popup。不过,Page Action是有状态的,分为显示和隐藏两种。为显示状态时,它在地址栏右边的Button变亮,并且可以点击。为隐藏状态时,它在地址栏右边的Button变灰,并且不可以点击。

       我们可以通过Chromium提供的Page Action API(chrome.pageAction.show和chrome.pageAction.hide)显示或者隐藏Page Action。一般就是根据当前打开的网页决定要显示还是隐藏一个Page Action。这个判断逻辑一般就是实现在background.js中,如下所示:

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 
  if (tab.url.indexOf("fast.com") >= 0) {
    chrome.pageAction.show(tabId);  
  }

  var views = chrome.extension.getViews({type: "tab"});
  if (views.length > 0) {
    console.log(views[0].whoiam);
  } else {
    console.log("No tab");
  }
});  

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    sendResponse({counter: request.counter + 1 }); 
  }
);

var whoiam = "background.html"
       这个background.js通过chrome.tabs.onUpdated.addListener这个API指定了一个Listener。每当我们切换到浏览器中的Tab时,与这个Listener绑定的函数就会被调用。这个函数是一个匿名函数,它做了两件事情。

       第一件事情是判断当前激活的Tab打开的网页的URL是否包含了"fast.com"关键字。如果包含了,那么就通过chrome.pageAction.show这个API将清单文件中指定的Page Action显示出来。

       第二件事情是调用chrome.extension.getViews这个API检查当前的Extension是否有页面在浏览器的Tab中打开。如果有打开,那么就会获得这些页面的window对象。一旦获得了一个页面window对象,我们就可以访问它的成员。例如,定义在页面中的函数和变量。这实际上是提供了一种方式,使得Extension的不同页面可以相互通信。这里我们就是访问了一个whoiam变量,并且将它的内容打印出来。

       关于Extension页面,及其通信方式,我们接下来还会进一步解释。

       上述background.js还通过chrome.runtime.onMessage.addListener这个API定义了一个Listener以及一个whoiam变量。其中,Listener用来接收其它页面给它发送的消息,变量whoiam可以被其它页面直接访问。

       当Page Action通过chrome.pageAction.show这个API被设置为显示状态时,我们就可以点击它在地址栏右边的按钮,弹出一个窗口。这个窗口在清单文件中指定加载的网页为popup.html,它的内容如下所示:

<html>
  <head>
    <title>Getting Started Extension's Popup</title>
    <style>
      body {
        font-family: "Segoe UI", "Lucida Grande", Tahoma, sans-serif;
        font-size: 100%;
      }
      #status {
        /* avoid an excessively wide status text */
        white-space: pre;
        text-overflow: ellipsis;
        overflow: hidden;
        max-width: 400px;
      }
    </style>

    <!--
      - JavaScript and HTML must be in separate files: see our Content Security
      - Policy documentation[1] for details and explanation.
      -
      - [1]: https://developer.chrome.com/extensions/contentSecurityPolicy
     -->
    <script src="popup.js"></script>
  </head>
  <body>
    <table align='center'>  
      <tr>  
        <td><button id="testRequest">send 0 to tab page</button></td>  
        <td id="resultsRequest"><font color="gray">response: null</font></td>  
      </tr>   
    </table>  
    <div id="status"></div>
    <img id="image-result" hidden>
  </body>
</html>
      与前面Browser action example中的popup.html一样,这个popup.html也包含了一个popup.js,一个div标签和一个img标签。不过,这个popup.html还多了一个table标签。这个table包含了一行两列。其中一列用来显示一个Button,另一列用来显示文本。

      上述popup.html显示出来的效果如图3所示:

图3 Page Action的popup.html

      这个popup.html显示的图片是通过它包含的popup.js指定的,如下所示:

function getImageUrl(callback, errorCallback) {
  callback("http://avatar.csdn.net/5/6/E/1_luoshengyang.jpg", 200, 200);
}

function renderStatus(statusText) {
  document.getElementById('status').textContent = statusText;
}

var counter = 0;

function testRequest() {  
  chrome.tabs.getSelected(null, function(tab) {   
    chrome.tabs.sendRequest(tab.id, {counter: counter}, function handler(response) {  
      counter = response.counter;
      document.querySelector('#resultsRequest').innerHTML = "<font color='gray'> response: " + counter + "</font>";
      document.querySelector('#testRequest').innerText = "send " + (counter -1) + " to tab page";
    });  
  });  
}  

document.addEventListener('DOMContentLoaded', function() {
  getImageUrl(function(imageUrl, width, height) {
    var imageResult = document.getElementById('image-result');
    imageResult.width = width;
    imageResult.height = height;
    imageResult.src = imageUrl;
    imageResult.hidden = false;

    console.log(chrome.extension.getBackgroundPage().whoiam);
  }, function(errorMessage) {
    renderStatus('Cannot display image. ' + errorMessage);
  });

  document.querySelector('#testRequest').addEventListener(  
      'click', testRequest);  
});

var whoiam = "popup.html"
       这个popup.js是在页面的文档加载完成时指定显示的图片的。同时,它还会在页面的文档加载完成时,做另外两件事情。

       第一件事情是调用chrome.extension.getBackgroundPage这个API获得当前Extension的Background页面的window对象,并且通过这个window对象访问定义在Background页面中的whoiam变量。前面我们在background.js定义了一个whoiam变量,因此这里就可以对它进行访问。

       第二件事情为popup.html页面中显示为"send to tab page"的Button指定一个Listener。这个Listener用来监听Button的Click事件。一旦监听到Click事件发生,函数testRequest就会被调用。

       函数testRequest首先通过chrome.tabs.getSelected这个API获得当前激活的Tab,接着又通过chrome.tabs.sendRequest这个API向获得的当前激活的Tab发送一个消息。消息是json格式的,传递一个由变量counter描述的计数给接收者。接收者接收到这个消息之后, 会将它封装的计数取出来,并且增加1,然后再将结果返回来。返回来的结果保存在变量counter的同时,也会显示在popup.html页面中id为resultsRequests的td标签中。

       这样,通过不断地点击popup.html页面中显示为"send to tab page"的Button,就可以不断地与当前激活的Tab通信。后面我们可以看到,实际上是与在Tab中加载的Content Script通信的。

       从popup.js的内容还可以看到,它定义了一个变量whoiam。这个变量可以被其它Extension页面直接访问。

       前面提到,Page action example的清单文件还指定了一个Content Script,即content.js。这个content.js通过matches字段指定仅对URL为“https://fast.com/”的页面生效,也就是它只会注入到URL为“https://fast.com/”的页面中去。注入的内容如下所示:

document.body.innerHTML = "<table align='center'>\
                             <tr>\
                               <td><button id='testRequest'>send 0 to background page</button></td>\
                               <td id='resultsRequest'>response: null</td>\
                               </tr>\
                           </table>" +
                           document.body.innerHTML;

chrome.extension.onRequest.addListener(  
  function(request, sender, sendResponse) {  
    sendResponse({counter: request.counter + 1 });  
  }
);

var counter = 0;

function testRequest() {  
  chrome.runtime.sendMessage({counter: counter}, function(response) {
    counter = response.counter;
    document.querySelector('#resultsRequest').innerText = "response: " + counter;
    document.querySelector('#testRequest').innerText = "send " + (counter -1) + " to background page";
  });
}

document.querySelector('#testRequest').addEventListener(  
   'click', testRequest);
      它注入了一个table。这个table包含一行两列。其中一个列包含了一个显示为“send 0 to background page”的Button,另外一个列用来显示文本。

      注入的效果如图4所示:

图4 Content Script

       当我们点击显示为“send 0 to background page”的Button时,定义在content.js中的函数testRequest就会被调用。函数testRequest用来向当前的Extension的Background页面发送消息。这个消息传递一个由变量counter描述的计数给Background页面。

       Background页面在background.js中通过chrome.runtime.onMessage.addListener这个API定义了一个Listener。这个Listener用来接收上述由content.js发送过来的消息。接收到该消息后,background.js会将其封装的计数取出,并且增加1,然后将结果返回给content.js显示。

       此外,content.js还通过chrome.extension.onRequest.addListener这个API定义了一个Listener。这个Listener用来监听其它页面发送过来的消息。也就是前面提到的从popup.html中发送过来的消息。

       分析到这里,我们小结一下,Page action example这个Extension所包含的内容:

       1. popup.html:显示在一个弹出窗口中。

       2. background.html: 这个页面是由Chromium根据background.js生成出来的,没有显示在窗口中,因此称为Background页面。

       3. content.js:一个注入在宿主页面中的JavaScript。

       其中,前两个属于页面,后面一个属于脚本。事实上,一个Extension可以同时拥有若干个页面。这些页面分为五种类型为:background、popup、tab、infobar、notification。它们分别代表在不同窗口打开的页面。其中,前面两种我们已经描述过了,后面三种也比较容易理解:

       1. tab:像正常网页一样在浏览器的Tab中打开的页面。

       2. infobar: 在浏览器顶部信息栏显示的信息页面。

       3. notification:在浏览器底部显示的通知页面。

       每一个页面都有一个URL。URL的规范为:chrome-extension://[extension-id]/path。其中,协议部分为chrome-extension,extension-id是Chromium为Extension分配的ID,path是页面的路径。

       从图1可以看到,Chrome为Page action example分配的Extension ID为“abcemahgedfccgcmlkaeiabpjjjhhmoc”。按照上述规范,图3显示的popup页面的URL就为“chrome-extension://abcemahgedfccgcmlkaeiabpjjjhhmoc/popup.html”。这个URL可以直接输入到Chrome的地址栏中去,使得popup.html也可以显示在一个Tab中。

       Page action example还包含了另外一个tab.html文件,它的内容如下所示:

<html>
  <head>
    <title>Getting Started Extension's Tab</title>
  </head>
  <body>
    <table align='center' height="100%">
      <tr>
        <td><img src="sample.png" /></td>
      </tr>
    </table>
  </body>
</html>
      这是一个简单的html页来,用来显示打包在Extension中的一个图片sample.png。

      我们可以在浏览器的地址栏中输入“chrome-extension://abcemahgedfccgcmlkaeiabpjjjhhmoc/tab.html”访问这个页面,效果如图5所示:

图5 Open extension page in tab

       这样,我们就可以归纳出一个Extension是由Extension Page和Content Script构成的。

       Extension Page与普通的网页一样,具有一个URL。常规的Extension Page有popup.html、background.html。其中,popup.html显示在一个弹出窗口中,background.html运行在后台,没有显示在窗口中。给出一个Extension Page的URL,我们也可以在浏览器的Tab中打开它。

       Content Script是一个JavaScript脚本,它会注入到宿主网页去执行,从页可以访问它的DOM Tree。这里说的宿主网页,就是在浏览器中加载的网页。这些网页是由网站提供的,不属于Extension的一部分。

       Extension Page之间,以及Extension Page与Content Script之间,是可以相互通信的。正是因为它们可以相互通信,才能形成一个整体,共同去完成一个Extension的功能。其中,background.js所在的background.html扮演着一个中心角色。它在Extension加载的时候就会默默加载,并且一直运行在后台。其它的Extension Page或者Content Script都是按需加载的。因此,我们就可以将Extension的状态维护在background.html中,其它的Extension Page或者Content Script需要的时候,可以与它进行通信。

       那么,Extension Page之间,以及Extension Page与Content Script之间,是如何通信的呢?我们通过图6说明,如下所示:


图6 Extension Page与Content Script之间的通信方式

       同一个Extension的Extension Page都是运行在同一个进程中的。这个进程称为Extension进程,这实际上也是一个Render进程。在同一个进程打开的网页可以在JavaScript中直接获得对方的window对象。有了一个网页的window对象,就可以调它里面定义的函数或者变量,相当于就是可以与它进行通信。

       Chromium的Extension机制提供了两个API:chrome.extension.getViews和chrome.extension.getBackgroundPage。其中,通过前者可以获得Background Page之外的Extension Page的window对象,而通过后者可以获得的Background Page的window对象。chrome.extension.getViews这个API在调用的时候,还可以指定一个type参数,用来指定要获取哪一种类型的Extension Page的window对象。如果没有指定,则获取Background Page之外的所有Extension Page的window对象。

       Extension的Content Script由于是注入在其它网页中运行,因此它们不能与Extension Page进行直接通信,而是要进行跨进程通信。又由于Content Script和Extension Page是相互不知道对方的,因此它们在进行跨进程通信的时候,需要有一个桥梁。这个桥梁就是Browser进程。

       Chromium的Extension机制提供了两个API:chrome.tabs.sendRequest和chome.extension.onRequest,用来从Extension Page向Content Script发送消息。同样,Chromium的Extension机制也为从Content Script向Extension Page发送消息提供了两个API:chrome.runtime.sendMessage和chrome.runtime.onMessage。它们的实现原理是一样的。当chrome.tabs.sendRequest或者chrome.runtime.sendMessage被调用的时候,它们会向Browser进程发送一个类型为ExtensionHostMsg_PostMessage的IPC消息。Browser进程接收到这个消息之后,又会向目标进程发送一个类型为ExtensionMsg_DeliverMessage的IPC消息。目标进程接收到这个消息之后,再通过JavaScript引擎分发给chome.extension.onRequest或者chrome.runtime.onMessage处理。

       了解了Extension Page之间,以及Extension Page与Content Script之间的通信机制之后,我们再来看Extension Page和Content Script的加载过程。

        Chromium的Browser进程在启动的时候,会创建一个Startup Task。这个Startup Task会初始化一个Extension Service,如图7所示:

 

图7  Extension加载过程

       Extension Service在初始化的过程中,会通过一个Installed Loader加载当前用户安装的所有设置为Enabled的Extension。这些Extension形成一个列表,保存在一个Extension Registry中。以后通过这个Extension Registry,就可以获得当前启用的所有Extension的信息。

       如果一个Extension指定了Background Page,那么Browser进程在初始化好浏览器窗口之后,还会自动加载它指定的Background Page,如图8所示:

图8 Background Page和Popup Page的加载过程

       Browser进程初始化好浏览器窗口之后,会发送一个OnBrowserWindowReady通知。这个通知会触发Browser进程创建一个ExtensionHost对象。这个ExtensionHost对象又会通过WebContents类的静态成员函数Create加载指定的Background Page。WebContents类是Chromium的Content层向外提供的一个API。通过这个API,就可以使用Chromium来加载一个指定的网页了。

       注意,Background Page会加载在一个Extension进程中。如果这个Extension进程还没有创建,那么WebContents类的静态成员函数Create会先创建它。以后Browser进程如果要与这个Background Page进行通信,那么就会通过上述创建的ExtensionHost对象进行。从这里我们也可以看到,每一个Background Page在Browser进程中都有一个对应的ExtensionHost对象,就类似于普通的网页在Browser进程中都有一个对应的RenderProcessHostImpl对象一样。这一点可以参考前面Chromium多进程架构简要介绍和学习计划这个系列的文章。这是很好理解的,因为Extension进程本质上也是一个Render进程。

       其它类型的Extension Page,它们则是按需加载的。例如,对于Popup Page来说,当用户点击了它在地址栏右边对应的Button的时候,Browser进程才会加载它们。它们的加载过程与上述的Background Page是类似的,即先创建一个ExtensionHost对象,然后再通过WebContents类的静态成员函数Create进行加载。

       最后,我们再来看Content Script的加载过程,如图9所示:


图9 Content Script的加载过程

       Content Script的加载过程由三个流程组成。

       第一个流程发生在Extension加载过程中,也就是图7所示的Extension加载流程。Browser进程在启动的时候,会创建一个UserScriptMaster对象,用来监听所有的Extension的加载事件。如果当前被加载的Extension指定了Content Script,那么指定的Content Script的内容就会保存在上述UserScriptMaster对象中。

       第二个流程发生在Content Script的宿主网页所对应的Render进程的启动过程中。当这个Render进程启动完成时,Browser进程会获得一个OnProcessLaunched通知。这个通知直接分发给代表该Render进程的一个RenderProcessHostImpl对象处理。这个RenderProcessHostImpl对象首先会到上述的UserScriptMaster对象中收集要在宿主网页中加载的Content Script,然后再通过一个类型为ExtensionMsg_UpdateUserScript的IPC消息将这些Content Script发送给宿主网页所在的Render进程。这个Render进程会通过一个Dispatcher对象接收该IPC消息,并且将发送过来的Content Script保存在一个UserScriptSlave对象中。

       第三个流程发生在Content Script的宿主网页的加载过程中。Content Script可以在Extension的清单文件中指定加载时机。有三个时机可以指定:document_start、document_end和document_idle,分别表示在宿主网页的Document对象开始创建、结束创建以及空闲时加载。接下来,我们假设Content Script指定了在document_start时加载。

       从前面Chromium网页DOM Tree创建过程分析一文可以知道,WebKit在加载的一个网页的时候,首先会为它创建一个Document对象。在创建这个Document对象的时候,WebKit会通过Chromium中的Content层,也就是调用一个RenderFrameImpl对象的成员函数didCreateDocumentElement。这个RenderFrameImpl对象是在Content层中描述的一个在当前Render进程中加载的网页的。

       RenderFrameImpl类的成员函数didCreateDocumentElement在执行的过程中,会到上述的UserScriptSlave对象收集要在当前加载的网页中加载的Content Script。这些Content Script又会进一步交给JavaScript引擎在一个Isolated World中执行。Content Script在Isolated World中执行,意味着它们是被隔离执行的,也就是它们不能访问在宿主网页中定义的JavaScript函数和变量。

       RenderFrameImpl类的成员函数didCreateDocumentElement是如何将Content Script交给JavaScript引擎执行的呢?从前面Chromium网页Frame Tree创建过程分析一文可以知道,Chromium的Content层的每一个RenderFrameImpl对象在WebKit中都对应有一个WebLocalFrameImpl对象,并且该WebLocalFrameImpl对象会保存在它对应的RenderFrameImpl对象的内部。这个WebLocalFrameImpl对象可以看作是WebKit向Chromium的Content层提供的一个API接口。通过调用这个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld,就可以将指定的Content Script交给JavaScript引擎执行了。

       这样,我们就通过两个Extension例子,对Extension机制涉及到的基本概念进行了介绍。为了更进一步理解Extension机制,接下来我们将结合源码,按照以下四个情景对Extension机制进行分析:

       1. Extension的加载过程

       2. Extension Page的加载过程

       3. Content Script的加载过程

       4. Extension Page之间,以及Extension Page与Content Script之间的通信过程

       注意,这些文章的侧重点是分析Extension机制的实现,而不是Extension的开发。Extension的开发,可以参考官方文档:http://code.google.com/chrome/extensions/getstarted.html

       另外,除了Extension机制,Chromium还提供了Plugin机制,用来增强浏览器的功能。Extension和Plugin是两种不同的机制。从狭义上讲,Plugin仅仅是用来增加网页的功能,而Extension不仅能用来增加网页的功能,也能增强浏览器本身功能。而且,两者的开发方式(开发语言和API接口)也是完全不一样的。为了更好地理解两者的区别,后面我们将会用另外一个系列的文章分析Chromium的Plugin机制。

       敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/9/5 0:58:34 原文链接
阅读:52021 评论:7 查看评论

Chromium扩展(Extension)加载过程分析

$
0
0

       Chromium在启动的时候,会根据当前用户的Profile创建一个Extension Service。Extension Service在创建过程中,会加载当前已经安装的所有Extension,并且将它们注册在一个Extension Registry中。以后通过这个Extension Registry,就可以得到当前可用的Extension的信息了。本文接下来就分析Extension的加载过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       Chromium的启动,指的实际上是Chromium的Browser进程的启动。关于Chromium的多进程架构,可以参考前面Chromium多进程架构简要介绍和学习计划这个系列的文章。Chromium的Browser进程在启动之后,会创建一系列的Startup Task。每一个Startup Task都会负责初始化相应的模块。上述的Extension Service就是在一个Startup Task中创建的,如图1所示:


图1 Extension的加载过程

       Extension Service在创建的过程中,会通过一个Installed Loader加载当前已经安装的所有Extension,并且将那些设置为Enabled的Extension注册到Extension Registry中,从而得到一个当前可用的Extension列表。

       接下来,我们就从Chromium的Browser进程的启动开始,分析它加载Extension的过程。

       在前面Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文中,我们以Content Shell APK为例,分析了Browser进程的启动过程。在这个启动过程中,会创建一个BrowserStartupController对象,并且调用这个BrowserStartupController对象的成员函数startBrowserProcessesAsync异步启动和初始化Chromium的Content模块,如下所示:

public class BrowserStartupController {
    ......

    public void startBrowserProcessesAsync(final StartupCallback callback)
            throws ProcessInitException {
        ......

        // Browser process has not been fully started yet, so we defer executing the callback.
        mAsyncStartupCallbacks.add(callback);
        ......

        if (!mHasStartedInitializingBrowserProcess) {
            ......

            prepareToStartBrowserProcess(MAX_RENDERERS_LIMIT);
            ......

            if (contentStart() > 0) {
                // Failed. The callbacks may not have run, so run them.
                enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED);
            }
        }
    }

    ......
}
      这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/BrowserStartupController.java中。

      在Browser进程的Content模块还没有启动过的情况下,BrowserStartupController类的成员变量mHasStartedInitializingBrowserProcess的值会等于false。在这种情况下,BrowserStartupController类的成员函数startBrowserProcessesAsync会做两件事情:

      1. 调用成员函数prepareToStartBrowserProcess在VM中加载libcontent_shell_content_view.so。

      2. 调用成员函数contentStart启动和初始化Content模块。

      从Dalvik虚拟机JNI方法的注册过程分析这篇文章可以知道,VM在加载so的过程中,将会调用它导出的一个名称为JNI_OnLoad的函数。对libcontent_shell_content_view.so来说,它导出的JNI_OnLoad函数的实现如下所示:

// This is called by the VM when the shared library is first loaded.
JNI_EXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  ......

  content::SetContentMainDelegate(new content::ShellMainDelegate());
  return JNI_VERSION_1_4;
}
      这个函数定义在文件external/chromium_org/content/shell/android/shell_library_loader.cc中。

      函数JNI_OnLoad将会创建一个ShellMainDelegate对象,并且调用函数SetContentMainDelegate将它保存在一个全局变量g_content_main_delegate中,如下所示:

namespace {
......

LazyInstance<scoped_ptr<contentmaindelegate> > g_content_main_delegate =
    LAZY_INSTANCE_INITIALIZER;
}  // namespace

void SetContentMainDelegate(ContentMainDelegate* delegate) {
  DCHECK(!g_content_main_delegate.Get().get());
  g_content_main_delegate.Get().reset(delegate);
}
      这个函数定义在文件external/chromium_org/content/app/android/content_main.cc中。

      这一步执行完成后,回到前面分析的BrowserStartupController类的成员函数startBrowserProcessesAsync中,它接下来将会调用另外一个成员函数contentStart启动和初始化Content模块,如下所示:

public class BrowserStartupController {
    ......

    int contentStart() {
        return ContentMain.start();
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/BrowserStartupController.java中。

       BrowserStartupController类的成员函数contentStart调用ContentMain类的静态成员函数start在Browser进程中启动和初始化Content模块。在前面Chromium的Render进程启动过程分析一文中,我们已经分析过ContentMain类的静态成员函数start的实现了。它最终会调用到C++层的一个函数Start启动和初始化Content模块,如下所示:

namespace {
LazyInstance<scoped_ptr<ContentMainRunner> > g_content_runner =
    LAZY_INSTANCE_INITIALIZER;

......
}  // namespace

......

static jint Start(JNIEnv* env, jclass clazz) {
  ......

  if (!g_content_runner.Get().get()) {
    ContentMainParams params(g_content_main_delegate.Get().get());
    g_content_runner.Get().reset(ContentMainRunner::Create());
    g_content_runner.Get()->Initialize(params);
  }
  return g_content_runner.Get()->Run();
}

       这个函数定义在文件external/chromium_org/content/app/android/content_main.cc中。

       函数Start首先判断一个全局变量g_content_runner是否已经指向了一个ContentMainRunner对象。如果还没有指向,那么就会调用ContentMainRunner类的静态成员函数Create创建一个ContentMainRunner对象,如下所示:

ContentMainRunner* ContentMainRunner::Create() {
  return new ContentMainRunnerImpl();
}
       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       从这里可以看到,ContentMainRunner类的静态成员函数Create创建的实际上是一个ContentMainRunnerImpl对象。这个ContentMainRunnerImpl对象返回给前面分析的函数Start之后,就会保存在全局变量g_content_runner中。

       函数Start获得了新创建的ContentMainRunnerImpl对象之后,会调用它的成员函数Initialize,并且将全局变量g_content_main_delegate指向的ShellMainDelegate对象封装在一个类型为ContentMainParams的参数中传递给它,让它执行初始化工作。

      ContentMainRunnerImpl类的成员函数Initialize的实现如下所示:

class ContentMainRunnerImpl : public ContentMainRunner {
 public:
  ......

  virtual int Initialize(const ContentMainParams& params) OVERRIDE {
    ......

    delegate_ = params.delegate;

    ......
  }
 
  ......

 private:
  ......

  ContentMainDelegate* delegate_;
  
  ......
};
      这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

      ContentMainRunnerImpl类的成员函数Initialize将封装在参数params中的一个ShellMainDelegate对象保存在成员变量delegate_中,也就是ContentMainRunnerImpl类的成员变量delegate_指向了一个ShellMainDelegate对象。

      回到前面分析的函数Start中,它最后调用前面创建的ContentMainRunnerImpl对象的成员函数Run启动和初始化Content模块,如下所示:

class ContentMainRunnerImpl : public ContentMainRunner {
 public:
  ......

  virtual int Run() OVERRIDE {
    ......
    const CommandLine& command_line = *CommandLine::ForCurrentProcess();
    std::string process_type =
          command_line.GetSwitchValueASCII(switches::kProcessType);

    MainFunctionParams main_params(command_line);
    ......

#if !defined(OS_IOS)
    return RunNamedProcessTypeMain(process_type, main_params, delegate_);
#else
    return 1;
#endif
  }

  ......
};

       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       ContentMainRunnerImpl类的成员函数Run首先检查当前进程是否指定了switches::kProcessType启动选项。如果指定了,那么就会获取它的值。获取到的值保存在本地变量process_type中,表示当前进程的类型。Browser进程没有指定switches::kProcessType启动选项,因此本地变量process_type的值将为空,表示当前进程是Browser进程。

        ContentMainRunnerImpl类的成员函数Run接下来调用函数RunNamedProcessTypeMain启动和初始化Content模块,如下所示:

int RunNamedProcessTypeMain(
    const std::string& process_type,
    const MainFunctionParams& main_function_params,
    ContentMainDelegate* delegate) {
  static const MainFunction kMainFunctions[] = {
#if !defined(CHROME_MULTIPLE_DLL_CHILD)
    { "",                            BrowserMain },
#endif
#if !defined(CHROME_MULTIPLE_DLL_BROWSER)
#if defined(ENABLE_PLUGINS)
#if !defined(OS_LINUX)
    { switches::kPluginProcess,      PluginMain },
#endif
    { switches::kWorkerProcess,      WorkerMain },
    { switches::kPpapiPluginProcess, PpapiPluginMain },
    { switches::kPpapiBrokerProcess, PpapiBrokerMain },
#endif  // ENABLE_PLUGINS
    { switches::kUtilityProcess,     UtilityMain },
    { switches::kRendererProcess,    RendererMain },
    { switches::kGpuProcess,         GpuMain },
#endif  // !CHROME_MULTIPLE_DLL_BROWSER
  };

  ......

  for (size_t i = 0; i < arraysize(kMainFunctions); ++i) {
    if (process_type == kMainFunctions[i].name) {
      if (delegate) {
        int exit_code = delegate->RunProcess(process_type,
            main_function_params);
#if defined(OS_ANDROID)
        // In Android's browser process, the negative exit code doesn't mean the
        // default behavior should be used as the UI message loop is managed by
        // the Java and the browser process's default behavior is always
        // overridden.
        if (process_type.empty())
          return exit_code;
#endif
        if (exit_code >= 0)
          return exit_code;
      }
      return kMainFunctions[i].function(main_function_params);
    }
  }

  ......

  return 1;
}
       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       函数RunNamedProcessTypeMain在内部定义了一个静态数组kMainFunctions,它会根据参数process_type的值在这个数组中找到对应的函数执行,也就是不同的进程执行不同的函数来启动和初始化Content模块。

       从前面Chromium的Render进程启动过程分析Chromium的GPU进程启动过程分析Chromium的Plugin进程启动过程分析这三篇文章可以知道,Render进程、GPU进程和Plugin进程分别通过调用函数RendererMain、GpuMain和PluginMain启动和初始化Content模块。

       对于Browser进程来说,情况有点特殊,它并没有调用函数BrowserMain来启动和初始化Content模块。这是因为当参数process_type的值等于空时,函数RunNamedProcessTypeMain调用完成另外一个参数delegate指向的一个ShellMainDelegate对象的成员函数RunProcess后,就会直接直接返回,从而不会执行函数BrowserMain。

       这意味着Browser进程是通过调用ShellMainDelegate类的成员函数RunProcess来启动和初始化Content模块的,如下所示:

int ShellMainDelegate::RunProcess(
    const std::string& process_type,
    const MainFunctionParams& main_function_params) {
  ......

  browser_runner_.reset(BrowserMainRunner::Create());
  return ShellBrowserMain(main_function_params, browser_runner_);
}
       这个函数定义在文件external/chromium_org/content/shell/app/shell_main_delegate.cc中。

       ShellMainDelegate类的成员函数RunProcess首先调用BrowserMainRunner类的静态成员函数Create创建一个BrowserMainRunnerImpl对象,如下所示:

BrowserMainRunner* BrowserMainRunner::Create() {
  return new BrowserMainRunnerImpl();
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_runner.cc中。

       创建出来的BrowserMainRunnerImpl对象将会保存在ShellMainDelegate类的成员变量browser_runner_中,并且这个BrowserMainRunnerImpl对象会传递给另外一个函数ShellBrowserMain进行处理,如下所示:

// Main routine for running as the Browser process.
int ShellBrowserMain(
    const content::MainFunctionParams& parameters,
    const scoped_ptr<content::BrowserMainRunner>& main_runner) {
  ......

  int exit_code = main_runner->Initialize(parameters);
  ......

  if (exit_code >= 0)
    return exit_code;

  ......
    
  return exit_code;
}
       这个函数定义在文件external/chromium_org/content/shell/browser/shell_browser_main.cc中。

       函数ShellBrowserMain主要是调用参数main_runner指向的一个BrowserMainRunnerImpl对象的成员函数Initialize在Browser进程中初始化Content模块。BrowserMainRunnerImpl类的成员函数Initialize的返回值将会大于等于0,这时候函数ShellBrowserMain就会沿着调用路径一直返回到Java层去了,从而使得当前线程(Browser进程的主线程)在Java层进入到消息循环中去。

       以上就是Content Shell APK的Browser进程的启动流程。Chrome APK的Browser进程的启动流程也是类似的,它们最后都会通过调用BrowserMainRunnerImpl类的成员函数Initialize初始化Content模块。为了方便描述,接下来我们就将以Chrome APK为例,继续分析BrowserMainRunnerImpl类的成员函数Initialize的实现,从中就可以看到Extension的加载过程。

       BrowserMainRunnerImpl类的成员函数Initialize的实现如下所示:

class BrowserMainRunnerImpl : public BrowserMainRunner {
 public:
  ......

  virtual int Initialize(const MainFunctionParams& parameters) OVERRIDE {
    ......

    if (!initialization_started_) {
      initialization_started_ = true;
      ......

      main_loop_.reset(new BrowserMainLoop(parameters));

      main_loop_->Init();

      ......
    }

    main_loop_->CreateStartupTasks();
    int result_code = main_loop_->GetResultCode();
    if (result_code > 0)
      return result_code;

    // Return -1 to indicate no early termination.
    return -1;
  }

  ......
};
       这个函数定义在文件external/chromium_org/content/browser/browser_main_runner.cc中。

       BrowserMainRunnerImpl类的成员函数Initialize首先检查成员变量initialization_started_的值是否不等于true。如果不等于true,那么就说明Browser进程还没有执行过初始化操作。在这种情况下,BrowserMainRunnerImpl类的成员函数Initialize接下来就会创建一个BrowserMainLoop对象,并且调用这个BrowserMainLoop对象的成员函数Init执行初始化工作,如下所示:

void BrowserMainLoop::Init() {
  ......
  parts_.reset(
      GetContentClient()->browser()->CreateBrowserMainParts(parameters_));
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       BrowserMainLoop对象的成员函数Init的主要任务是创建一个BrowserMainParts对象,并且保存在成员变量parts_中。后面会通过这个BrowserMainParts对象执行一些初始化工作。

       为了创建这个BrowserMainParts对象,BrowserMainLoop对象的成员函数Init首先调用函数GetContentClient获得一个ContentClient对象。对于Chrome APK来说,这个ContentClient对象的实际类型为ChromeContentClient,也就是这里调用函数GetContentClient获得的是一个ChromeContentClient对象。

       有了这个ChromeContentClient对象之后,就可以调用它的成员函数browser获得一个ChromeContentBrowserClient对象。有了这个ChromeContentBrowserClient对象,就可以调用它的成员函数CreateBrowserMainParts创建一个BrowserMainParts对象了,如下所示:

content::BrowserMainParts* ChromeContentBrowserClient::CreateBrowserMainParts(
    const content::MainFunctionParams& parameters) {
  ChromeBrowserMainParts* main_parts;
  // Construct the Main browser parts based on the OS type.
#if defined(OS_WIN)
  main_parts = new ChromeBrowserMainPartsWin(parameters);
#elif defined(OS_MACOSX)
  main_parts = new ChromeBrowserMainPartsMac(parameters);
#elif defined(OS_CHROMEOS)
  main_parts = new chromeos::ChromeBrowserMainPartsChromeos(parameters);
#elif defined(OS_LINUX)
  main_parts = new ChromeBrowserMainPartsLinux(parameters);
#elif defined(OS_ANDROID)
  main_parts = new ChromeBrowserMainPartsAndroid(parameters);
#elif defined(OS_POSIX)
  main_parts = new ChromeBrowserMainPartsPosix(parameters);
#else
  NOTREACHED();
  main_parts = new ChromeBrowserMainParts(parameters);
#endif

  ......

  return main_parts;
}
       这个函数定义在文件external/chromium_org/chrome/browser/chrome_content_browser_client.cc中。

       从这里可以看到,在Android平台上,ChromeContentBrowserClient类的成员函数CreateBrowserMainParts创建的是一个ChromeBrowserMainPartsAndroid对象。这个ChromeBrowserMainPartsAndroid对象是从ChromeBrowserMainParts类继承下来的。

       这一步执行完成之后,BrowserMainLoop对象的成员函数Init就创建了一个ChromeBrowserMainPartsAndroid对象,并且保存在成员变量parts_中。回到前面分析的BrowserMainRunnerImpl类的成员函数Initialize中,它最后会调用前面已经初始化好的BrowserMainLoop对象的成员函数CreateStartupTasks创建一系列Startup Tasks,用来初始化Content模块。这其中就包含了一个类型为PreMainMessageLoopRun的Startup Task,也就是在Browser进程的主线程进入消息循环前执行的Startup Task,如下所示:

void BrowserMainLoop::CreateStartupTasks() {
  .....

  // First time through, we really want to create all the tasks
  if (!startup_task_runner_.get()) {
#if defined(OS_ANDROID)
    startup_task_runner_ = make_scoped_ptr(new StartupTaskRunner(
        base::Bind(&BrowserStartupComplete),
        base::MessageLoop::current()->message_loop_proxy()));
#else
    startup_task_runner_ = make_scoped_ptr(new StartupTaskRunner(
        base::Callback<void(int)>(),
        base::MessageLoop::current()->message_loop_proxy()));
#endif
    ......

    StartupTask pre_main_message_loop_run = base::Bind(
        &BrowserMainLoop::PreMainMessageLoopRun, base::Unretained(this));
    startup_task_runner_->AddTask(pre_main_message_loop_run);

    ......
  }
#if defined(OS_ANDROID)
  if (!BrowserMayStartAsynchronously()) {
    // A second request for asynchronous startup can be ignored, so
    // StartupRunningTasksAsync is only called first time through. If, however,
    // this is a request for synchronous startup then it must override any
    // previous call for async startup, so we call RunAllTasksNow()
    // unconditionally.
    startup_task_runner_->RunAllTasksNow();
  }
#else
  startup_task_runner_->RunAllTasksNow();
#endif
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       BrowserMainLoop类的成员函数CreateStartupTasks首先会检查成员变量startup_task_runner_是否还没有指向一个StartupTaskRunner对象。如果没有指向,那么就会创建一个StartupTaskRunner对象让它指向。这个StartupTaskRunner对象可以用来向当前线程(Browser进程的主线程)的消息队列发送消息,从而可以执行指定的Startup Task。

       类型为PreMainMessageLoopRun的Startup Task绑定了BrowserMainLoop类的成员函数PreMainMessageLoopRun。这意味着接下来BrowserMainLoop类的成员函数PreMainMessageLoopRun会在Browser进程的主线程执行,如下所示:

int BrowserMainLoop::PreMainMessageLoopRun() {
  if (parts_) {
    ......
    parts_->PreMainMessageLoopRun();
  }

  ......
  return result_code_;
}

       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       从前面的分析可以知道,BrowserMainLoop类的成员变量parts_指向的是一个ChromeBrowserMainPartsAndroid对象。BrowserMainLoop类的成员函数PreMainMessageLoopRun调用这个ChromeBrowserMainPartsAndroid对象的成员函数PreMainMessageLoopRun执行Browser进程在PreMainMessageLoopRun阶段的初始化工作。

       ChromeBrowserMainPartsAndroid类的成员函数PreMainMessageLoopRun是从父类ChromeBrowserMainParts继承下来的,它的实现如下所示:

void ChromeBrowserMainParts::PreMainMessageLoopRun() {
  ......

  result_code_ = PreMainMessageLoopRunImpl();

  ......
}
      这个函数定义在文件external/chromium_org/chrome/browser/chrome_browser_main.cc中。

      ChromeBrowserMainParts类的成员函数PreMainMessageLoopRun调用另外一个成员函数PreMainMessageLoopRunImpl执行Browser进程在PreMainMessageLoopRun阶段的初始化工作,如下所示:

int ChromeBrowserMainParts::PreMainMessageLoopRunImpl() {
  ......

  profile_ = CreatePrimaryProfile(parameters(),
                                  user_data_dir_,
                                  parsed_command_line());
  ......

  return result_code_;
}
       这个函数定义在文件external/chromium_org/chrome/browser/chrome_browser_main.cc中。

       ChromeBrowserMainParts类的成员函数PreMainMessageLoopRunImpl执行了一系列的初始化工作。其中的一个初始化工作是为当前登录的用户创建Profile。这是通过调用函数CreatePrimaryProfile实现的。在创建Profile的过程中,就会加载为当前登录的用户安装的Extension。

       接下来我们就继续分析函数CreatePrimaryProfile的实现,如下所示:

Profile* CreatePrimaryProfile(const content::MainFunctionParams& parameters,
                              const base::FilePath& user_data_dir,
                              const CommandLine& parsed_command_line) {
  ......

  Profile* profile = NULL;
#if defined(OS_CHROMEOS) || defined(OS_ANDROID)
  ......
  profile = ProfileManager::GetActiveUserProfile();
#else
  ......
#endif
  if (profile) {
    ......
    return profile;
  }

  ......

  return NULL;
}

       这个函数定义在文件external/chromium_org/chrome/browser/chrome_browser_main.cc中。

       在Android平台上,函数CreatePrimaryProfile调用ProfileManager类的静态成员函数GetActiveUserProfile获得当前用户的Profile,如下所示:

Profile* ProfileManager::GetActiveUserProfile() {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  ......
  Profile* profile =
      profile_manager->GetActiveUserOrOffTheRecordProfileFromPath(
          profile_manager->user_data_dir());
  ......
  return profile;
}
       这个函数定义在文件external/chromium_org/chrome/browser/profiles/profile_manager.cc中。

       ProfileManager类的静态成员函数GetActiveUserProfile首先在当前进程(Browser进程)中获得一个ProfileManager单例对象。通过调用这个ProfileManager单例对象的成员函数user_data_dir可以获得当前用户的数据目录。有了这个数据目录之后 ,再调用上述ProfileManager单例对象的成员函数GetActiveUserOrOffTheRecordProfileFromPath就可以获得当前用户的Profile,如下所示:

Profile* ProfileManager::GetActiveUserOrOffTheRecordProfileFromPath(
    const base::FilePath& user_data_dir) {
#if defined(OS_CHROMEOS)
  ......
#else
  base::FilePath default_profile_dir(user_data_dir);
  default_profile_dir = default_profile_dir.Append(GetInitialProfileDir());
  return GetProfile(default_profile_dir);
#endif
}
       这个函数定义在文件external/chromium_org/chrome/browser/profiles/profile_manager.cc中。

       ProfileManager类的成员函数GetActiveUserOrOffTheRecordProfileFromPath首先调用另外一个成员函数etInitialProfileDir获得Profile目录。这个Profile目录是相对参数user_data_dir描述的数据目录之下的。将Profile目录添加到数据目录之后,就得到Profile目录的绝对路径。有了这个绝对路径之后,ProfileManager类的成员函数GetActiveUserOrOffTheRecordProfileFromPath就调用成员函数GetProfile获得当前用户的Profile,如下所示:

Profile* ProfileManager::GetProfile(const base::FilePath& profile_dir) {
  TRACE_EVENT0("browser", "ProfileManager::GetProfile")
  // If the profile is already loaded (e.g., chrome.exe launched twice), just
  // return it.
  Profile* profile = GetProfileByPath(profile_dir);
  if (NULL != profile)
    return profile;

  profile = CreateProfileHelper(profile_dir);
  DCHECK(profile);
  if (profile) {
    bool result = AddProfile(profile);
    DCHECK(result);
  }
  return profile;
}
       这个函数定义在文件external/chromium_org/chrome/browser/profiles/profile_manager.cc中。

       ProfileManager类的成员函数GetProfile首先调用成员函数GetProfileInfoByPath检查是否已经为参数profile_dir描述的Profile目录创建过Profile。如果已经创建,那么就将该Profile返回给调用者。否则的话,就会调用成员函数CreateProfileHelper为参数profile_dir描述的Profile目录创建一个Profile,并且调用另外一个成员函数AddProfile将其保存在内部,以及返回给调用者。

       ProfileManager类的成员函数AddProfile在将当前用户的Profile保存在内部之后,会根据Profile的内容执行相应初始化工作,如下所示:

bool ProfileManager::AddProfile(Profile* profile) {
  ......

  RegisterProfile(profile, true);
  ......
  DoFinalInit(profile, ShouldGoOffTheRecord(profile));
  return true;
}
       这个函数定义在文件external/chromium_org/chrome/browser/profiles/profile_manager.cc中。

       ProfileManager类的成员函数AddProfile首先调用成员函数RegisterProfile将参数profile描述的Profile保存在内部,接下来调用另外一个成员函数DoFinalnit根据该Profile执行相应的初始化工作,其中就包括创建Extension Service,如下所示:

void ProfileManager::DoFinalInit(Profile* profile, bool go_off_the_record) {
  DoFinalInitForServices(profile, go_off_the_record);
  ......
}
      这个函数定义在文件external/chromium_org/chrome/browser/profiles/profile_manager.cc中。

      ProfileManager类的成员函数DoFinalnit是在调用成员函数DoFinalInitForServices的过程中创建Extension Service的,如下所示:

void ProfileManager::DoFinalInitForServices(Profile* profile,
                                            bool go_off_the_record) {
#if defined(ENABLE_EXTENSIONS)
  extensions::ExtensionSystem::Get(profile)->InitForRegularProfile(
      !go_off_the_record);
  ......
#endif
  ......
}
      这个函数定义在文件external/chromium_org/chrome/browser/profiles/profile_manager.cc中。

      从这里可以看到,在定义了宏ENABLE_EXTENSIONS的情况下,Chromium才会支持Extension。这时候ProfileManager类的成员函数DoFinalInitForServices首先根据参数profile描述的Profile获得一个ExtensionSystemImpl对象,然后再调用这个ExtensionSystemImpl对象的成员函数InitForRegularProfile创建一个Extension Service,如下所示:

void ExtensionSystemImpl::InitForRegularProfile(bool extensions_enabled) {
  ......

  process_manager_.reset(ProcessManager::Create(profile_));

  shared_->Init(extensions_enabled);
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_system_impl.cc中。

      ExtensionSystemImpl类的成员函数InitForRegularProfile首先会调用ProcessManager类的静态成员函数Create创建一个ProcessManager对象,并且保存在成员变量process_manager_。在接下来一篇文章中,我们就会看到,Extension的Background Page就是通过这个ProcessManager对象加载起来的。

      ExtensionSystemImpl类的成员变量shared_指向的是一个ExtensionSystemImpl::Shared对象。ExtensionSystemImpl类的成员函数InitForRegularProfile调用这个ExtensionSystemImpl::Shared对象的成员函数Init创建一个Extension Service,如下所示:

void ExtensionSystemImpl::Shared::Init(bool extensions_enabled) {
  ......

  user_script_master_ = new UserScriptMaster(profile_);
  ......

  extension_service_.reset(new ExtensionService(
      profile_,
      CommandLine::ForCurrentProcess(),
      profile_->GetPath().AppendASCII(extensions::kInstallDirectoryName),
      ExtensionPrefs::Get(profile_),
      blacklist_.get(),
      autoupdate_enabled,
      extensions_enabled,
      &ready_));
  ......

  extension_service_->Init();
 
  ......
}

       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_system_impl.cc中。

       Extension Service通过一个ExtensionService对象描述。ExtensionSystemImpl::Shared类的成员函数Init创建了这个ExtensionService对象之后,会保存在成员变量extension_service_中,并且调用这个ExtensionService对象的成员函数对其描述的Extension Service进行初始化,如下所示:

       此外,我们还看到,ExtensionSystemImpl::Shared类的成员函数Init还创建了一个UserScriptMaster对象保存在成员变量user_script_master_中。这个UserScriptMaster是用来管理接下来要加载的Extension的Content Script的。这一点我们在后面的文章会进行详细分析。

       现在,我们主要关注Extension Service的初始化过程。因此,接下来我们继续分析ExtensionService类的成员函数Init的实现,如下所示:

void ExtensionService::Init() {
  ......

  const CommandLine* cmd_line = CommandLine::ForCurrentProcess();
  if (cmd_line->HasSwitch(switches::kInstallFromWebstore) ||
      cmd_line->HasSwitch(switches::kLimitedInstallFromWebstore)) {
    ......
  } else {
    ......

    // LoadAllExtensions() calls OnLoadedInstalledExtensions().
    ......
    extensions::InstalledLoader(this).LoadAllExtensions();
    ......

    // Attempt to re-enable extensions whose only disable reason is reloading.
    std::vector<std::string> extensions_to_enable;
    const ExtensionSet& disabled_extensions = registry_->disabled_extensions();
    for (ExtensionSet::const_iterator iter = disabled_extensions.begin();
        iter != disabled_extensions.end(); ++iter) {
      const Extension* e = iter->get();
      if (extension_prefs_->GetDisableReasons(e->id()) ==
          Extension::DISABLE_RELOAD) {
        extensions_to_enable.push_back(e->id());
      }
    }
    for (std::vector<std::string>::iterator it = extensions_to_enable.begin();
         it != extensions_to_enable.end(); ++it) {
      EnableExtension(*it);
    }
 
    ......
  }

  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_service.cc中。

       ExtensionService类的成员函数Init首先检查Browser进程的启动参数是否包含有switches::kInstallFromWebstore或者switches::kLimitedInstallFromWebstore选项。如果包含有,那么就只会从Web Store上加载当前用户的Extension。我们假设没有包含这两个选项,那么ExtensionService类的成员函数Init将会从本地加载当前用户的Extension。

       ExtensionService类的成员函数Init首先构造一个InstalledLoader对象,然后再调用这个InstalledLoader对象的成员函数LoadAllExtensions加载当前用户安装的所有Extension。这些加载的Extension,即有Enabled的,也有Disabled的。

       加载后的Extension会保存在ExtensionService类的成员变量registry_描述的一个Extension Registry中。ExtensionService类的成员函数Init最后会从这个Extension Registry获得那些处于Enabled状态的Extension,并且调用另外一个成员函数EnableExtension启用它们。

       接下来,我们主要关注Extension的加载过程。因此,我们继续分析InstalledLoader类的成员函数LoadAllExtensions的实现,如下所示:

void InstalledLoader::LoadAllExtensions() {
  ......

  Profile* profile = extension_service_->profile();
  scoped_ptr<ExtensionPrefs::ExtensionsInfo> extensions_info(
      extension_prefs_->GetInstalledExtensionsInfo());

  ......

  for (size_t i = 0; i < extensions_info->size(); ++i) {
    if (extensions_info->at(i)->extension_location != Manifest::COMMAND_LINE)
      Load(*extensions_info->at(i), should_write_prefs);
  }

  ......
}

       这个函数定义在文件external/chromium_org/chrome/browser/extensions/installed_loader.cc中。

       InstalledLoader类的成员函数LoadAllExtensions首先获得当前用户安装的所有Extension。注意,这些Extension既包括用户在“chrome://extensions”页面中安装的Extension,也包括用户在启动Chromium时通过命令行参数“--load-extension”指定的Extension。不过,InstalledLoader类的成员函数LoadAllExtensions只会加载那些非命令行参数指定的Extension。对于命令行参数指定的Extension,在Extension Service初始化结束后,Extension System会通过另外一个Unpacked Installer来加载它们。

       InstalledLoader类的成员函数LoadAllExtensions是通过调用另外一个成员函数Load加载那些非命令行参数指定的Extension的,如下所示:

void InstalledLoader::Load(const ExtensionInfo& info, bool write_to_prefs) {
  std::string error;
  scoped_refptr<const Extension> extension(NULL);
  if (info.extension_manifest) {
    extension = Extension::Create(
        info.extension_path,
        info.extension_location,
        *info.extension_manifest,
        GetCreationFlags(&info),
        &error);
  } else {
    error = errors::kManifestUnreadable;
  }

  ......

  extension_service_->AddExtension(extension.get());
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/installed_loader.cc中。

       InstalledLoader类的成员函数Load首先是根据参数info描述的Extension Info创建一个Extension对象,这是通过调用Extension类的静态成员函数Create实现的。这个Extension对象最终会交给前面创建的Extension Service处理。当Extension Service处理完毕,参数info描述的Extension就加载完毕。

       InstalledLoader类的成员变量extension_service_指向的是一个ExtensionService对象。这个ExtensionService对象描述的就是前面创建的Extension Service。通过调用这个ExtensionService对象的成员函数AddExtension即可以将参数info描述的Extension交给前面创建的Extension Service处理。处理过程如下所示:

void ExtensionService::AddExtension(const Extension* extension) {
  ......

  if (extension_prefs_->IsExtensionBlacklisted(extension->id())) {
    ......
    registry_->AddBlacklisted(extension);
  } else if (!reloading &&
             extension_prefs_->IsExtensionDisabled(extension->id())) {
    registry_->AddDisabled(extension);
    ......
  } else if (reloading) {
    ......
    EnableExtension(extension->id());
  } else {
    ......
    registry_->AddEnabled(extension);
    ......
    NotifyExtensionLoaded(extension);
  }
  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_service.cc中。

       ExtensionService类的成员函数AddExtension会判断参数extension描述的Extension的状态,并且执行的操作:

       1. 如果它被用户列入黑名单,那么就将它记录在Extension Registry内部的Black List上。

       2. 如果它被用户禁用,那么将它记录在Extension Registry内部的Disabled List上。

       3. 如果它被重新加载,那么对它执行一个Enable操作。

       4. 如果它是第一次加载,那么将它记录在Extension Registry内部的Enabled List上。

       接下来我们主要关注第4种情况。这时候ExtensionService类的成员函数AddExtension首先会调用成员变量registry_指向的一个ExtensionRegistry对象的成员函数AddEnabled将参数extension描述的Extension记录在Extension Registry内部的Enabled List上,接下来又调用另外一个成员函数NotifyExtensionLoaded通知其它模块,有一个新的Extension被加载。

       接下来我们就继续分析ExtensionRegistry类的成员函数AddEnabled和ExtensionService类的成员函数NotifyExtensionLoaded的实现,以便完整了解Extension的加载过程。

       ExtensionRegistry类的成员函数AddEnabled的实现如下所示:

bool ExtensionRegistry::AddEnabled(
    const scoped_refptr<const Extension>& extension) {
  return enabled_extensions_.Insert(extension);
}
       这个函数定义在文件external/chromium_org/extensions/browser/extension_registry.cc中。

       ExtensionRegistry类的成员变量enabled_extensions_描述的就是一个Enabled List,因此ExtensionRegistry类的成员函数AddEnabled会将参数extension描述的Extension保存在里面。

       ExtensionService类的成员函数NotifyExtensionLoaded的实现如下所示:

void ExtensionService::NotifyExtensionLoaded(const Extension* extension) {
  ......

  registry_->TriggerOnLoaded(extension);
 
  ......
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_service.cc中。

      ExtensionService类的成员函数NotifyExtensionLoaded会通过Extension Registry通知其它模块有一个新的Extension被加载,这是通过调用成员变量registry_指向的一个ExtensionRegistry对象的成员函数TriggerOnLoaded实现的,如下所示:

void ExtensionRegistry::TriggerOnLoaded(const Extension* extension) {
  DCHECK(enabled_extensions_.Contains(extension->id()));
  FOR_EACH_OBSERVER(ExtensionRegistryObserver,
                    observers_,
                    OnExtensionLoaded(browser_context_, extension));
}
       这个函数定义在文件external/chromium_org/extensions/browser/extension_registry.cc中。

       一个模块如果需要关注新加载的Extension,那么就会注册一个Extension Registry Observer到Extension Registry的内部。这些Extension Registry Observer保存在ExtensionRegistry类的成员变量observers_描述的一个List中。

       ExtensionRegistry类的成员函数TriggerOnLoaded所做的事情就是调用每一个注册在Extension Registry中的Extension Registry Observer的成员函数OnExtensionLoaded,分别通知它们有一个新的Extension被加载。在后面的文章中,我们就会看到,负责管理Content Script的User Script Master模块会注册一个Extension Registry Observer到Extension Registry,目的就是获取每一个新加载的Extension指定的Content Script,以便在合适的时候注入到宿主网页中去执行。

       至此,我们就分析完成了Chromium加载Extension的过程。这是在Extension Service的初始化过程中执行的。这个Extension Service又是在Chromium为当前用户创建Profile的过程中启动的。最后,为当前用户创建Profile的操作是在Chromium启动的过程中执行的。这意味着Extension是在Chromium启动的时候加载的,也就是在Chromium的Browser进程启动时加载。在接下来的一篇文章中,我们继续分析Extension的Background Page和Popup Page的加载过程,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/9/12 0:59:50 原文链接
阅读:39383 评论:6 查看评论
Viewing all 35570 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>