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

leetcode: 33. Search in Rotated Sorted Array

$
0
0

Q

Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.

(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2).

You are given a target value to search. If found in the array return its index, otherwise return -1.

You may assume no duplicate exists in the array.

AC

class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) / 2
            if nums[mid] == target:
                return mid
            elif (nums[mid] >= nums[left] and nums[left] <= target < nums[mid]) or \
                    (nums[mid] < nums[left] and not (nums[mid] < target <= nums[right])):
                right = mid - 1
            else:
                left = mid + 1
        return -1

if __name__ == "__main__":
    assert Solution().search([3, 5, 1], 3) == 0
    assert Solution().search([1], 1) == 0
    assert Solution().search([4, 5, 6, 7, 0, 1, 2], 5) == 1


作者:JNingWei 发表于2017/11/11 17:19:23 原文链接
阅读:109 评论:0 查看评论

leetcode: 34. Search for a Range

$
0
0

Q

Given an array of integers sorted in ascending order, find the starting and ending position of a given target value.

Your algorithm's runtime complexity must be in the order of O(log n).

If the target is not found in the array, return [-1, -1].

Example

Given [5, 7, 7, 8, 8, 10] and target value 8,
return [3, 4].

AC

class Solution(object):
    def searchRange(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        if len(nums) == 0:
            return [-1, -1]
        n = len(nums)
        l, r = 0, n-1
        while l < r:
            m = (l+r)/2
            if nums[m] < target: l = m+1
            else: r = m
        if nums[l] != target: return -1, -1
        left = l
        l, r = left, n-1
        while l < r:
            m = (l+r)/2+1
            if nums[m] == target: l = m
            else: r = m-1
        right = l
        return left, right


class Solution2(object):
    def searchRange(self, nums, target):
        import bisect
        l = bisect.bisect_left(nums, target)
        r = bisect.bisect_right(nums, target)
        if l >= len(nums) or nums[l]!=target:
            l = -1
        if r == 0 or nums[r-1] != target:
            r = -1
        else:
            r = r-1
        return [l, r]

if __name__ == "__main__":
    assert Solution().searchRange([2, 2], 3) == (-1, -1)
    assert Solution().searchRange([5, 7, 7, 8, 8, 10], 8) == (3, 4)


作者:JNingWei 发表于2017/11/11 17:21:08 原文链接
阅读:109 评论:0 查看评论

leetcode: 35. Search Insert Position

$
0
0

Q

Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.

You may assume no duplicates in the array.

Example 1:

Input: [1,3,5,6], 5
Output: 2

Example 2:

Input: [1,3,5,6], 2
Output: 1

Example 3:

Input: [1,3,5,6], 7
Output: 4

Example 1:

Input: [1,3,5,6], 0
Output: 0

AC

class Solution(object):
    def searchInsert(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        if not nums:
            return 0
        for idx,val in enumerate(nums):
            if val >= target:
                return idx
        return len(nums)

class Solution2(object):
    def searchInsert(self, nums, target):
        import bisect
        return bisect.bisect_left(nums, target)

class Solution3(object):
    def searchInsert(self, nums, target):
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) / 2
            if nums[mid] >= target:
                right = mid - 1
            else:
                left = mid + 1
        return left


if __name__ == "__main__":
    assert Solution().searchInsert([1, 3, 5, 6], 5) == 2
    assert Solution().searchInsert([1, 3, 5, 6], 2) == 1
    assert Solution().searchInsert([1, 3, 5, 6], 7) == 4
    assert Solution().searchInsert([1, 3, 5, 6], 0) == 0


作者:JNingWei 发表于2017/11/11 17:23:18 原文链接
阅读:105 评论:0 查看评论

leetcode: 36. Valid Sudoku

$
0
0

Q

Determine if a Sudoku is valid, according to: Sudoku Puzzles - The Rules.

The Sudoku board could be partially filled, where empty cells are filled with the character '.'.

这里写图片描述

Note:

A valid Sudoku board (partially filled) is not necessarily solvable. Only the filled cells need to be validated.

AC

class Solution(object):
    def isValidSudoku(self, board):
        """
        :type board: List[List[str]]
        :rtype: bool
        """
        map_row = [{} for _ in xrange(9)]
        map_col = [{} for _ in xrange(9)]
        map_cell = [[{} for _ in xrange(3)] for __ in xrange(3)]
        for i in xrange(9):
            for j in xrange(9):
                char = board[i][j]
                if char == '.': continue
                if char in map_row[i]: return False
                else: map_row[i][char] = [i,j]
                if char in map_col[j]: return False
                else: map_col[j][char] = [i,j]
                if char in map_cell[i/3][j/3]: return False
                else: map_cell[i/3][j/3][char] = [i,j]
        return True

class Solution2(object):
    def isValidSudoku(self, board):
        """
        :type board: List[List[str]]
        :rtype: bool
        """
        for i in xrange(9):
            if not self.isValidList([board[i][j] for j in xrange(9)]) or \
                    not self.isValidList([board[j][i] for j in xrange(9)]):
                return False
        for i in xrange(3):
            for j in xrange(3):
                if not self.isValidList([board[m][n] for n in xrange(3 * j, 3 * j + 3) \
                                         for m in xrange(3 * i, 3 * i + 3)]):
                    return False
        return True

    def isValidList(self, xs):
        xs = filter(lambda x: x != '.', xs)
        return len(set(xs)) == len(xs)

if __name__ == "__main__":
    board = [[1, '.', '.', '.', '.', '.', '.', '.', '.'],
             ['.', 2, '.', '.', '.', '.', '.', '.', '.'],
             ['.', '.', 3, '.', '.', '.', '.', '.', '.'],
             ['.', '.', '.', 4, '.', '.', '.', '.', '.'],
             ['.', '.', '.', '.', 5, '.', '.', '.', '.'],
             ['.', '.', '.', '.', '.', 6, '.', '.', '.'],
             ['.', '.', '.', '.', '.', '.', 7, '.', '.'],
             ['.', '.', '.', '.', '.', '.', '.', 8, '.'],
             ['.', '.', '.', '.', '.', '.', '.', '.', 9]]
    assert Solution().isValidSudoku(board)


作者:JNingWei 发表于2017/11/11 17:28:06 原文链接
阅读:3 评论:0 查看评论

python: 切片符号(slice notation)

$
0
0

起因

在阅读大神的代码时看到这么一段:

nums[k + 1:] = nums[:k:-1]

Python语言的正序切片符号我懂,逆序切片符号我就不是很懂了。上StackoverFlow搜了一下,总结如下。

总结

正序

Grammar Annotation
L[:] [seq[0], seq[1], …, seq[-1] ]
L[low:] [seq[low], seq[low+1], …, seq[-1] ]
L[:high] [seq[0], seq[1], …, seq[high-1]]
L[low:high] [seq[low], seq[low+1], …, seq[high-1]]
L[::stride] [seq[0], seq[stride], …, seq[-1] ]
L[low::stride] [seq[low], seq[low+stride], …, seq[-1] ]
L[:high:stride] [seq[0], seq[stride], …, seq[high-1]]
L[low:high:stride] [seq[low], seq[low+stride], …, seq[high-1]]

逆序

Grammar Annotation
L[::-stride] [seq[-1], seq[-1-stride], …, seq[0] ]
L[high::-stride] [seq[high], seq[high-stride], …, seq[0] ]
L[:low:-stride] [seq[-1], seq[-1-stride], …, seq[low+1]]
L[high:low:-stride] [seq[high], seq[high-stride], …, seq[low+1]]

实验代码

# coding=utf-8

L = [0, 10, 20, 30, 40, 50]
low, high, stride = 1, 4, 2

# 正序
assert L[:] == [0, 10, 20, 30, 40, 50] # [seq[0],   seq[1],          ..., seq[-1]    ]
assert L[low:] == [10, 20, 30, 40, 50] # [seq[low], seq[low+1],      ..., seq[-1]    ]
assert L[:high] == [0, 10, 20, 30]     # [seq[0],   seq[1],          ..., seq[high-1]]
assert L[low:high] == [10, 20, 30]     # [seq[low], seq[low+1],      ..., seq[high-1]]
assert L[::stride] == [0, 20, 40]      # [seq[0],   seq[stride],     ..., seq[-1]    ]
assert L[low::stride] == [10, 30, 50]  # [seq[low], seq[low+stride], ..., seq[-1]    ]
assert L[:high:stride] == [0, 20]      # [seq[0],   seq[stride],     ..., seq[high-1]]
assert L[low:high:stride] == [10, 30]  # [seq[low], seq[low+stride], ..., seq[high-1]]

# 逆序
assert L[::-stride] == [50, 30, 10]    # [seq[-1],   seq[-1-stride],   ..., seq[0]    ]
assert L[high::-stride] == [40, 20, 0] # [seq[high], seq[high-stride], ..., seq[0]    ]
assert L[:low:-stride] == [50, 30]     # [seq[-1],   seq[-1-stride],   ..., seq[low+1]]
assert L[high:low:-stride] == [40, 20] # [seq[high], seq[high-stride], ..., seq[low+1]]


作者:JNingWei 发表于2017/11/11 20:02:21 原文链接
阅读:5 评论:0 查看评论

从零开始前端学习[49]:js函数的初步认识

$
0
0

js函数的初步认识

  • 函数的定义
  • 函数作用
  • 函数的分类与调用

提示
博主:章飞_906285288
博客地址:http://blog.csdn.net/qq_29924041


函数的定义

函数,其实更多的是针对面向过程语言的一种叫法,如C语言中,都是以函数来称谓的,对于面向对象语言来说,更对的是称为方法,在js中,则统一称谓函数,函数是什么?通俗来说,函数就好像我们的css类名一样,我们书写类里面的样式的时候,就是函数的定义,给标签添加上类名之后,则可以多次去使用,这个过程叫做函数调用
函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块
一般来说函数主要分为三个步骤,函数声明,函数定义,以及函数的调用这三个过长,但是js中,很多时候声明和定义都是一起的,只有在c或者C++中可以声明单独拿出来。

函数的定义过程如下所示:

    function myfirstFunction(){
        console.log("my first function");
    }
    myfirstFunction();

所以函数的定义包括了:function,函数名,形式参数,以及大括号等

    function 函数名(参数){
        函数体;
    }

注意:函数的调用的位置可以是在定义之前,也可以是在定义之后,这个过程就类似java中方法的调用,可以是之前或者之后都行。
当然 函数的调用可以是直接调用,也可以是通过变量的事件来进行触发调用
如:

    function changeBackground(){
        var box =  document.getElementById("p");
        box.style.backgroundColor = "blue";
    }

    document.getElementById("p").onclick = changeBackground;

以上函数在调用的时候,需要注意的是,如果在调用的时候加上(),的话,表示的是函数的自执行,也就是自动执行的意思,这个时候会默认执行一次,之后函数就会被回收掉,单次情况下,这个时候就不会再去执行了


函数作用

一句话搞定:

函数的作用:在出现大量程序相同的时候,可以封装为一个function,这样只需要调用一次就能执行很多语句,模块化编程,让复杂的逻辑变得简单。


函数的分类与调用

首先来讲函数的分类,在js中函数分为命名函数和匿名函数,故名思议,命名函数就是有名字的函数,而匿名函数则可以理解为没有名字的函数。如下:

命名函数的定义:
function myfirstfunction(){
    console.log("myfirst");
}
上面对应的函数名字为myfirstfunction

而匿名函数则主要分为两种,一种也是先定义,但是这个时候赋值给变量,第二种就是在触发事件,或者调用的时候直接定义

第一种赋值给变量
var function_1 = function(){
    console.log("function_1");
}
第二种调用的时候直接定义:

box.onclick = function(){
    alert("box onclick");
}

对于js来说,主要函数分类也就是这两种,当然这是从有无函数名上来进行划分的,还可以从其它类型上进行划分。

其次主要是函数的调用,其实从一开始我们就用了函数的调用,即是匿名函数的调用,在函数调用上,主要可以分为函数的自执行以及被动执行。

函数的执行:
自执行:即不需要有任何触发条件的情况下,函数自己就去执行了,
var function_1 = function(){
    alert("function_1");
}
function_1();

被动执行:
box.onclick  = function_1;

从上面可以看到,他们的主要区别就在于有无括号上面。如果函数执行的时候有括号,那么,这个时候就是自执行,如果没有括号,这个时候就是被动执行。
当然还有一个奇葩点:

box.onclick = function_1();
这个时候函数function_1();会自执行,执行完毕之后box再去点击的时候是不会有任何效果的
函数栈在调用完毕之后会进行回收,所以点击是无效的。

下面上代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <meta charset="UTF-8"><!--申明当前网页的编码集UTF-8-->
  <meta name="Generator" content="EditPlus®">   <!--编辑器的名称-->
  <meta name="Author" content="作者是谁">       
  <meta name="Keywords" content="关键词">
  <meta name="Description" content="描述和简介">
  <style type="text/css">                                        
        body,dl,dd,dt,p,h1,h2,h3,h4,h5,h6{ margin: 0;}
        ul,ol{margin: 0; list-style: none; padding: 0;}
        a{ text-decoration: none; }
        *{ margin: 0; padding: 0; }
        .main{width: 1200px;margin: 10px auto;box-shadow: 0 0 10px 0px red;border: 1px solid transparent}
        p{width: 100px;height: 100px;margin: 10px;background: deeppink}
  </style>
</head>
<body>
  <div class="main">
    <p id="box_p_1">box_p_1</p>
    <p id="box_p_2">box_p_2</p>
  </div>
  <script>
    var box_p_1 = document.getElementById("box_p_1");
    var box_p_2 = document.getElementById("box_p_2");
    box_p_1.onclick = function () {   //匿名函数1
        alert("沃日 你竟然弹出来了");
    }
    //匿名函数2,将函数赋值给function_2,然后调用的时候直接去调用function_2
    var function_2 = function(){
        console.log("function2");
    }
    function_2();
    function myFirstFunction() {  //命名函数
        alert("my FirstFunction and box_p_1 clicked");
    }
    myFirstFunction(); //函数名字加括号表示函数的调用过程,这个是函数的自执行,也就是自动执行。每个函数被执行完毕之后,会被回收掉
    box_p_2.onclick = myFirstFunction_2; //函数在被动调用的时候,不能够加(),如果加()的话,这个时候就会自动执行,点击后没有任何效果
    function myFirstFunction_2() {
        alert("my FirstFunction box_p_2 clicked");
    }

  </script>
</body>
</html>

至于效果就不演示了,有兴趣的自己run一下,感受一下。

作者:qq_29924041 发表于2017/11/11 23:19:43 原文链接
阅读:26 评论:0 查看评论

Effective Java -- 消除过期的对象引用

$
0
0

本文是 《Effective Java Second Edition》第6条的读书笔记,文中如有错误或表述不当,非常欢迎能批评指正,本人不胜感激!


我们来用一段通俗易懂的话来描述什么是过期的对象引用
在我们的程序中,有一些对象我们程序员已经非常明确的知道其不会再被使用,由于还存在被引用(我们知道如果执行了这种引用就是非法操作),所以不会被GC(内存泄露),所以这种引用的额存在已经没有任何的价值,故我们将这种引用行为称之为 过期的对象引用

在我们数据结构之中,堆栈和队列的实现就是一个可能会存在过期的对象引用的例子。
看如下队列实现的代码

package com.blog.effective6;

/**
 * 存在过期对象引用的队列.
 *
 * @author 张俊强~.
 * @date 2017/11/12-8:48.
 */
@SuppressWarnings("unchecked")
public class MyQueue<T> {

    private T[] elements;   //保存数据的对象数组
    private int size = 0;   //当前队列的大小
    private int cur = 0;    //标识下一个pop出来的对象的位置
    private static final int MyQueueSize = 20;//队列的最大大小

    public MyQueue() {
        elements = (T[]) new Object[MyQueueSize];
    }

    public void push(T t) {
        if (size < MyQueueSize) {
            elements[size++] = t;
        } else {
            throw new RuntimeException("当前队列已满");
        }
    }

    public T pop() {
        if (cur < size) {
            return elements[cur++];
        }
        throw new RuntimeException("当前队列为空");
    }

}

如果我们首先push()五个元素,再pop()两个元素出来。
这时候我们的cur指向的就是第三个元素的位置
前两个元素由于被pop()出去,所以我们知道其不会再被使用,然后它仍然在内存中存在。那么对这个存在的对象的应用就是过期的了!

如下图示
过期的对象引用的图示

那么在这个例子中,我们要怎样来消除这一引用呢?

如下,我们在pop()的时候,将对应的要pop()的数组元素的对象引用指向null就可以了。

    public T pop() {
        if (cur < size) {
            T t= elements[cur];
            elements[cur]=null;     //指向 null
            cur++;
            return t;
        }
        throw new RuntimeException("当前队列为空");
    }

过期的对象引用势必会引起内存泄露,有关内存泄露的更多内容下次再写!


以上 2017/11/12 09:49 于上海

作者:zjq_1314520 发表于2017/11/12 9:54:46 原文链接
阅读:0 评论:0 查看评论

从零开始前端学习[50]:js操作标签属性,读写属性标签

$
0
0

js操作标签属性

  • 标签属性
  • js下操作标签属性

提示:
博主:章飞_906285288
博客地址:http://blog.csdn.net/qq_29924041


前端的一些标签属性

标签属性:通俗的说,就是写在我们标签里面有等号的一些属性
如img标签固有的属性

<img src="" alt="" title="" id ="" class = "" width = "" height = ""/>
src是图片的路径,alt是对应加载失败的时候的提示,title故名思议就是图片名字

如标签固有的一些属性class,id

<div id = "" class ="" ></div>

而css的属性则是在style属性里面写出来的。二者之间是有区别的

注意:标签属性是可读可写的,即可以读取这些标签属性,同时也可以对这些标签属性进行修改


js下如何去操作标签属性

js对标签属性的读取

js下面是可以通过对象来找到对应标签的属性的,如对于img标签来说:

<img src = "" alt = "" title = "" id ="img_demo" class ="" width="" height =""/>
<script>
    var img_demo = document.getElementById("img_demo");
    img_demo.onclick = function(){
        console.log(img_demo.src);
        console.log(img_demo.alt);
        console.log(img_demo.title);
        console.log(img_demo.id);
        console.log(img_demo.class);
        console.log(img_demo.width);
        console.log(img_demo.height);
    }
</script>

从上面可以看到,标签属性的获取,就是通过对象.属性的形式获取

即:对象.标签属性

js对标签属性的修改

既然能够读取到标签属性,那么就可以对标签属性进行相对应的修改,
如下所示:

var img_demo = document.getElementById("img_demo");
img_demo.id = "img_demo2";

即通过先获取标签属性,然后再去进行赋值操作
但是注意一点就是:

var img_demo = document.getElementById("img_demo");
var test = img_demo.id;
test = "img_demo2";

以上的这种场景是无效的,因为img_demo.id得到就是img_demo,然后将这个值赋值给了test,所以test是一个常量,常量是不能够进行修改的,所以是失败的。

如下是以img标签为例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <meta charset="UTF-8"><!--申明当前网页的编码集UTF-8-->
  <meta name="Generator" content="EditPlus®">   <!--编辑器的名称-->
  <meta name="Author" content="作者是谁">       
  <meta name="Keywords" content="关键词">
  <meta name="Description" content="描述和简介">
  <style type="text/css">                                        
        body,dl,dd,dt,p,h1,h2,h3,h4,h5,h6{ margin: 0;}
        ul,ol{margin: 0; list-style: none; padding: 0;}
        a{ text-decoration: none; }
        *{ margin: 0; padding: 0; }
        .main{width: 1200px;box-shadow: 0 0 10px 0 deeppink;margin: auto;padding: 10px;
          text-align: center;}
        .main .header{width: 100%;text-align: center;padding: 5px}

  </style>
</head>
<body>
<div class="main">
  <div class="header">标签属性</div>
  <img src="img/u=3060298968,3727985394&fm=27&gp=0.jpg" title="dota2图片" class="dota_img_class" id= "dota_img" alt="图片加载" width="534px" height="300px" >
  <script>
    var  dota_img = document.getElementById("dota_img");
    var src = dota_img.src;  //注意,dota_img.src获得的值是一个常量赋值给了src,那么src不能代表dota_img.src,z只能表示是dota_img.src的值
    var id = dota_img.id;
    var alt = dota_img.alt;
    var width = dota_img.width;
    var height = dota_img.height;
    var title = dota_img.title;
    console.log(src,id,alt,width,height,title);
    dota_img.onclick = function () {
        dota_img.src = "img/u=516965642,3851528614&fm=27&gp=0.jpg"
        dota_img.id = "dota_img_id";
        dota_img.style.width = "400px"; //注意img的宽高只能通过样式来进行更改,普通更改dota_img.width无效,
        dota_img.style.height = "300px";
        dota_img.title = "change2"
        console.log(dota_img.src,  dota_img.id ,dota_img.width,dota_img.height,dota_img.title);
    }
  </script>
</div>
</body>
</html>

显示效果如下所示:

这里写图片描述

作者:qq_29924041 发表于2017/11/12 10:26:53 原文链接
阅读: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 原文链接
阅读:39097 评论: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 原文链接
阅读:38473 评论: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 原文链接
阅读:42708 评论: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 原文链接
阅读:37279 评论: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 原文链接
阅读:38091 评论: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 原文链接
阅读:51443 评论: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 原文链接
阅读:38799 评论:6 查看评论

Chromium扩展(Extension)的页面(Page)加载过程分析

$
0
0

       Chromium的Extension Page其实就是网页,因此它们的加载过程与普通网页相同。常见的Extension Page有Background Page和Popup Page。其中,Background Page在浏览器窗口初始化完成后自动加载,之后运行在后台中。Popup Page在用户点击地址栏右边的按钮时加载,并且显示在弹窗中。本文接下来就分析Extension Page的加载过程。

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

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

       Extension Page是加载在Extension Process中的,如图1所示:


图1 Extension的Background Page和Popup Page的加载示意图

       Extension Process实际上就是Render Process。Chromium的Content层向外提供了一个WebContents类,通过调用这个类的静态成员函数Create就可以在一个Extension Process加载一个指定的Extension Page。

       Background Page是一个特殊的网页,它的内容是空的,不过包含有一个background.js。这个background.js是在Extension的清单文件中指定的。Popup Page则与普通网页是一样的,它既可以包含有UI元素,也可以包含JavaScript脚本。

       接下来,我们就结合源代码,先分析Background Page的加载过程,再分析Popup Page的加载过程。

       Chromium的chrome模块会创建一个ChromeNotificationObserver对象,用来监听每一个新打开的浏览器窗口的NOTIFICATION_BROWSER_WINDOW_READY事件。这时候上述ChromeNotificationObserver对象的成员函数OnBrowserWindowReady会被调用,如下所示:

void ChromeNotificationObserver::OnBrowserWindowReady(Browser* browser) {
  Profile* profile = browser->profile();
  ......

  extensions::ProcessManager* manager =
      ExtensionSystem::Get(profile)->process_manager();
  ......

  manager->OnBrowserWindowReady();

  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_notification_observer.cc中。

       参数browser指向的是一个Browser对象。这个Browser对象描述的就是一个新打开的浏览器窗口,ChromeNotificationObserver类的成员函数OnBrowserWindowReady首先调用它的成员函数profile获得浏览器在启动过程中创建的Profile,然后再根据这个Profile获得一个ProcessManager对象。有了这个ProcessManager对象之后,就可以调用它的成员函数OnBrowserWindowReady,用来通知它有一个新的浏览器窗口打开了。浏览器启动时创建Profile的过程,以及根据Profile创建ProcessManager对象的过程,可以参考前面Chromium扩展(Extension)加载过程分析一文。

        ProcessManager类的成员函数OnBrowserWindowReady在执行的过程中,就会为当前加载的Extension创建Background Page,如下所示:

void ProcessManager::OnBrowserWindowReady() {
  ......

  CreateBackgroundHostsForProfileStartup();
}
       这个函数定义在文件external/chromium_org/extensions/browser/process_manager.cc中。

       ProcessManager类的成员函数OnBrowserWindowReady调用另外一个成员函数CreateBackgroundHostsForProfileStartup为当前加载的Extension创建Background Page,如下所示:

void ProcessManager::CreateBackgroundHostsForProfileStartup() {
  ......

  const ExtensionSet& enabled_extensions =
      ExtensionRegistry::Get(GetBrowserContext())->enabled_extensions();
  for (ExtensionSet::const_iterator extension = enabled_extensions.begin();
       extension != enabled_extensions.end();
       ++extension) {
    CreateBackgroundHostForExtensionLoad(this, extension->get());

    ......
  }

  ......
}
      这个函数定义在文件external/chromium_org/extensions/browser/process_manager.cc中。

      在前面Chromium扩展(Extension)加载过程分析一文提到,Chromium的Browser进程在启动的时候,会将那些状态设置为Enabled的Extension保存在一个Extension Registry的Enabled List中。ProcessManager类的成员函数CreateBackgroundHostsForProfileStartup主要就是遍历这个Enabled List中的每一个Extension,并且调用函数CreateBackgroundHostForExtensionLoad检查它们是否指定了Background Page。如果指定了,那么就会进行加载。

      函数CreateBackgroundHostForExtensionLoad的实现如下所示:

static void CreateBackgroundHostForExtensionLoad(
    ProcessManager* manager, const Extension* extension) {
  DVLOG(1) << "CreateBackgroundHostForExtensionLoad";
  if (BackgroundInfo::HasPersistentBackgroundPage(extension))
    manager->CreateBackgroundHost(extension,
                                  BackgroundInfo::GetBackgroundURL(extension));
}
       这个函数定义在文件external/chromium_org/extensions/browser/process_manager.cc中。

       函数CreateBackgroundHostForExtensionLoad首先检查参数extension描述的Extension是否指定了类型为persitent的Background Page。如果指定了,那么就会调用参数manager指向的一个ProcessManager对象的成员函数CreateBackgroundHost对它进行加载。对于非persitent的Background Page,它们只会在特定事件发生时,才会被加载。本文主要以类型为persitent的Background Page为例,说明它们的加载过程。非persitent的Background Page的加载过程,也是类似的。

       函数CreateBackgroundHostForExtensionLoad在调用ProcessManager类的成员函数CreateBackgroundHost加载一个Background Page之前,首先要获得这个Background Page的URL。这个URL是通过调用BackgroundInfo类的静态成员函数GetBackgroundURL获得的,如下所示:

GURL BackgroundInfo::GetBackgroundURL(const Extension* extension) {
  const BackgroundInfo& info = GetBackgroundInfo(extension);
  if (info.background_scripts_.empty())
    return info.background_url_;
  return extension->GetResourceURL(kGeneratedBackgroundPageFilename);
}
       这个函数定义在文件external/chromium_org/extensions/common/manifest_handlers/background_info.cc中。

       Chromium将其平台上的程序分为扩展(Extension)和应用(App)两种。两者具有相同的文件结构,在Chromium中都是通过一个Extension类描述,但是后者比前者具有更严格的权限限制。应用又分为Hosted App(托管应用)和Packaged App(打包应用)两种。Hosted App只提供一个图标和Manifest文件,并且在Manifest文件中声明了它的Popup Page和Background Page等URL。Packaged App则将Popup Page和Background Page文件打包在一起安装在本地。关于Extension、Hosted App和Packaged App的更详细描述,可以参考Chrome应用基础一文。

       我们假设参数extension描述的是一个Extension,并且是一个安装在本地的Extension。这时候它在Manifest文件实际上只是为Backgropund Page指定了Background Script,如我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文所示的Page action example:

{  
  ......  
  
  "background": {  
    "scripts": ["background.js"]  
  },  

  ......
}
       BackgroundInfo类的静态成员函数GetBackgroundURL首先会获得参数extension描述的Extension的Background Page信息。这些信息保存在一个BackgroundInfo对象中。当这个BackgroundInfo对象的成员变量background_scripts_的字符串不为空时,这个字符串描述的就是一个Background Page的Background Script,同时也表明参数extension描述的不是一个Hosted App。在这种情况下,我们就不能直接获得Background Page的URL,而是要通过调用参数extension指向的Extension对象的成员函数GetResourceURL获得。

       Extension类的成员函数GetResourceURL的实现如下所示:

class Extension : public base::RefCountedThreadSafe<Extension> {
 public:
  ......

  GURL GetResourceURL(const std::string& relative_path) const {
    return GetResourceURL(url(), relative_path);
  }

  ......
};
      这个函数定义在文件external/chromium_org/extensions/common/extension.h中。

      从前面的调用过程可以知道,参数relative_path的值为kGeneratedBackgroundPageFilename。Extension类的成员函数GetResourceURL首先调用另外一个成员函数url获得当前正在处理的Extension的URL。这个URL的形式为:chrome-extension://[extension_id]/。其中,[extension_id]为当前正在处理的Extension的ID。

      最后,Extension类的成员函数GetResourceURL将参数relative_path的值kGeneratedBackgroundPageFilename添加在前面获得的URL的后面,从而得到当前正在处理的Extension的Background Page的URL。这是通过调用Extension类的静态成员函数GetResourceURL实现的,如下所示:

// static
GURL Extension::GetResourceURL(const GURL& extension_url,
                               const std::string& relative_path) {
  ......

  std::string path = relative_path;

  // If the relative path starts with "/", it is "absolute" relative to the
  // extension base directory, but extension_url is already specified to refer
  // to that base directory, so strip the leading "/" if present.
  if (relative_path.size() > 0 && relative_path[0] == '/')
    path = relative_path.substr(1);

  GURL ret_val = GURL(extension_url.spec() + path);
  ......

  return ret_val;
}
      这个函数定义在文件external/chromium_org/extensions/common/extension.cc中。

      通过上面的分析,我们就可以知道,一个Extension如果指定了Background Page,那么这个Background Page的URL就为chrome-extension://[extension_id]/kGeneratedBackgroundPageFilename。其中,kGeneratedBackgroundPageFilename是一个常量,它的定义如下所示:

const char kGeneratedBackgroundPageFilename[] =
    "_generated_background_page.html";
      这个常量定义在文件external/chromium_org/extensions/common/constants.cc中。

      这意味着,一个Extension的Background Page的URL为:chrome-extension://[extension_id]/_generated_background_page.html。Chromium的extension模块会注册一个Chromium Extension Protocol Handler,用来处理chrome-extension协议。也就是说,当我们在浏览器的地址栏输入上述URL时Chromium Extension Protocol Handler会返回[extension_id]/_generated_background_page.html的内容给WebKit处理。这个文件的内容是动态生成的。以前面的Page action example为例,Chromium Extension Protocol Handler为它生成的_generated_background_page.html的内容如下所示:

<!DOCTYPE html>
<body>
<script src="background.js"></script>
      获得了要加载的Background Page的URL之后,回到前面分析的函数CreateBackgroundHostForExtensionLoad中,接下来它就会调用ProcessManager类的成员函数CreateBackgroundHost加载Background Page,如下所示:

bool ProcessManager::CreateBackgroundHost(const Extension* extension,
                                          const GURL& url) {
  ......

  ExtensionHost* host =
      new ExtensionHost(extension, GetSiteInstanceForURL(url), url,
                        VIEW_TYPE_EXTENSION_BACKGROUND_PAGE);
  host->CreateRenderViewSoon();
  ......
  return true;
}
       这个函数定义在文件external/chromium_org/extensions/browser/process_manager.cc中。

       ProcessManager类的成员函数CreateBackgroundHost首先是创建一个ExtensionHost对象。这个ExtensionHost对象类似于Chromium加载一个普通URL时在Browser进程中创建的RenderProcessHost对象。关于RenderProcessHost的详细描述,可以参考Chromium多进程架构简要介绍和学习计划这个系列的文章。

       ExtensionHost对象在创建的过程中,会创建一个WebContents对象,如下所示:

ExtensionHost::ExtensionHost(const Extension* extension,
                             SiteInstance* site_instance,
                             const GURL& url,
                             ViewType host_type)
    : ......,
      initial_url_(url),
      ...... {
  ......

  host_contents_.reset(WebContents::Create(
      WebContents::CreateParams(browser_context_, site_instance))),
  
  ......
}

       这个函数定义在文件external/chromium_org/extensions/browser/extension_host.cc中。

       创建出来的WebContents对象保存在ExtensionHost类的成员变量host_contents_中。同时,要加载的Background Page URL将会保存在ExtensionHost类的成员变量initial_url_中。

       回到前面分析的ProcessManager类的成员函数CreateBackgroundHost,它创建了一个ExtensionHost对象之后,接下来就会调用这个ExtensionHost对象的成员函数CreateRenderViewSoon加载指定的Background Page,如下所示:

void ExtensionHost::CreateRenderViewSoon() {
  if ((render_process_host() && render_process_host()->HasConnection())) {
    // If the process is already started, go ahead and initialize the RenderView
    // synchronously. The process creation is the real meaty part that we want
    // to defer.
    CreateRenderViewNow();
  } else {
    ProcessCreationQueue::GetInstance()->CreateSoon(this);
  }
}
       这个函数定义在文件external/chromium_org/extensions/browser/extension_host.cc中。

       ExtensionHost类的成员函数CreateRenderViewSoon首先判断用来加载指定的Background Page的Extension Process是否已经创建出来了。如果已经创建,那么就直接调用另外一个成员函数CreateRenderViewNow同步请求在这个Extension Process中加载指定的Background Page。否则的话,则需要通过调用当前进程中的一个ProcessCreationQueue单例对象的成员函数CreateSoon异步请求加载指定的的Background Page。后一种情况之所以要异步请求,是因为这种情况需要先创建一个Extension Process,然后才能加载指定的Background Page,而创建Extension Process是一个相对耗时的操作。

       在异步请求情况下,指定的Background Page同样也是通过ExtensionHost类的成员函数CreateRenderViewNow进行加载的。因此,接下来我们继续分析它的实现,如下所示:

void ExtensionHost::CreateRenderViewNow() {
  LoadInitialURL();
  ......
}
       这个函数定义在文件external/chromium_org/extensions/browser/extension_host.cc中。

       ExtensionHost类的成员函数CreateRenderViewNow调用另外一个成员函数LoadInitialURL加载指定的Background Page,如下所示:

void ExtensionHost::LoadInitialURL() {
  host_contents_->GetController().LoadURL(
      initial_url_, content::Referrer(), content::PAGE_TRANSITION_LINK,
      std::string());
}
       这个函数定义在文件external/chromium_org/extensions/browser/extension_host.cc中。

       从前面的分析可以知道,指定要加载的Background Page的URL保存在ExtensionHost类的成员变量initial_url_中。ExtensionHost类的成员函数LoadInitialURL首先通过调用成员变量host_contents_指向的WebContents对象的成员函数GetController获得一个NavigationControllerImpl对象。有了这个NavigationControllerImpl对象之后,就可以调用它的成员函数LoadURL在相应的Extension Process中加载指定的Background Page了。这个过程与加载一个普通的URL是一样的,具体可以参考前面Chromium网页Frame Tree创建过程分析一文。

       这样,我们就分析完成了Extension的Background Page的加载过程。从分析的过程可以知道,Extension的Background Page本质上是一个保存在本地的网页,因此它的加载过程与普通的URL并无异。接下来,我们继续分析Extension的Popup Page的加载过程。

       从前面Chromium扩展(Extension)机制简要介绍和学习计划一文可以知道,Browser Action和Page Action均可在地址栏右边放置一个按钮,并且在点击该按钮时,显示一个Popup Page。两者加载Popup Page的原理都是一样的,因此接下来我们以Browser Action为例,分析Popup Page的加载过程。

       当一个Extension指定了Browser Action时,Chromium将会为其创建一个BrowserActionButton。这个BrowserActionButton描述的就是Extension在地址栏右边的按钮。当用户点击这个按钮的时候,BrowserActionButton类的成员函数ButtonPressed就会被调用,如下所示:

void BrowserActionButton::ButtonPressed(views::Button* sender,
                                        const ui::Event& event) {
  delegate_->OnBrowserActionExecuted(this);
}
      这个函数定义在文件external/chromium_org/chrome/browser/ui/views/toolbar/browser_action_view.cc中。

      BrowserActionButton类的成员变量delegate_指向的是一个BrowserActionsContainer对象。这个BrowserActionsContainer对象描述的是包含Browser Action Button的容器,BrowserActionButton类的成员函数ButtonPressed调用它的成员函数OnBrowserActionExecuted,通知它在一个弹出窗口中加载一个Popup Page。

      BrowserActionsContainer类的成员函数OnBrowserActionExecuted的实现如下所示:

void BrowserActionsContainer::OnBrowserActionExecuted(
    BrowserActionButton* button) {
  ShowPopup(button, ExtensionPopup::SHOW, true);
}
     这个函数定义在文件external/chromium_org/chrome/browser/ui/views/toolbar/browser_actions_container.cc中。

     BrowserActionsContainer类的成员函数OnBrowserActionExecuted调用另外一个成员函数ShowPopup显示一个Popup Page,如下所示:

bool BrowserActionsContainer::ShowPopup(
    BrowserActionButton* button,
    ExtensionPopup::ShowAction show_action,
    bool should_grant) {
  const Extension* extension = button->extension();
  GURL popup_url;
  if (model_->ExecuteBrowserAction(
          extension, browser_, &popup_url, should_grant) !=
      extensions::ExtensionToolbarModel::ACTION_SHOW_POPUP) {
    return false;
  }

  ......

  popup_ = ExtensionPopup::ShowPopup(popup_url, browser_, reference_view,
                                     views::BubbleBorder::TOP_RIGHT,
                                     show_action);
  ......

  return true;
}
      这个函数定义在文件external/chromium_org/chrome/browser/ui/views/toolbar/browser_actions_container.cc中。

      BrowserActionsContainer类的成员函数ShowPopup首先调用成员变量model_指向的一个ExtensionToolbarModel对象的成员函数ExecuteBrowserAction获得要显示的Popup Page的URL,如下所示:

ExtensionToolbarModel::Action ExtensionToolbarModel::ExecuteBrowserAction(
    const Extension* extension,
    Browser* browser,
    GURL* popup_url_out,
    bool should_grant) {
  ......

  ExtensionAction* browser_action =
      ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension);

  ......

  if (browser_action->HasPopup(tab_id)) {
    if (popup_url_out)
      *popup_url_out = browser_action->GetPopupUrl(tab_id);
    return ACTION_SHOW_POPUP;
  }

  ......

  return ACTION_NONE;
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_toolbar_model.cc中。

       参数extension描述的就是要显示Popup Page的Extension。ExtensionToolbarModel类的成员函数ExecuteBrowserAction首先通过调用ExtensionActionManager类的静态成员函数Get获得与当前使用的Profile关联的一个ExtensionActionManager对象。有了这个ExtensionActionManager对象之后,就可以获得参数extension描述的Extension在清单文件中配置的Browser Action信息。这些信息封装在一个ExtensionAction对象中。

       获得了要显示Popup Page的Extension的Browser Action信息之后,就可以检查它是否指定了Popup Page。如果指定了,并且输出参数popup_url_out的值不等于NULL,那么ExtensionToolbarModel类的成员函数ExecuteBrowserAction就会调用前面获得的一个ExtensionAction对象的成员函数GetPopupUrl获得当前要显示的Popup Page的URL。获取到的Popup Page URL将会通过输出参数popup_url_out返回给调用者。

       接下来我们继续分析ExtensionAction类的成员函数GetPopupUrl的实现,以便了解Extension的Popup Page URL的结构,如下所示:

GURL ExtensionAction::GetPopupUrl(int tab_id) const {
  return GetValue(&popup_url_, tab_id);
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_action.cc中。

       ExtensionAction类的成员函数GetPopupUrl返回的是成员变量popup_url_的值。这个值是在ExtensionAction类的构造函数中设置的,如下所示:

ExtensionAction::ExtensionAction(const std::string& extension_id,
                                 extensions::ActionInfo::Type action_type,
                                 const extensions::ActionInfo& manifest_data)
    : extension_id_(extension_id), action_type_(action_type) {
  ......
  SetPopupUrl(kDefaultTabId, manifest_data.default_popup_url);
  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_action.cc中。

       参数manifest_data指向的是一个ActionInfo对象。这个ActionInfo对象描述的是Extension在清单文件中配置的Browser Action信息。通过这个ActionInfo对象的成员变量default_popup_url可以获得为Browser Action指定的Popup Page文件名。以前面Chromium扩展(Extension)机制简要介绍和学习计划一文中的Browser action example为例,它为Browser Action指定的Popup Page文件名为“popup.html”,如下所示:

{  
  ...... 
  
  "browser_action": {  
    "default_icon": "icon.png",  
    "default_popup": "popup.html"  
  },  
  
  ......
}  
       回到ExtensionAction类的构造函数中,它获得的Popup Page URL将会通过调用成员函数SetPopupUrl保存在成员变量popup_url_中,如下所示:

void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) {
  ......
  SetValue(&popup_url_, tab_id, url);
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_action.cc中。

       从前面的分析就可以知道,Extension的Popup Page的URL来自于ExtensionAction类的成员变量popup_url_,而后者的值又来自于ActionInfo类的成员变量default_popup_url。因此,接下来我们继续分析ActionInfo类的成员变量default_popup_url的设置过程,以便了解Extension的Popup Page URL的结构。

       ActionInfo类的成员变量default_popup_url是在加载Extension的过程中设置的。一个Extension如果指定了Browser Action,那么Chromium就会调用ActionInfo类的静态成员函数Load解析Browser Action的信息,如下所示:

scoped_ptr<ActionInfo> ActionInfo::Load(const Extension* extension,
                                        const base::DictionaryValue* dict,
                                        base::string16* error) {
  scoped_ptr<ActionInfo> result(new ActionInfo());
  ......

  // Read the action's |popup| (optional).
  const char* popup_key = NULL;
  if (dict->HasKey(keys::kPageActionDefaultPopup))
    popup_key = keys::kPageActionDefaultPopup;

  ......

  if (popup_key) {
    const base::DictionaryValue* popup = NULL;
    std::string url_str;

    if (dict->GetString(popup_key, &url_str)) {
      // On success, |url_str| is set.  Nothing else to do.
    } 
    ......

    if (!url_str.empty()) {
      // An empty string is treated as having no popup.
      result->default_popup_url = Extension::GetResourceURL(extension->url(),
                                                            url_str);
      ......
    } 
    ......
  }

  return result.Pass();
}
       这个函数定义在文件external/chromium_org/chrome/common/extensions/api/extension_action/action_info.cc中。

       ActionInfo类的静态成员函数Load首先创建一个ActionInfo对象描述当前要解析的Browser Action的信息。

       ActionInfo类的静态成员函数Load接下来检查当前要解析的Browser Action是否指定了popup属性。如果指定了,那么它的值就是一个Popup Page的文件名。这个文件名会被提取出来,保存在变量url_str中。

       有了Popup Page的文件名之后,我们还需要知道它所属的Extension的URL。这可以通过调用参数extension指向的Extension对象的成员函数url获得。前面我们提到过,一个Extension的URL的形式为:chrome-extension://[extension_id]/。其中,[extension_id]为当前正在处理的Extension的ID。

       有了Extension的URL之后,就可以将前面获得的Popup Page文件名附加在它之后,从而得到一个Popup Page的URL。这是通过调用Extension类的静态成员函数GetResourceURL实现的。Extension类的静态成员函数GetResourceURL我们在前面已经分析过,这里不再复述。

       最后,ActionInfo类的静态成员函数Load会将获得Popup Page URL保存在前面创建的ActionInfo对象的成员变量default_popup_url中。这样,我们前面分析的ExtensionAction类的构造函数就可以通过这个成员变量获得一个Popup Page的URL,并且将它保存在自己的成员变量popup_url_中了。

       这一步执行完成之后,回到前面分析的BrowserActionsContainer类的成员函数ShowPopup中,这时候就它获得了要加载的Popup Page的URL,接下来它又会通过调用ExtensionPopup类的静态成员函数ShowPopup加载该URL,如下所示:

ExtensionPopup* ExtensionPopup::ShowPopup(const GURL& url,
                                          Browser* browser,
                                          views::View* anchor_view,
                                          views::BubbleBorder::Arrow arrow,
                                          ShowAction show_action) {
  extensions::ExtensionViewHost* host =
      extensions::ExtensionViewHostFactory::CreatePopupHost(url, browser);
  ExtensionPopup* popup = new ExtensionPopup(host, anchor_view, arrow,
      show_action);

  ......

  return popup;
}
       这个函数定义在文件external/chromium_org/chrome/browser/ui/views/extensions/extension_popup.cc中。

       ExtensionPopup类的静态成员函数ShowPopup首先调用ExtensionViewHostFactory类的静态成员函数CreatePopupHost创建一个ExtensionViewHost对象。有了这个ExtensionViewHost对象之后,就可以将它封装在一个ExtensionPopup对象中。这个ExtensionPopup对象描述的就是当前要显示的Popup Page。

       ExtensionViewHostFactory类的静态成员函数CreatePopupHost在创建ExtensionViewHost对象的过程中,就会通过前面提到的WebContents接口加载Popup Page,如下所示:

ExtensionViewHost* ExtensionViewHostFactory::CreatePopupHost(const GURL& url,
                                                             Browser* browser) {
  DCHECK(browser);
  return CreateViewHost(
      url, browser->profile(), browser, VIEW_TYPE_EXTENSION_POPUP);
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_view_host_factory.cc中。

       ExtensionViewHostFactory类的静态成员函数CreatePopupHost通过调用函数CreateViewHost创建一个ExtensionViewHost对象,如下所示:

ExtensionViewHost* CreateViewHost(const GURL& url,
                                  Profile* profile,
                                  Browser* browser,
                                  extensions::ViewType view_type) {
  ......

  const Extension* extension = GetExtensionForUrl(profile, url);
  ......

  return CreateViewHostForExtension(
      extension, url, profile, browser, view_type);
}

       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_view_host_factory.cc中。

       函数CreateViewHost首先通过调用函数GetExtensionForUrl获得一个Extension对象。这个Extension对象描述的就是当前要显示Popup Page的Extension。有了这个Extension对象之后,函数CreateViewHost最后调用另外一个函数CreateViewHostForExtension创建一个ExtensionViewHost对象,如下所示:

ExtensionViewHost* CreateViewHostForExtension(const Extension* extension,
                                              const GURL& url,
                                              Profile* profile,
                                              Browser* browser,
                                              ViewType view_type) {
  ......
  ProcessManager* pm =
      ExtensionSystem::Get(profile)->process_manager();
  content::SiteInstance* site_instance = pm->GetSiteInstanceForURL(url);
  ExtensionViewHost* host =
#if defined(OS_MACOSX)
      new ExtensionViewHostMac(extension, site_instance, url, view_type);
#else
      new ExtensionViewHost(extension, site_instance, url, view_type);
#endif
  host->CreateView(browser);
  return host;
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_view_host_factory.cc中。

      从这里可以看到,在非Mac OS X平台上,函数CreateViewHostForExtension会创建一个ExtensionViewHost对象。这个ExtensionViewHost对象的创建过程,也就是ExtensionViewHost的构造函数的实现,如下所示:

ExtensionViewHost::ExtensionViewHost(
    const Extension* extension,
    content::SiteInstance* site_instance,
    const GURL& url,
    ViewType host_type)
    : ExtensionHost(extension, site_instance, url, host_type),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/extension_view_host.cc中。

       ExtensionViewHost类是从ExtensionHost类继承下来的。因此,ExtensionViewHost的构造函数会调用父类ExtensionHost的构造函数,用来执行初始化工作。前面我们在分析Extension的Background Page的加载过程时,已经分析过ExtensionHost类的构造函数了。它将会创建一个WebContents对象。有了这个WebContents对象之后,以后就可以加载这里的参数url描述的一个Popup Page了。

       这样,我们就分析完成Extension的Popup Page的加载过程了。从分析的过程就可以看出,Extension的Popup Page与Background Page都具有自己的URL。URL的形式为chrome-extension://[extension_id]/xxx.html。这些URL都是通过Chromium的Content层向外提供的API接口WebContents在一个Extension Process中加载的。

       与此同时,普通网页的URL也是通过Chromium的Content层向外提供的API接口WebContents在一个Render Process中加载的。Extension Process和Render Process的概念是一样的,都是用来加载、解析和渲染网页的。从这个角度看,Extension Page的加载、解析和渲染与一般的Web Page并无异。

       至此,我们就以Background Page和Popup Page为例,分析了Extension的Page加载过程。我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文提到,Extension由Page和Content Script组成。在接下来一篇文章中,我们将继续分析Extension的Content Script的加载过程。这样我们就更加完整地理解Extension的实现原理了。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/9/19 1:00:29 原文链接
阅读:53060 评论:7 查看评论

Chromium扩展(Extension)的Content Script加载过程分析

$
0
0

       Chromium的Extension由Page和Content Script组成。Page有UI和JS,它们加载在自己的Extension Process中渲染和执行。Content Script只有JS,这些JS是注入在宿主网页中执行的。Content Script可以访问宿主网页的DOM Tree,从而可以增强宿主网页的功能。本文接下来分析Content Script注入到宿主网页执行的过程。

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

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

       我们可以在Extension的清单文件中指定Content Script对哪些网页有兴趣,如前面Chromium扩展(Extension)机制简要介绍和学习计划一文的Page action example所示:

{
  ......

  "content_scripts": [
    {
      "matches": ["https://fast.com/"],
      "js": ["content.js"],
      "run_at": "document_start",
      "all_frames": true
    }
  ]
}

       这个清单文件仅对URL为“https://fast.com/”的网页感兴趣。当这个网页在Chromium中加载的时候,Chromium就会往里面注入脚本content.js。注入过程如图1所示:


图1 Content Script注入到宿主网页执行的过程

       首先,在前面Chromium扩展(Extension)加载过程分析一文提到,Browser进程在加载Extension之前,会创建一个UserScriptMaster对象。此后每当加载一个Extension,这个UserScriptMaster对象的成员函数OnExtensionLoaded都会被调用,用来收集当前正在加载的Extension的Content Script。

       此后,每当Browser进程启动一个Render进程时,代表该Render进程的一个RenderProcessHostImpl对象的成员函数OnProcessLaunched都会被调用,用来通知Browser进程新的Render进程已经启动起来的。这时候这个RenderProcessHostImpl对象会到上述UserScriptMaster对象中获取当前收集到的所有Content Script。这些Content Script接下来会通过一个类型为ExtensionMsg_UpdateUserScript的IPC消息传递给新启动的Render进程。新启动的Render进程通过一个Dispatcher对象接收这个IPC消息,并且会将它传递过来的Content Script保存在一个UserScriptSlave对象中。

       接下来,每当Render进程加载一个网页时,都会在三个时机检查是否需要在该网页中注入Content Script。从前面Chromium扩展(Extension)机制简要介绍和学习计划一文可以知道,这三个时机分别为document_start、document_end和document_idle,分别表示网页的Document对象开始创建、结束创建以及空闲时。接下来我们以document_start这个时机为例,说明Content Script注入到宿主网页的过程。

       网页的Document对象是在WebKit中创建的。WebKit为网页创建了Document对象之后,会调用Content层的一个RenderFrameImpl对象的成员函数didCreateDocumentElement,用来通知后者,它描述的网页的Document对象已经创建好了。这时候这个RenderFrameImpl对象将会调用前面提到的UserScriptSlave对象的成员函数InjectScripts,用来通知后者,现在可以将Content Script注入当前正在加载的网页中去执行。前面提到的UserScriptSlave对象会调用另外一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld,用来注入符合条件的Content Script到当前正在加载的网页中去,并且在JS引擎的一个Isolated World中执行。Content Script在Isolated World中执行,意味着它不可以访问在宿主网页中定义的JavaScript,包括不能调用在宿主网页中定义的JavaScript函数,以及访问宿主网页中定义的变量。

       以上就是Content Script注入到宿主网页中执行的大概流程。接下来我们结合源代码进行详细的分析,以便对这个注入流程有更深刻的认识。

       我们首先分析UserScriptMaster类收集Content Script的过程。这要从UserScriptMaster类的构造函数说起,如下所示:

UserScriptMaster::UserScriptMaster(Profile* profile)
    : ......,
      extension_registry_observer_(this) {
  extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
  registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY,
                 content::Source<profile>(profile_));
  registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED,
                 content::NotificationService::AllBrowserContextsAndSources());
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       UserScriptMaster类的成员变量extension_registry_observer_描述的是一个ExtensionRegistryObserver对象。这个ExtensionRegistryObserver对象接下来会注册到与参数profile描述的一个Profile对象关联的一个Extension Registry对象中去,也就是注入到与当前使用的Profile关联的一个Extension Registry对象中去。这个Extension Registry对象的创建过程可以参考前面Chromium扩展(Extension)加载过程分析一文。

       与此同时,UserScriptMaster类的构造函数还会通过成员变量registrar_描述的一个NotificationRegistrar对象监控chrome::NOTIFICATION_EXTENSIONS_READY和content::NOTIFICATION_RENDERER_PROCESS_CREATED事件。其中,事件chrome::NOTIFICATION_EXTENSIONS_READY用来通知Chromium的Extension Service已经启动了,而事件content::NOTIFICATION_RENDERER_PROCESS_CREATED用来通知有一个新的Render进程启动起来。如前面所述,当新的Render进程启动起来的时候,UserScriptMaster类会将当前加载的Extension定义的Content Script传递给它处理。这个过程我们在后面会进行详细分析。

       从前面Chromium扩展(Extension)加载过程分析一文还可以知道,接下来加载的每一个Extension,都会保存在上述Extension Registry对象内部的一个Enabled List中,并且都会调用注册在上述Extension Registry对象中的每一个ExtensionRegistryObserver对象的成员函数OnExtensionLoaded,通知它们有一个新的Extension被加载。

       当UserScriptMaster类的成员变量extension_registry_observer_描述的ExtensionRegistryObserver对象的成员函数OnExtensionLoaded被调用时,它又会调用UserScriptMaster类的成员函数OnExtensionLoaded,以便UserScriptMaster类可以收集新加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  // Add any content scripts inside the extension.
  extensions_info_[extension->id()] =
      ExtensionSet::ExtensionPathAndDefaultLocale(
          extension->path(), LocaleInfo::GetDefaultLocale(extension));
  ......
  const UserScriptList& scripts =
      ContentScriptsInfo::GetContentScripts(extension);
  for (UserScriptList::const_iterator iter = scripts.begin();
       iter != scripts.end();
       ++iter) {
    user_scripts_.push_back(*iter);
    .....
  }
  if (extensions_service_ready_) {
    changed_extensions_.insert(extension->id());
    if (script_reloader_.get()) {
      pending_load_ = true;
    } else {
      StartLoad();
    }
  }
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       参数extension描述的就是当前正在加载的Extension。UserScriptMaster类的成员函数OnExtensionLoaded首先会调用ExtensionSet类的静态成员函数ExtensionPathAndDefaultLocale将该Extension的Path和Locale信息封装在一个ExtensionPathAndDefaultLocale对象中,并且以该Extension的ID为键值,将上述ExtensionPathAndDefaultLocale对象保存在成员变量extensions_info_描述的一个std::map中。

       UserScriptMaster类的成员函数OnExtensionLoaded接下来将当前正在加载的Extension定义的所有Content Script保存在成员变量user_scripts_描述的一个std::vector中。

       UserScriptMaster类有一个类型为bool的成员变量extensions_service_ready_。当它的值等于true的时候,表示Chromium的Extension Service已经启动起来了。这时候extensions_service_ready_就会将当前正在加载的Extension的ID插入到UserScriptMaster类的成员变量changed_extensions_描述的一个std::set中去,表示有一个新的Extension需要处理。这里说的处理,就是将新加载的Extension定义的Content Script的内容读取出来,并且保存在一个共享内存中。

       将Extension定义的Content Script的内容读取出来,并且保存在一个共享内存中,是通过UserScriptMaster类的成员变量script_reloader_指向的一个ScriptReloader对象实现的。如果这个ScriptReloader已经创建出来,那么就表示它现在正在读取Content Script的过程中。这时候UserScriptMaster类的成员变量pending_load_的值会被设置为true,表示当前需要读取的Content Script发生了变化,因此需要重新进行读取。

       如果UserScriptMaster类的成员变量script_reloader_指向的ScriptReloader对象还没有创建出来,那么UserScriptMaster类的成员函数OnExtensionLoaded就会调用另外一个成员函数StartLoad创建该ScriptReloader对象,并且通过该ScriptReloader对象读取当前已经加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::StartLoad() {
  if (!script_reloader_.get())
    script_reloader_ = new ScriptReloader(this);

  script_reloader_->StartLoad(user_scripts_, extensions_info_);
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       从这里可以看到,如果UserScriptMaster类的成员变量script_reloader_指向的ScriptReloader对象还没有创建出来,那么UserScriptMaster类的成员函数StartLoad就会创建,并且在创建之后,调用它的成员函数StartLoad读取当前已经加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::ScriptReloader::StartLoad(
    const UserScriptList& user_scripts,
    const ExtensionsInfo& extensions_info) {
  // Add a reference to ourselves to keep ourselves alive while we're running.
  // Balanced by NotifyMaster().
  AddRef();

  ......
  this->extensions_info_ = extensions_info;
  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      base::Bind(
          &UserScriptMaster::ScriptReloader::RunLoad, this, user_scripts));
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       ScriptReloader类的成员函数StartLoad首先调用成员函数AddRef增加当前正在处理的ScriptReloader对象的引用计数,避免它在读取Content Script的过程中被销毁。

       从前面的调用过程可以知道,参数user_scripts描述的是一个std::vector。这个std::vector保存在当前已经加载的Extension定义的Content Script。另外一个参数extension_info指向的是一个std::map。这个std::map描述了当前加载的所有Extension。

       ScriptReloader类的成员函数StartLoad将参数extension_info指向的std::map保存在自己的成员变量extension_info_之后,就向Browser进程中专门用来执行文件读写操作的BrowserThread::FILE线程的消息队列发送一个Task。这个Task绑定了ScriptReloader类的成员函数RunLoad。

       这意味着ScriptReloader类的成员函数RunLoad接下来会在BrowserThread::FILE线程被调用,用来读取保存在参数user_scripts描述的std::vector中的Content Script,如下所示:

// This method will be called on the file thread.
void UserScriptMaster::ScriptReloader::RunLoad(
    const UserScriptList& user_scripts) {
  LoadUserScripts(const_cast<UserScriptList*>(&user_scripts));

  // Scripts now contains list of up-to-date scripts. Load the content in the
  // shared memory and let the master know it's ready. We need to post the task
  // back even if no scripts ware found to balance the AddRef/Release calls.
  BrowserThread::PostTask(master_thread_id_,
                          FROM_HERE,
                          base::Bind(&ScriptReloader::NotifyMaster,
                                     this,
                                     base::Passed(Serialize(user_scripts))));
}

      这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

      ScriptReloader类的成员函数RunLoad首先调用成员函数LoadUserScripts读取当前已经加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::ScriptReloader::LoadUserScripts(
    UserScriptList* user_scripts) {
  for (size_t i = 0; i < user_scripts->size(); ++i) {
    UserScript& script = user_scripts->at(i);
    scoped_ptr<SubstitutionMap> localization_messages(
        GetLocalizationMessages(script.extension_id()));
    for (size_t k = 0; k < script.js_scripts().size(); ++k) {
      UserScript::File& script_file = script.js_scripts()[k];
      if (script_file.GetContent().empty())
        LoadScriptContent(
            script.extension_id(), &script_file, NULL, verifier_.get());
    }
    for (size_t k = 0; k < script.css_scripts().size(); ++k) {
      UserScript::File& script_file = script.css_scripts()[k];
      if (script_file.GetContent().empty())
        LoadScriptContent(script.extension_id(),
                          &script_file,
                          localization_messages.get(),
                          verifier_.get());
    }
  }
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

      参数user_srcipts描述的std::vector里面保存的是一系列的UserScript对象。每一个UserScript对象里面包含若干个Content Script文件。每一个Content Script文件都是通过一个UserScript::File对象描述。注意,这些Content Script有可能是JavaScript,也有可能是CSS Script。这意味着Extension不仅可以注入JavaScript到宿主网页中,还可以注入CSS Script。

      ScriptReloader类的成员函数LoadUserScripts依次调用函数LoadScriptContent读取这些Content Script文件的内容,如下所示:

static bool LoadScriptContent(const std::string& extension_id,
                              UserScript::File* script_file,
                              const SubstitutionMap* localization_messages,
                              ContentVerifier* verifier) {
  std::string content;
  const base::FilePath& path = ExtensionResource::GetFilePath(
      script_file->extension_root(), script_file->relative_path(),
      ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT);
  if (path.empty()) {
    ......
  } else {
    if (!base::ReadFileToString(path, &content)) {
      LOG(WARNING) << "Failed to load user script file: " << path.value();
      return false;
    }
    ......
  }

  ......

  // Remove BOM from the content.
  std::string::size_type index = content.find(base::kUtf8ByteOrderMark);
  if (index == 0) {
    script_file->set_content(content.substr(strlen(base::kUtf8ByteOrderMark)));
  } else {
    script_file->set_content(content);
  }

  return true;
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       函数LoadScriptContent首先调用ExtensionResource类的静态成员函数GetFilePath获得要读取的Content Script的文件路径,然后再调用函数base::ReadFileToString读取该文件的内容。这样就可以获得要读取的Content Script的内容,这些内容最终又会保存在参数script_file描述的一个UserScript::File对象的内部。

       这一步执行完成后,Chromium就获得了当前已经加载的Extension所定义的Content Script的内容。这些内容保存在每一个Content Script对应的UserScript::File对象中。回到前面分析的ScriptReloader类的成员函数RunLoad中,接下来它调用函数Serialize将前面读取的Content Script的内容保存在一块共享内存中,如下所示:

// Pickle user scripts and return pointer to the shared memory.
static scoped_ptr<base::SharedMemory> Serialize(const UserScriptList& scripts) {
  Pickle pickle;
  pickle.WriteUInt64(scripts.size());
  for (size_t i = 0; i < scripts.size(); i++) {
    const UserScript& script = scripts[i];
    // TODO(aa): This can be replaced by sending content script metadata to
    // renderers along with other extension data in ExtensionMsg_Loaded.
    // See crbug.com/70516.
    script.Pickle(&pickle);
    // Write scripts as 'data' so that we can read it out in the slave without
    // allocating a new string.
    for (size_t j = 0; j < script.js_scripts().size(); j++) {
      base::StringPiece contents = script.js_scripts()[j].GetContent();
      pickle.WriteData(contents.data(), contents.length());
    }
    for (size_t j = 0; j < script.css_scripts().size(); j++) {
      base::StringPiece contents = script.css_scripts()[j].GetContent();
      pickle.WriteData(contents.data(), contents.length());
    }
  }

  // Create the shared memory object.
  base::SharedMemory shared_memory;

  base::SharedMemoryCreateOptions options;
  options.size = pickle.size();
  options.share_read_only = true;
  if (!shared_memory.Create(options))
    return scoped_ptr<base::SharedMemory>();

  if (!shared_memory.Map(pickle.size()))
    return scoped_ptr<base::SharedMemory>();

  // Copy the pickle to shared memory.
  memcpy(shared_memory.memory(), pickle.data(), pickle.size());

  base::SharedMemoryHandle readonly_handle;
  if (!shared_memory.ShareReadOnlyToProcess(base::GetCurrentProcessHandle(),
                                            &readonly_handle))
    return scoped_ptr<base::SharedMemory>();

  return make_scoped_ptr(new base::SharedMemory(readonly_handle,
                                                /*read_only=*/true));
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       函数Serialize依次遍历保存在参数scripts描述的一个std::vector中的每一个UserScript对象,并且将这些UserScript对象包含的Content Script写入到本地变量pickle描述一个Pickle对象中。从前面Chromium的IPC消息发送、接收和分发机制分析一文可以知道,Pickle类是Chromium定义的一种IPC消息格式,它将数据按照一定的格式打包在一块内存中。

       函数Serialize将Content Script写入到本地变量pickle描述的Pickle对象中去之后,接下来又会创建一块共享内存。这块共享内存通过本地变量shared_memory描述的一个SharedMemory对象描述。有了这块共享内存之后,函数Serialize就会将Content Script的内容从本地变量pickle描述的Pickle对象中拷贝到它里面去。

       函数Serialize最后获得已经写入了Content Script的共享内存的只读版本,并且将这个只读版本封装在另外一个SharedMemory对象中返回给调用者,以便调用者以后可以将它传递给宿主网页所在的Render进程进行只读访问。

       这一步执行完成后,Chromium就获得了一块只读的共享内存,这块共享内存保存了当前已经加载的Extension所定义的Content Script的内容。回到前面分析的ScriptReloader类的成员函数RunLoad中,接下来它又会向成员变量master_thread_id_描述的线程的消息队列发送一个Task。这个Task绑定了ScriptReloader类的成员函数NotifyMaster,用来通知其内部引用的一个UserScriptMaster对象,当前已经加载的Extension所定义的Content Script的内容已经读取完毕。

       ScriptReloader类的成员函数NotifyMaster的实现如下所示:

void UserScriptMaster::ScriptReloader::NotifyMaster(
    scoped_ptr<base::SharedMemory> memory) {
  // The master could go away
  if (master_)
    master_->NewScriptsAvailable(memory.Pass());

  // Drop our self-reference.
  // Balances StartLoad().
  Release();
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

      ScriptReloader类内部引用的UserScriptMaster对象保存在成员变量master_中。ScriptReloader类的成员函数NotifyMaster首先调用这个UserScriptMaster对象的成员函数NewScriptsAvailable,通知它已经加载的Extension所定义的Content Script的内容已经读取完毕。

      通知完成后,ScriptReloader类的成员函数NotifyMaster还会调用成员函数Release减少当前正在处理的ScriptReloader对象的引用计数,用来平衡在读取Content Script之前,对该ScriptReloader对象的引用计数的增加。

      接下来我们继续分析UserScriptMaster类的成员函数NewScriptsAvailable的实现,以便了解它是如何处理当前已经加载的Extension的Content Script的内容的,如下所示:

void UserScriptMaster::NewScriptsAvailable(
    scoped_ptr<base::SharedMemory> handle) {
  if (pending_load_) {
    // While we were loading, there were further changes.  Don't bother
    // notifying about these scripts and instead just immediately reload.
    pending_load_ = false;
    StartLoad();
  } else {
    ......

    // We've got scripts ready to go.
    shared_memory_ = handle.Pass();

    for (content::RenderProcessHost::iterator i(
            content::RenderProcessHost::AllHostsIterator());
         !i.IsAtEnd(); i.Advance()) {
      SendUpdate(i.GetCurrentValue(),
                 shared_memory_.get(),
                 changed_extensions_);
    }
    ......
  }
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       UserScriptMaster类的成员函数NewScriptsAvailable首先判断成员变量pending_load_的值是否等于true。如果等于true,那么就说明前面在读取Content Script的过程中,又有新的Extension被加载。这时候UserScriptMaster类的成员函数会调用前面分析过的成员函数StartLoad对Content Script进行重新读取。

       我们假设这时候UserScriptMaster类的成员变量pending_load_的值等于false。在这种情况下,UserScriptMaster类的成员函数NewScriptsAvailable首先将参数handle描述的共享内存保存在成员变量shared_memory_中,接下来又会将这块共享内存传递给当前已经启动的Render进程,这是通过调用成员函数SendUpdate实现的。

       我们假设当前还没有Render进程启动起来。前面分析UserScriptMaster类的构造函数的实现时提到,UserScriptMaster类会监控Render进程启动事件,也就是content::NOTIFICATION_RENDERER_PROCESS_CREATED事件。以后每当有一个Render进程启动完成,UserScriptMaster类的成员函数Observe就会被调用。UserScriptMaster类的成员函数Observe在调用的过程中,就会将前面已经加载的Extension的Content Script发送给新启动的Render进程,以便后者可以将Content Script注入到它后续加载的网页中去。

      接下来我们就从Render进程启动完成后开始,分析UserScriptMaster类将Content Script传递给Render进程的过程。从前面Chromium的Render进程启动过程分析一文可以知道,Render进程是由Browser进程启动的。Browser进程又是通过ChildProcessLauncher::Context类启动Render进程的。当Render进程启动完成后,ChildProcessLauncher::Context类的静态成员函数OnChildProcessStarted就会被调用,它的实现如下所示:

class ChildProcessLauncher::Context
    : public base::RefCountedThreadSafe<ChildProcessLauncher::Context> {
 public:
  ......

  static void OnChildProcessStarted(
      // |this_object| is NOT thread safe. Only use it to post a task back.
      scoped_refptr<Context> this_object,
      BrowserThread::ID client_thread_id,
      const base::TimeTicks begin_launch_time,
      base::ProcessHandle handle) {
    RecordHistograms(begin_launch_time);
    if (BrowserThread::CurrentlyOn(client_thread_id)) {
      // This is always invoked on the UI thread which is commonly the
      // |client_thread_id| so we can shortcut one PostTask.
      this_object->Notify(handle);
    } else {
      BrowserThread::PostTask(
          client_thread_id, FROM_HERE,
          base::Bind(
              &ChildProcessLauncher::Context::Notify,
              this_object,
              handle));
    }
  }

  ......
};
      这个函数定义在文件external/chromium_org/content/browser/child_process_launcher.cc中。

      参数client_thread_id描述的是当初请求启动Render进程的线程的ID。另外一个参数this_object描述的是用来启动Render进程的一个ChildProcessLauncher::Context对象。这个ChildProcessLauncher::Context对象就是在请求启动Render进程的线程中创建的。

      ChildProcessLauncher::Context类的静态成员函数OnChildProcessStarted首先检查当前线程是否就是当初请求启动Render进程的线程。如果是的话,那么就直接调用参数this_object描述的ChildProcessLauncher::Context对象的成员函数Notify,用来通知它请求的Render进程已经启动完成了。否则的话,会向当初请求启动Render进程的线程的消息队列发送一个Task,然后在这个Task执行的时候,再调用参数this_object描述的ChildProcessLauncher::Context对象的成员函数Notify。通过这种方式,保证参数this_object描述的ChildProcessLauncher::Context对象总是在创建它的线程中使用。这样可以避免在多线程环境下,访问对象(调用对象的成员函数)需要加锁的问题(加锁会引入竞争,竞争会带来不确定的延时)。这是Chromium多线程编程哲学所要遵循的原则之一。关于Chromium多线程编程哲学,可以参考前面Chromium多线程模型设计和实现分析一文。

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

class ChildProcessLauncher::Context
    : public base::RefCountedThreadSafe<ChildProcessLauncher::Context> {
  ......

 private:
  ......

  void Notify(
#if defined(OS_POSIX) && !defined(OS_MACOSX) && !defined(OS_ANDROID)
      bool zygote,
#endif
      base::ProcessHandle handle) {
    ......

    if (client_) {
      if (handle) {
        client_->OnProcessLaunched();
      } else {
        client_->OnProcessLaunchFailed();
      }
    } 

    ......
  }

  ......
};
       这个函数定义在文件external/chromium_org/content/browser/child_process_launcher.cc中。

       ChildProcessLauncher::Context类的成员变量client_指向的是一个RenderProcessHostImpl对象。这个RenderProcessHostImpl对象是在Browser进程中描述它启动的一个Render进程的。通过这个RenderProcessHostImpl对象,可以与Render进程进行IPC。

       参数handle描述的就是前面请求启动的Render进程的句柄。当这个句柄值不等于0时,就表示请求的Render进程已经成功地启动起来了。这时候ChildProcessLauncher::Context类的成员函数就会调用成员变量client_指向的RenderProcessHostImpl对象的成员函数OnProcessLaunched,以便它可以发出一个content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知,如下所示:

void RenderProcessHostImpl::OnProcessLaunched() {
  ......

  NotificationService::current()->Notify(
      NOTIFICATION_RENDERER_PROCESS_CREATED,
      Source<RenderProcessHost>(this),
      NotificationService::NoDetails());

  ......
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。

      从前面的分析可以知道,一旦RenderProcessHostImpl对象的成员函数OnProcessLaunched发出content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知,UserScriptMaster类的成员函数Observe就会被调用,如下所示:

void UserScriptMaster::Observe(int type,
                               const content::NotificationSource& source,
                               const content::NotificationDetails& details) {
  ......

  switch (type) {
    ......
    case content::NOTIFICATION_RENDERER_PROCESS_CREATED: {
      content::RenderProcessHost* process =
          content::Source<content::RenderProcessHost>(source).ptr();
      ......
      if (ScriptsReady()) {
        SendUpdate(process,
                   GetSharedMemory(),
                   std::set<std::string>());  // Include all extensions.
      }
      break;
    }
    ......
  }

  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       UserScriptMaster类的成员函数Observe在处理content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知的时候,首先会调用成员函数ScriptsReady检查当前加载的Extension的Content Script是否已经读取出来,并且保存在内部维护的一块共享内存中去了。如果是的话,那么就会继续调用另外一个成员函数SendUpdate将这块共享内存传递给当前启动完成的Render进程。

       UserScriptMaster类的成员函数SendUpdate的实现如下所示:

void UserScriptMaster::SendUpdate(
    content::RenderProcessHost* process,
    base::SharedMemory* shared_memory,
    const std::set<std::string>& changed_extensions) {
  ......

  base::SharedMemoryHandle handle_for_process;
  if (!shared_memory->ShareToProcess(handle, &handle_for_process))
    return;  // This can legitimately fail if the renderer asserts at startup.

  if (base::SharedMemory::IsHandleValid(handle_for_process)) {
    process->Send(new ExtensionMsg_UpdateUserScripts(handle_for_process,
                                                     changed_extensions));
  }
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       参数shared_memory描述的共享内存就是要发送给参数process描述的Render进程的,它里面包含了当前加载的Extension的Content Script。UserScriptMaster类的成员函数SendUpdate将这块共享封装在一个类型为ExtensionMsg_UpdateUserScripts的IPC消息中,并且发送给参数process描述的Render进程。

       Render进程在启动的时候会创建一个Dispatcher对象。这个Dispatcher对象会通过成员函数OnControlMessageReceived接收类型为ExtensionMsg_UpdateUserScripts的IPC消息,如下所示:

bool Dispatcher::OnControlMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(Dispatcher, message)
  ......
  IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateUserScripts, OnUpdateUserScripts)
  ......
  IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  return handled;
}
      这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

      Dispatcher类的成员函数OnControlMessageReceived将类型为ExtensionMsg_UpdateUserScripts的IPC消息分发给另外一个成员函数OnUpdateUserScripts处理,如下所示:

void Dispatcher::OnUpdateUserScripts(
    base::SharedMemoryHandle scripts,
    const std::set<std::string>& extension_ids) {
  ......

  user_script_slave_->UpdateScripts(scripts, extension_ids);
  ......
}
      这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

      Dispatcher类的成员变量user_script_slave_指向的是一个UserScriptSlave对象。Dispatcher类的成员函数OnUpdateUserScripts调用这个UserScriptSlave对象的成员函数UpdateScripts将参数scripts描述的共享内存包含的Content Script交给它处理。处理过程如下所示:

bool UserScriptSlave::UpdateScripts(
    base::SharedMemoryHandle shared_memory,
    const std::set<std::string>& changed_extensions) {
  ......

  // Unpickle scripts.
  uint64 num_scripts = 0;
  Pickle pickle(reinterpret_cast<char*>(shared_memory_->memory()), pickle_size);
  PickleIterator iter(pickle);
  CHECK(pickle.ReadUInt64(&iter, &num_scripts));
  ......

  // If we pass no explicit extension ids, we should refresh all extensions.
  bool include_all_extensions = changed_extensions.empty();
  ......
  
  if (include_all_extensions) {
    script_injections_.clear();
  } 

  ......

  for (uint64 i = 0; i < num_scripts; ++i) {
    scoped_ptr<UserScript> script(new UserScript());
    script->Unpickle(pickle, &iter);
    ......
    
    for (size_t j = 0; j < script->js_scripts().size(); ++j) {
      const char* body = NULL;
      int body_length = 0;
      CHECK(pickle.ReadData(&iter, &body, &body_length));
      script->js_scripts()[j].set_external_content(
          base::StringPiece(body, body_length));
    }
    for (size_t j = 0; j < script->css_scripts().size(); ++j) {
      const char* body = NULL;
      int body_length = 0;
      CHECK(pickle.ReadData(&iter, &body, &body_length));
      script->css_scripts()[j].set_external_content(
          base::StringPiece(body, body_length));
    }

    // If we include all extensions or the given extension changed, we add a
    // new script injection.
    if (include_all_extensions ||
        changed_extensions.count(script->extension_id()) > 0) {
      script_injections_.push_back(new ScriptInjection(script.Pass(), this));
    } 

    ......
  }
  return true;
}
       这个函数定义在文件external/chromium_org/extensions/renderer/user_script_slave.cc中。

       UserScriptSlave类的成员函数UpdateScripts会通过本地变量pickle描述的Pickle对象解析和读取包含在参数shared_memory中的Content Script。这些Content Script将会保存在UserScriptSlave类的成员变量script_injections_描述的一个Vector中。

       当参数changed_extensions描述的字符串不等于空时,它的值就表示Content Script内容发生过变化的Extension。这时候UserScriptSlave类可以对内部维护的Content Script进行增量更新。从前面的调用过程可以知道,在我们这种情景中,参数changed_extensions描述的字符串等于空。在这种情况下,UserScriptSlave类将会对内部维护的Content Script进行全部更新。也就是先清空成员变量script_injections_描述的Vector,然后再将包含在参数shared_memory中的Content Script增加到这个Vector中去。

       从前面分析的函数Serialize可以知道,包含在参数shared_memory中的Content Script是按照Extension进行组织的。一个Extension对应一个UserScript对象。一个UserScript对象又可以包含若干个Content Script。这些Content Script又可能同时包含有JavaScript和CSS Script。UserScriptSlave类的成员函数UpdateScripts通过本地变量pickle描述的Pickle对象依次获得每一个Extension的Content Script,并且封装在一个ScriptInjection对象中。这些ScriptInjection对象会保存在UserScriptSlave类的成员变量script_injections_描述的一个Vector中。

       这一步执行完成后,每一个Render进程就会获得当前加载的所有Extension定义的Content Script。这些Content Script由一个UserScriptSlave对象维护。以后每当Render进程加载一个网页,就会询问这个UserScriptSlave对象,是否需要往里面注入相应的Content Script。

       接下来,我们以前面提到的Page action example的Content Script注入到URL为“https://fast.com/”的网页为例,分析Content Script注入宿主网页执行的过程。从Page action example的Content Script的定义可以知道,它是在URL为“https://fast.com/”的网页的Document对象创建时注入的。

       前面提到,网页的Document对象是在WebKit中创建的。WebKit为网页创建了Document对象之后,会调用Content层的一个RenderFrameImpl对象的成员函数didCreateDocumentElement,用来通知后者,它描述的网页的Document对象已经创建好了,如下所示:

void RenderFrameImpl::didCreateDocumentElement(blink::WebLocalFrame* frame) {
  ......

  FOR_EACH_OBSERVER(RenderViewObserver, render_view_->observers(),
                    DidCreateDocumentElement(frame));
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       RenderFrameImpl类的成员变量render_view_指向的是一个RenderViewImpl对象。这个RenderViewImpl对象的内部维护有一系列的Render View Observer。这些Render View Observer用来监听WebKit事件。这样,如果一个模块要监听某一个网页的WebKit事件,那么就往与该网页对应的RenderViewImpl对象内部注册一个Render View Observer即可。

       Chromium的Extension模块会监听所有网页的WebKit事件,这是通过向与这些网页对应的RenderViewImpl对象内部注册一个类型为ExtensionHelper的Render View Observer实现的。这意味着当WebKit发出事件通知时,ExtensionHelper类的相应成员函数会被调用。在我们这个情景中,就是当网页的Document对象已经创建出来时,ExtensionHelper类的成员函数DidCreateDocumentElement会被调用。在调用的过程中,它就会往当前加载的网页注入那些运行时机为“document_start”的Content Script,如下所示:

void ExtensionHelper::DidCreateDocumentElement(WebLocalFrame* frame) {
  dispatcher_->user_script_slave()->InjectScripts(
      frame, UserScript::DOCUMENT_START);
  ......
}
      这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

      ExtensionHelper类的成员变量dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象就是前面分析的用来处理从Browser进程发送过来的类型为ExtensionMsg_UpdateUserScripts的IPC消息的Dispatcher对象。ExtensionHelper类的成员函数DidCreateDocumentElement首先调用这个Dispatcher对象的成员函数user_script_slave获得它内部维护的一个UserScriptSlave对象。有了这个UserScriptSlave对象之后,就可以调用它的成员函数InjectScripts向当前加载的网页注入那些运行时机为“document_start”的Content Script,如下所示:

void UserScriptSlave::InjectScripts(WebFrame* frame,
                                    UserScript::RunLocation location) {
  GURL document_url = ScriptInjection::GetDocumentUrlForFrame(frame);
  ......

  ScriptInjection::ScriptsRunInfo scripts_run_info;
  for (ScopedVector<ScriptInjection>::const_iterator iter =
           script_injections_.begin();
       iter != script_injections_.end();
       ++iter) {
    (*iter)->InjectIfAllowed(frame, location, document_url, &scripts_run_info);
  }

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

       参数frame描述的就是当前加载的网页。UserScriptSlave类的成员函数InjectScripts首先通过调用ScriptInjection类的静态成员函数GetDocumentUrlForFrame获得这个网页的URL。有了这个URL之后,UserScriptSlave类的成员函数InjectScripts接下来就会遍历保存在成员变量script_injections_描述的Vector中的每一个ScriptInjection对象,并且调用这些ScriptInjection对象的成员函数InjectIfAllowed。

       从前面的分析可以知道,保存在UserScriptSlave类的成员变量script_injections_中的ScriptInjection对象描述的就是当前加载的Extension的Content Script。这些ScriptInjection对象的成员函数InjectIfAllowed在执行期间,就会判断是否需要将自己描述的Content Script注入到当前加载的网页中去执行,也就是前面获得的URL对应的网页。

       接下来我们就继续分析ScriptInjection类的成员函数InjectIfAllowed的实现,以便了解Content Script注入到宿主网页执行的过程,如下所示:

void ScriptInjection::InjectIfAllowed(blink::WebFrame* frame,
                                      UserScript::RunLocation run_location,
                                      const GURL& document_url,
                                      ScriptsRunInfo* scripts_run_info) {
  if (!WantsToRun(frame, run_location, document_url))
    return;

  const Extension* extension = user_script_slave_->GetExtension(extension_id_);
  DCHECK(extension);  // WantsToRun() should be false if there's no extension.

  // We use the top render view here (instead of the render view for the
  // frame), because script injection on any frame requires permission for
  // the top frame. Additionally, if we have to show any UI for permissions,
  // it should only be done on the top frame.
  content::RenderView* top_render_view =
      content::RenderView::FromWebView(frame->top()->view());

  int tab_id = ExtensionHelper::Get(top_render_view)->tab_id();

  // By default, we allow injection.
  bool should_inject = true;

  // Check if the extension requires user consent for injection *and* we have a
  // valid tab id (if we don't have a tab id, we have no UI surface to ask for
  // user consent).
  if (tab_id != -1 &&
      extension->permissions_data()->RequiresActionForScriptExecution(
          extension, tab_id, frame->top()->document().url())) {
    int64 request_id = kInvalidRequestId;
    int page_id = top_render_view->GetPageId();

    // We only delay the injection if the feature is enabled.
    // Otherwise, we simply treat this as a notification by passing an invalid
    // id.
    if (FeatureSwitch::scripts_require_action()->IsEnabled()) {
      should_inject = false;
      ScopedVector<PendingInjection>::iterator pending_injection =
          pending_injections_.insert(
              pending_injections_.end(),
              new PendingInjection(frame, run_location, page_id));
      request_id = (*pending_injection)->id;
    }

    top_render_view->Send(
        new ExtensionHostMsg_RequestContentScriptPermission(
            top_render_view->GetRoutingID(),
            extension->id(),
            page_id,
            request_id));
  }

  if (should_inject)
    Inject(frame, run_location, scripts_run_info);
}
      这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。

      ScriptInjection类的成员函数InjectIfAllowed首先调用成员函数WantsToRun判断当前加载的网页是否是当前正在处理的Content Script感兴趣的网页,也就是判断当前加载的网页的URL是否匹配Content Script在其Extension的清单文件设置的URL规则。如果不匹配,那么就说明不需要将当前正在处理的Content Script注入到当前加载的网页中执行。

      如果匹配,ScriptInjection类的成员函数InjectIfAllowed还会进一步检查当前正在处理的Content Script所在的Extension是否对当前正在加载的网页申请了Permission。如果没有申请,并且Chromium启用了Scripts Require Action Feature,那么就需要用户同意后,才能将当前正在处理的Content Script注入到当前加载的网页中执行。这个需要用户同意的操作是通过向Browser进程发出一个类型ExtensionHostMsg_RequestContentScriptPermission的IPC消息触发的。

      我们假设当前加载的网页是当前正在处理的Content Script感兴趣的网页,并且Chromium没有开启Scripts Require Action Feature。这时候ScriptInjection类的成员函数InjectIfAllowed就会马上调用成员函数Inject将当前正在处理的Content Script注入到当前加载的网页中执行,如下所示:

void ScriptInjection::Inject(blink::WebFrame* frame,
                             UserScript::RunLocation run_location,
                             ScriptsRunInfo* scripts_run_info) const {
  ......

  if (ShouldInjectCSS(run_location))
    InjectCSS(frame, scripts_run_info);
  if (ShouldInjectJS(run_location))
    InjectJS(frame, scripts_run_info);
}
       这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。

       ScriptInjection类的成员函数Inject首先调用成员函数ShouldInjectsCSS判断当前正在处理的Content Script是否包含有CSS Script,并且参数run_location描述的Content Script运行时机是否为“document_start”。如果都是的话,那么当前正在处理的Content Script包含的CSS Script就会通过调用另外一个成员函数InjectCSS注入到当前加载的网页中去。这意味着CSS Script只可以在宿主网页的Document对象创建时注入。

       ScriptInjection类的成员函数Inject接下来又调用成员函数ShouldInjectJS判断当前正在处理的Content Script是否包含有JavaScript,并且参数run_location描述的Content Script运行时机是否为包含的JavaScript在清单文件中指定的运行时机。如果都是的话,那么当前正在处理的Content Script包含的JavaScript就会通过调用另外一个成员函数InjectJS注入到当前加载的网页中去执行。

       接下来我们只关注JavaScript注入到宿主网页执行的过程,因此我们继续分析ScriptInjection类的成员函数InjectJS的实现,如下所示:

void ScriptInjection::InjectJS(blink::WebFrame* frame,
                               ScriptsRunInfo* scripts_run_info) const {
  const UserScript::FileList& js_scripts = script_->js_scripts();
  std::vector<blink::WebScriptSource> sources;
  scripts_run_info->num_js += js_scripts.size();
  for (UserScript::FileList::const_iterator iter = js_scripts.begin();
       iter != js_scripts.end();
       ++iter) {
    std::string content = iter->GetContent().as_string();

    .......
    sources.push_back(blink::WebScriptSource(
        blink::WebString::fromUTF8(content), iter->url()));
  }

  ......

  int isolated_world_id =
      user_script_slave_->GetIsolatedWorldIdForExtension(
          user_script_slave_->GetExtension(extension_id_), frame);
  ......

  DOMActivityLogger::AttachToWorld(isolated_world_id, extension_id_);
  frame->executeScriptInIsolatedWorld(isolated_world_id,
                                      &sources.front(),
                                      sources.size(),
                                      EXTENSION_GROUP_CONTENT_SCRIPTS);
  
 
  ......
}
       这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。

       ScriptInjection类的成员函数InjectJS首先遍历将要执行的每一个JavaScript文件。在遍历的过程中,每一个JavaScript文件的内容都会被读取出来,并且封装在一个blink::WebScriptSource对象中。这些blink::WebScriptSource对象最后又会保存在本地变量sources描述的一个blink::WebScriptSource向量中。这个blink::WebScriptSource向量接下来会传递给WebKit中的JS引擎。JS引擎获得了这个向量之后,就会执行保存在里面的JavaScript。

       Extension的Content Script是在一个Isolated World中执行的,这意味着它们不能访问在宿主网页中定义的JS变量和函数,也不能访问在宿主网页中注入的其它Extension的Content Script定义的JS变量和函数。为此,Chromium会为每一个Extension分配一个不同的Isolated World ID,使得它们的Content Script注入到宿主网页时,既不能互相访问各自定义的JS变量和函数,也不能访问宿主网页定义的JS变量和函数。

       按照上述规则,ScriptInjection类的成员函数InjectJS在注入指定的JavaScript到宿主网页中执行之前,首先会调用成员变量user_script_slave_指向的一个UserScriptSlave对象的成员函数GetIsolatedWorldIdForExtension获得一个Isolated World ID。有了这个Isolated World ID之后,就可以调用参数frame指向的一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld了。

       WebLocalFrameImpl类是WebKit向Chromium提供的一个API接口。这个API接口的成员函数executeScriptInIsolatedWorld会将指定的JavaScript交给WebKit内部使用的JS引擎在指定的Isolated World中执行。在Chromium中,这个JS引擎就是V8引擎。V8引擎执行JavaScript的过程,以后我们有机会再分析。

       至此,我们就分析完成Extension的Content Script注入到宿主网页中执行的过程了。这些Content Script虽然是在宿主网页中执行,但是它们是不能访问宿主网页定义的JS变量和函数的,也不能访问注入在宿主网页中的其它Extension的Content Script定义的JS变量和函数。不过,它们却可以操作宿主网页的DOM Tree,这样就可以修改宿主网页的UI和行为了。

       如果我们将Extension看作是一个App,那么它的Page和Content Script就可以看作是它的Module。既然是Module,它们之间就避免不了互相通信,以完成一个App的功能。Chromium为Extension的Page与Page之间,以及Page和Content Script之间,均提供了通信接口。不过,Page与Page之间的通信方式,与Page和Content Script之间的通信方式,是不一样的。这是因为Page运行在所属Extension加载在的Extension Process中,而Content Script运行在宿主网页所加载在的Render Process中。在接下来一篇文章中,我们就继续分析Extension的Page与Page之间,以及Page与Content Script之间的通信机制,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/9/26 1:00:57 原文链接
阅读:37292 评论:10 查看评论

Chromium扩展(Extension)通信机制分析

$
0
0

       Chromium的Extension由Page和Content Script组成。如果将Extension看作是一个App,那么Page和Content Script就是Extension的Module。既然是Module,就避免不了需要相互通信。也正是由于相互通信,使得它们形成一个完整的App。本文接下来就分析Extension的Page之间以及Page与Content Script之间的通信机制。

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

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

       从前面Chromium扩展(Extension)的页面(Page)加载过程分析Chromium扩展(Extension)的Content Script加载过程分析这两篇文章可以知道,Extension的Page,实际上就是Web Page,它们加载在同一个Extension Process中,而Extension的Content Script,实际上是JavaScript,它们加载在宿主网页所在的Render Process中。这意味着Extension的Page之间,可以进行进程内通信,但是Page与Content Script之间,需要进行进程间通信。

       Chromium的Extension模块提供了接口,让Extension的Page与Page之间,以及Page与Content Script之间,可以方便地通信,如图1所示:


图1 Extension的通信机制

      Extension的Page之间的通信,表现为可以访问各自定义的JS变量和函数。例如,我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example,定义了一个Background Page和一个Popup Page。其中,Background Page包含了的一个background.js,它的内容如下所示:

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {   
  ...... 
  
  var views = chrome.extension.getViews({type: "tab"});  
  if (views.length > 0) {  
    console.log(views[0].whoiam);  
  } else {  
    console.log("No tab");  
  }  
});    
  
......  
  
var whoiam = "background.html"
       它定义了一个变量whoiam,同时它又会通过Extension模块提供的API接口chrome.extension.getViews,获得在浏览器窗口的Tab中加载的所有Extension Page的window对象。假设此时Page action example在Tab中加载了一个Extension Page,并且这个Page也像Background Page一样定义了变量whoiam,那么Background Page就可以通过它的window对象直接访问它的变量whoiam。

       API接口chrome.extension.getViews除了可以获得在Tab中加载的Page的window对象,还可以获得以其它方式加载的Page的window对象。例如,在弹窗口中加载的Popup Page的window对象,以及在浏览器的Info Bar(信息栏)和Notification(通知面板)中加载的Page的window对象。可以通过type参数指定要获取哪一种类型的Page的window对象。如果没有指定,那么就会获得所有类型的Page的window对象。

      注意,API接口chrome.extension.getViews获得的是非Background Page的window对象。如果需要获得Background Page的window对象,可以使用另外一个API接口chrome.extension.getBackgroundPage。例如,我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example的Popup Page,包含有一个popup.js,它的内容如下所示:

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);  
  });  
  
  ......   
});  
  
var whoiam = "popup.html" 
       它在Popup Page中显示图片时,就可以通过调用API接口chrome.extension.getBackgroundPage获得Background Page的window对象,然后通过这个window对象访问在Background Page中定义的变量whoiam。从前面的定义可以知道,这个变量的值等于“background.html”。

       总结来说,就是Extension的Page之间,可以通过chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口获得对方的window对象。有了对方的window对象之后,就可以直接进行通信了。

       由于Extension的Page和Content Script不在同一个进程,它们的通信过程就会复杂一些。总体来说,是通过消息进行通信的。接下来我们以Popup Page与Content Script的通信为例,说明Extension的Page和Content Script的通信过程。

       在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example,它的Popup Page可以通过API接口chrome.tabs.sendRequest向Content Script发送请求,如下所示:

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";  
    });    
  });    
}    
       这个请求会被封装在一个类型为ExtensionHostMsg_PostMessage的IPC消息,并且发送给Browser进程。Browser进程会找到Page action example的Content Script的宿主网页所在的Render进程,并且将请求封装成另外一个类型为ExtensionMsg_DeliverMessage的IPC消息发送给它。

       Render进程收到类型为ExtensionMsg_DeliverMessage的IPC消息后,就会将封装在里面的请求提取出来,并且交给Content Script处理,如下所示:

chrome.extension.onRequest.addListener(    
  function(request, sender, sendResponse) {    
    sendResponse({counter: request.counter + 1 });    
  }  
);  
       Content Script需要通过API接口chrome.extension.onRequest.addListener注册一个函数,用来接收来自Extension Page的请求。这个函数的第三个参数是一个Callback函数。通过这个Callback函数,Content Script可以向Background Page发送Response。

       Extension的Content Script同样也可以向Extension的Page发送请求。不过,它是通过另外一个API接口chrome.runtime.sendMessage进行发送的。例如,上述Page action example的Content Script就是通过这个接口向Background Page发送请求的,如下所示:

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";  
  });  
}  
       这个请求同样是先通过一个类型为ExtensionHostMsg_PostMessage的IPC消息传递到Browser进程,然后再由Browser进程通过另外一个类型为ExtensionMsg_DeliverMessage的IPC消息传递给Extension进程中的Background Page。

       Background Page需要通过API接口chrome.runtime.onMessage.addListener注册一个函数,用来接收来自Content Script的请求,如下所示:

chrome.runtime.onMessage.addListener(  
  function(request, sender, sendResponse) {  
    sendResponse({counter: request.counter + 1 });   
  }  
);  
       这个函数的第三个参数同样是一个Callback函数。通过这个Callback函数,Background Page可以向Content Script发送Response。

       总结来说,就是Extension的Page可以通过API接口chrome.tabs.sendRequest和chrome.extension.onRequest.addListener与Content Script通信,而Content Script可以通过API接口chrome.runtime.sendMessage和chrome.runtime.onMessage.addListener与Page通信。

       接下来,我们结合源代码分析chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage这三个API接口的实现。了解这三个API接口的实现之后,我们就会对上述的Extension通信机制,有更深刻的认识。

       在分析上述三个API接口之前,我们首先简单介绍一下JS Binding。JS Binding类型于Java里面的JNI,用来在JS与C/C++之间建立桥梁,也就是用来将一个JS接口绑定到一个C/C++函数中去。

       Chromium使用的JS引擎是V8。V8引擎在创建完成Script Context之后,会向WebKit发出通知。WebKit再向Chromium的Content模块发出通知。Content模块又会向Extension模块发出通知。Extension模块获得通知后,就会将chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage接口绑定到Chromium内部定义的函数中去,从而使得它们可以通过Chromium的基础设施来实现Extension的通信机制。

      V8引擎的初始化发生在V8WindowShell类的成员函数initialize中,它的实现如下所示:

bool V8WindowShell::initialize()
{
    TRACE_EVENT0("v8", "V8WindowShell::initialize");
    ......

    createContext();
    ......

    ScriptState::Scope scope(m_scriptState.get());
    v8::Handle<v8::Context> context = m_scriptState->context();
    ......

    m_frame->loader().client()->didCreateScriptContext(context, m_world->extensionGroup(), m_world->worldId());
    return true;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/bindings/v8/V8WindowShell.cpp中。

       V8WindowShell类的成员函数initialize在初始化完成V8引擎之后,会通过调用成员变量m_frame指向的一个LocalFrame对象的成员函数loader获得一个FrameLoader对象。有了这个FrameLoader对象之后,再调用它的成员函数client就可以获得一个FrameLoaderClientImpl对象。有了这个FrameLoaderClientImpl对象,就可以调用它的成员函数didCreateScriptContext向WebKit发出通知,V8引擎的Script Context创建好了。

       FrameLoaderClientImpl类的成员函数didCreateScriptContext的实现如下所示:

void FrameLoaderClientImpl::didCreateScriptContext(v8::Handle<v8::Context> context, int extensionGroup, int worldId)
{
    WebViewImpl* webview = m_webFrame->viewImpl();
    ......
    if (m_webFrame->client())
        m_webFrame->client()->didCreateScriptContext(m_webFrame, context, extensionGroup, worldId);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FrameLoaderClientImpl.cpp中。

       FrameLoaderClientImpl类的成员变量m_webFrame指向的是一个WebLocalFrameImpl对象。FrameLoaderClientImpl类的成员函数didCreateScriptContext首先调用这个WebLocalFrameImpl对象的成员函数viewImpl获得一个WebViewImpl对象。有了这个WebViewImpl对象之后,再调用它的成员函数client可以获得一个RenderFrameImpl对象。这个RenderFrameImpl对象实现了WebViewClient接口,它是从Chromium的Content层设置进来的,作为WebKit回调Content的接口。因此,有了这个RenderFrameImpl对象之后,FrameLoaderClientImpl类的成员函数didCreateScriptContext就可以调用它的成员函数didCreateScriptContext,用来通知它V8引擎的Script Context创建好了。

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

void RenderFrameImpl::didCreateScriptContext(blink::WebLocalFrame* frame,
                                             v8::Handle<v8::Context> context,
                                             int extension_group,
                                             int world_id) {
  DCHECK(!frame_ || frame_ == frame);
  GetContentClient()->renderer()->DidCreateScriptContext(
      frame, context, extension_group, world_id);
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       我们假设当前基于Chromium实现的浏览器为Chrome。这时候RenderFrameImpl类的成员函数didCreateScriptContext调用函数GetContentClient获得的是一个ChromeContentClient对象。有了这个ChromeContentClient对象之后,调用它的成员函数renderer可以获得一个ChromeContentRendererClient对象。有了这个ChromeContentRendererClient对象之后,RenderFrameImpl类的成员函数didCreateScriptContext就可以调用它的成员函数DidCreateScriptContext,用来通知它V8引擎的Script Context创建好了。

       ChromeContentRendererClient类的成员函数DidCreateScriptContext的实现如下所示:

void ChromeContentRendererClient::DidCreateScriptContext(
    WebFrame* frame, v8::Handle<v8::Context> context, int extension_group,
    int world_id) {
  extension_dispatcher_->DidCreateScriptContext(
      frame, context, extension_group, world_id);
}
       这个函数定义在文件external/chromium_org/chrome/renderer/chrome_content_renderer_client.cc中。

       ChromeContentRendererClient类的成员变量extension_dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象就是我们在前面Chromium扩展(Extension)的Content Script加载过程分析一文中提到的那个用来接收Extension相关的IPC消息的Dispatcher对象,ChromeContentRendererClient类的成员函数DidCreateScriptContext调用所做的事情就是调用它的成员函数DidCreateScriptContext,用来通知它V8引擎的Script Context创建好了。

       Dispatcher类的成员函数DidCreateScriptContext的实现如下所示:

void Dispatcher::DidCreateScriptContext(
    WebFrame* frame,
    const v8::Handle<v8::Context>& v8_context,
    int extension_group,
    int world_id) {
  ......

  std::string extension_id = GetExtensionID(frame, world_id);

  const Extension* extension = extensions_.GetByID(extension_id);
  ......

  Feature::Context context_type =
      ClassifyJavaScriptContext(extension,
                                extension_group,
                                ScriptContext::GetDataSourceURLForFrame(frame),
                                frame->document().securityOrigin());

  ScriptContext* context =
      delegate_->CreateScriptContext(v8_context, frame, extension, context_type)
          .release();
  ......

  {
    scoped_ptr<ModuleSystem> module_system(
        new ModuleSystem(context, &source_map_));
    context->set_module_system(module_system.Pass());
  }
  ModuleSystem* module_system = context->module_system();

  ......

  RegisterNativeHandlers(module_system, context);

  ......
}

       这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

       参数frame描述的是当前加载的网页,另外一个参数world_id描述的是V8引擎中的一个Isolated World ID。从前面Chromium扩展(Extension)的Content Script加载过程分析一文可以知道,Isolated World是用来执行Extension的Content Script的,并且每一个Extension在其宿主网页中都有一个唯一的Isolated World。这意味着根据这个Isolated World ID可以获得它所对应的Extension。这可以通过调用Dispatcher类的成员函数GetExtensionID获得。

       知道了参数world_id描述的Isolated World对应的Extension之后,Dispatcher类的成员函数DidCreateScriptContext就可以为这个Extension创建一个Script Context。这个Script Context实际上只是对参数v8_context描述的V8 Script Context进行封装。

       创建上述Script Context的目的是创建一个Module System。通过这个Module System,可以向参数world_id描述的Isolated World注册Native Handler。Native Handler的作用就是创建JS Binding。有了这些JS Binding之后,我们就可以在Content Script中调用Extension相关的API接口了。

       Dispatcher类的成员函数DidCreateScriptContext最后是通过调用另外一个成员函数RegisterNativeHandlers向参数world_id描述的Isolated World注册Native Handler的,它的实现如下所示:

void Dispatcher::RegisterNativeHandlers(ModuleSystem* module_system,
                                        ScriptContext* context) {
  ......

  module_system->RegisterNativeHandler(
      "messaging_natives",
      scoped_ptr<NativeHandler>(MessagingBindings::Get(this, context)));

  ......

  module_system->RegisterNativeHandler(
      "runtime", scoped_ptr<NativeHandler>(new RuntimeCustomBindings(context)));

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

       Dispatcher类的成员函数RegisterNativeHandlers注册了一系列的Native Handler。每一个Native Handler都对应有一个名称。这个名称在JS中称为Module,可以通过JS函数require进行引用。这一点后面我们就会看到它的用法。

       这里我们只关注与前面提到的API接口chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage相关的两个Module:“message_natives”和“runtime”。

       其中,名称为“message_natives”的Module使用的Native Handler是一个ExtensionImpl对象。这个ExtensionImpl对象是通过调用MessagingBindings类的静态成员函数Get创建的,如下所示:

ObjectBackedNativeHandler* MessagingBindings::Get(Dispatcher* dispatcher,
                                                  ScriptContext* context) {
  return new ExtensionImpl(dispatcher, context);
}
       这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

       这个ExtensionImpl对象在创建的时候,就会为名称为“message_natives”的Module导出的JS函数创建Binding,如下所示:

class ExtensionImpl : public ObjectBackedNativeHandler {
 public:
  ExtensionImpl(Dispatcher* dispatcher, ScriptContext* context)
      : ObjectBackedNativeHandler(context), dispatcher_(dispatcher) {
    ......
    RouteFunction(
        "PostMessage",
        base::Bind(&ExtensionImpl::PostMessage, base::Unretained(this)));
    ......
  }

  ......
};
       这个类定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

       从这里可以看到,名称为“message_natives”的Module导出了一个名称为“PostMessage”的JS函数。这个JS函数绑定了ExtensionImpl类的成员函数PostMessage。这意味着以后我们在JS中调用了messaging_natives.PostMessage函数时,ExtensionImpl类的成员函数PostMessage就会被调用。

       回到Dispatcher类的成员函数RegisterNativeHandlers中,它注册的另外一个名称为“runtime”的Module使用的Native Handler是一个RuntimeCustomBindings对象。这个RuntimeCustomBindings对象在创建的过程中,就会为名称为“runtime”的Module导出的JS函数创建Binding,如下所示:

RuntimeCustomBindings::RuntimeCustomBindings(ScriptContext* context)
    : ObjectBackedNativeHandler(context) {
  ......
  RouteFunction("GetExtensionViews",
                base::Bind(&RuntimeCustomBindings::GetExtensionViews,
                           base::Unretained(this)));
}
       这个类定义在文件external/chromium_org/extensions/renderer/runtime_custom_bindings.cc中。

       这里可以看到,名称为“runtime”的Module导出了一个名称为“GetExtensionViews”的JS函数。这个JS函数绑定了RuntimeCustomBindings类的成员函数GetExtensionViews。这意味着以后我们在JS中调用了runtime.GetExtensionViews函数时,RuntimeCustomBindings类的成员函数GetExtensionViews就会被调用。

       有了以上JS Binding相关的背景知识之后,接下来我们就开始分析Chromium的Extension提供的API接口chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage的实现了。

       我们首先分析chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口的实现,如下所示:

var binding = require('binding').Binding.create('extension');
.....
var runtimeNatives = requireNative('runtime');
var GetExtensionViews = runtimeNatives.GetExtensionViews;

binding.registerCustomHook(function(bindingsAPI, extensionId) {
  ......

  var apiFunctions = bindingsAPI.apiFunctions;

  apiFunctions.setHandleRequest('getViews', function(properties) {
    var windowId = WINDOW_ID_NONE;
    var type = 'ALL';
    if (properties) {
      if (properties.type != null) {
        type = properties.type;
      }
      if (properties.windowId != null) {
        windowId = properties.windowId;
      }
    }
    return GetExtensionViews(windowId, type);
  });

  ......

  apiFunctions.setHandleRequest('getBackgroundPage', function() {
    return GetExtensionViews(-1, 'BACKGROUND')[0] || null;
  });

  ......
});

       这两个JS接口定义在文件external/chromium_org/extensions/renderer/resources/extension_custom_bindings.js中。

       从这里可以看到,chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口都是通过调用名称为"runtime"的Module导出的函数GetExtensionViews(即runtime.GetExtensionViews)实现的。不过,后者在调用函数runtime.GetExtensionViews时,两个参数被固定为-1和"BACKGROUND",表示要获取的是Background Page的window对象。

       从前面的分析可以知道,函数runtime.GetExtensionViews绑定到了RuntimeCustomBindings类的成员函数GetExtensionViews。这意味着,当我们在JS中调用chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口时,最后会调用到C++层的RuntimeCustomBindings类的成员函数GetExtensionViews。它的实现如下所示:

void RuntimeCustomBindings::GetExtensionViews(
    const v8::FunctionCallbackInfo<v8::Value>& args) {
  if (args.Length() != 2)
    return;

  if (!args[0]->IsInt32() || !args[1]->IsString())
    return;

  // |browser_window_id| == extension_misc::kUnknownWindowId means getting
  // all views for the current extension.
  int browser_window_id = args[0]->Int32Value();

  std::string view_type_string = *v8::String::Utf8Value(args[1]->ToString());
  StringToUpperASCII(&view_type_string);
  // |view_type| == VIEW_TYPE_INVALID means getting any type of
  // views.
  ViewType view_type = VIEW_TYPE_INVALID;
  if (view_type_string == kViewTypeBackgroundPage) {
    view_type = VIEW_TYPE_EXTENSION_BACKGROUND_PAGE;
  } else if (view_type_string == kViewTypeInfobar) {
    view_type = VIEW_TYPE_EXTENSION_INFOBAR;
  } else if (view_type_string == kViewTypeTabContents) {
    view_type = VIEW_TYPE_TAB_CONTENTS;
  } else if (view_type_string == kViewTypePopup) {
    view_type = VIEW_TYPE_EXTENSION_POPUP;
  } else if (view_type_string == kViewTypeExtensionDialog) {
    view_type = VIEW_TYPE_EXTENSION_DIALOG;
  } else if (view_type_string == kViewTypeAppWindow) {
    view_type = VIEW_TYPE_APP_WINDOW;
  } else if (view_type_string == kViewTypePanel) {
    view_type = VIEW_TYPE_PANEL;
  } else if (view_type_string != kViewTypeAll) {
    return;
  }

  std::string extension_id = context()->GetExtensionID();
  if (extension_id.empty())
    return;

  std::vector<content::RenderView*> views = ExtensionHelper::GetExtensionViews(
      extension_id, browser_window_id, view_type);
  v8::Local<v8::Array> v8_views = v8::Array::New(args.GetIsolate());
  int v8_index = 0;
  for (size_t i = 0; i < views.size(); ++i) {
    v8::Local<v8::Context> context =
        views[i]->GetWebView()->mainFrame()->mainWorldScriptContext();
    if (!context.IsEmpty()) {
      v8::Local<v8::Value> window = context->Global();
      DCHECK(!window.IsEmpty());
      v8_views->Set(v8::Integer::New(args.GetIsolate(), v8_index++), window);
    }
  }

  args.GetReturnValue().Set(v8_views);
}
       这个函数定义在文件external/chromium_org/extensions/renderer/runtime_custom_bindings.cc中。

       RuntimeCustomBindings类的成员函数GetExtensionViews首先将从JS传递过来的参数(Page Window ID和Page Type)提取出来,并且获得当前正在调用的Extension的ID,然后就调用ExtensionHelper类的静态成员函数GetExtensionViews获得指定的Page所加载在的Render View。

       在Render进程中,每一个网页都是加载在一个Render View中的。有了Render View之后,就可以获得它在WebKit层为网页创建的V8 Script Context。有了V8 Script Context之后,就可以获得网页的window对象了。这些window对象最后会返回到JS层中去给调用者。

       接下来,我们继续分析ExtensionHelper类的静态成员函数GetExtensionViews的实现,以便了解Extension Page对应的Render View的获取过程,如下所示:

std::vector<content::RenderView*> ExtensionHelper::GetExtensionViews(
    const std::string& extension_id,
    int browser_window_id,
    ViewType view_type) {
  ViewAccumulator accumulator(extension_id, browser_window_id, view_type);
  content::RenderView::ForEach(&accumulator);
  return accumulator.views();
}
       这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

       ExtensionHelper类的静态成员函数GetExtensionViews通过调用RenderView类的静态成员函数ForEach遍历在当前Render进程创建的所有Render View,并且通过一个ViewAccumulator对象挑选出那些符合条件的Render View返回给调用者。

       RenderView类的静态成员函数ForEach的实现如下所示:

void RenderView::ForEach(RenderViewVisitor* visitor) {
  ViewMap* views = g_view_map.Pointer();
  for (ViewMap::iterator it = views->begin(); it != views->end(); ++it) {
    if (!visitor->Visit(it->second))
      return;
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc中。

       RenderView类的静态成员函数ForEach通过遍历全局变量g_view_map描述的一个Map可以获得当前Render进程创建的所有Render View。这些Render View将会进一步交给参数visitor描述的一个ViewAccumulator对象进行处理。

       从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程为网页创建的Render View实际上是一个RenderViewImpl对象。每一个RenderViewImpl对象在创建完成后,它们的成员函数Initialize都会被调用,用来执行初始化工作。在初始化的过程,RenderViewImpl对象就会将自己保存在上述全局变量g_view_map描述的一个Map中,如下所示:

void RenderViewImpl::Initialize(RenderViewImplParams* params) {
  ......

  g_view_map.Get().insert(std::make_pair(webview(), this));

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

      因此,前面分析的RenderView类的静态成员函数ForEach,可以通过遍历全局变量g_view_map描述的Map获得当前Render进程创建的所有Render View。

      这样,我们就分析完成了chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口的实现。接下来,我们继续分析chrome.runtime.sendMessage这个API接口的实现,如下所示:

var messaging = require('messaging');
......

binding.registerCustomHook(function(binding, id, contextType) {
  ......

  apiFunctions.setHandleRequest('sendMessage',
      function(targetId, message, options, responseCallback) {
    var connectOptions = {name: messaging.kMessageChannel};
    forEach(options, function(k, v) {
      connectOptions[k] = v;
    });
    var port = runtime.connect(targetId || runtime.id, connectOptions);
    messaging.sendMessageImpl(port, message, responseCallback);
  });

  ......
});

       这个JS接口定义在文件external/chromium_org/extensions/renderer/resources/runtime_custom_bindings.js中。

       从这里可以看到,chrome.runtime.sendMessage这个API接口是通过调用名称为"messaging"的Module导出的函数sendMessageImpl(即messaging.sendMessageImpl)实现的。

       在调用函数messaging.sendMessageImpl的时候,需要指定的一个Port。在Extension中,所有的消息都是通过通道进行传输的。这个通道就称为Port。我们可以通过调用另外一个API接口runtime.connect获得一个连接到目标通信对象的Port。有了这个Port之后,就可以向目标通信对象发送消息了。目标通信对象可以通过参数targetId描述。如果没有指定targetId,则使用默认的Port进行发送消息。这个默认的Port由runtime.id描述。

       函数messaging.sendMessageImpl的实现如下所示:

function sendMessageImpl(port, request, responseCallback) {
    if (port.name != kNativeMessageChannel)
      port.postMessage(request);

    ......

    function messageListener(response) {
      try {
        responseCallback(response);
      } finally {
        port.disconnect();
      }
    }

    ......

    port.onMessage.addListener(messageListener);
};
       这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。

       从前面的调用过程可以知道,参数port描述的Port的名称被设置为kMessageChannel,它的值不等于kNativeMessageChannel。在这种情况下,函数messaging.sendMessageImpl将会调用参数port描述的Port的成员函数postMessage发送参数request描述的消息给目标通信对象,并且它会将参数responseCallback描述的一个Callback封装在一个Listener中。当目标通信对象处理完成参数request描述的消息进行Reponse时,上述Listener就会调用它内部封装的Callback,这样消息的发送方就可以得到接收方的回复了。

       参数port描述的Port的成员函数postMessage的实现如下所示:

var messagingNatives = requireNative('messaging_natives');
......

PortImpl.prototype.postMessage = function(msg) {
    .....
    messagingNatives.PostMessage(this.portId_, msg);
};
      这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。

      从这里可以看到,Port类的成员函数postMessage是通过调用名称为“messaging_natives”的Module导出的函数PostMessage(即messaging_natives.PostMessage)实现的。

      从前面的分析可以知道,函数messaging_natives.PostMessage绑定到了ExtensionImpl类的成员函数PostMessage。这意味中,当我们在JS中调用chrome.runtime.sendMessage这个API接口时,最后会调用到C++层的ExtensionImpl类的成员函数PostMessage。它的实现如下所示:

class ExtensionImpl : public ObjectBackedNativeHandler {
 ......

 private:
  ......

  // Sends a message along the given channel.
  void PostMessage(const v8::FunctionCallbackInfo<v8::Value>& args) {
    content::RenderView* renderview = context()->GetRenderView();
    ......

    int port_id = args[0]->Int32Value();
    ......

    renderview->Send(new ExtensionHostMsg_PostMessage(
        renderview->GetRoutingID(), port_id,
        Message(*v8::String::Utf8Value(args[1]),
                blink::WebUserGestureIndicator::isProcessingUserGesture())));
  }

  ......
};

       这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

       ExtensionImpl类的成员函数PostMessage首先获得一个Render View。这个Render View描述的是消息发送方所属的网页。获得这个Render View的目的,是为了调用它的成员函数Send向Browser进程发送一个类型为ExtensionHostMsg_PostMessage的IPC消息。这个IPC消息封装了JS层所要发送的消息。

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

bool ChromeExtensionWebContentsObserver::OnMessageReceived(
    const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(ChromeExtensionWebContentsObserver, message)
    IPC_MESSAGE_HANDLER(ExtensionHostMsg_PostMessage, OnPostMessage)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_extension_web_contents_observer.cc中。 

      从这里可以看到,ChromeExtensionWebContentsObserver类的成员函数OnMessageReceived将类型为ExtensionHostMsg_PostMessage的IPC消息分发给另外一个成员函数OnPostMessage处理,如下所示:

void ChromeExtensionWebContentsObserver::OnPostMessage(int port_id,
                                                       const Message& message) {
  MessageService* message_service = MessageService::Get(browser_context());
  if (message_service) {
    message_service->PostMessage(port_id, message);
  }
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_extension_web_contents_observer.cc中。

      ChromeExtensionWebContentsObserver类的成员函数OnPostMessage首先通过MessageService类的静态成员函数Get获得一个MessageService对象。这个MessageService对象负责管理在当前Render进程创建的所有Port。因此,有了这个MessageService对象之后,就可以调用它的成员函数PostMessage将参数message描述的消息分发给参数port_id描述的Port处理,如下所示:

void MessageService::PostMessage(int source_port_id, const Message& message) {
  int channel_id = GET_CHANNEL_ID(source_port_id);
  MessageChannelMap::iterator iter = channels_.find(channel_id);
  ......

  DispatchMessage(source_port_id, iter->second, message);
}
       这个函数定义在external/chromium_org/chrome/browser/extensions/api/messaging/message_service.cc中。

       MessageService类的成员函数PostMessage首先根据参数source_port_id获得一个Channel ID。有了这个Channel ID之后,就可以在成员变量channels_描述的一个Map中获得一个对应的MessageChannel对象。这个MessageChannel描述的就是一个Port。因此,有了这个MessageChannel对象之后,MessageService类的成员函数PostMessage就可以调用另外一个成员函数DispatchMessage将参数message描述的消息分发给它处理,如下所示:

void MessageService::DispatchMessage(int source_port_id,
                                     MessageChannel* channel,
                                     const Message& message) {
  // Figure out which port the ID corresponds to.
  int dest_port_id = GET_OPPOSITE_PORT_ID(source_port_id);
  MessagePort* port = IS_OPENER_PORT_ID(dest_port_id) ?
      channel->opener.get() : channel->receiver.get();

  port->DispatchOnMessage(message, dest_port_id);
}
       这个函数定义在external/chromium_org/chrome/browser/extensions/api/messaging/message_service.cc中。

       MessageService类的成员函数DispatchMessage首先根据参数source_port_id获得目标通信对象用来接收消息的Port的ID。这个ID就称为Dest Port ID。有了这个Dest Port ID之后,就可以通过参数channel描述的MessageChannel对象获得一个MessagePort对象。通过调用这个MessagePort对象的成员函数DispatchOnMessage,就可以将参数message描述的消息发送给目标通信对象。

       上述获得的MessagePort对象的实际类型是ExtensionMessagePort。ExtensionMessagePort类重写了父类MessagePort的成员函数DispatchOnMessage。因此,MessageService类的成员函数DispatchMessage实际上是通过调用ExtensionMessagePort类的成员函数DispatchOnMessage向目标通信对象发送消息,如下所示:

void ExtensionMessagePort::DispatchOnMessage(const Message& message,
                                             int target_port_id) {
  process_->Send(new ExtensionMsg_DeliverMessage(
      routing_id_, target_port_id, message));
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/api/messaging/extension_message_port.cc中。

       ExtensionMessagePort类的成员变量process_指向的是一个RenderProcessHost对象。这个RenderProcessHost对象描述的是目标通信对象所在的Render进程,ExtensionMessagePort类的成员函数DispatchOnMessage通过调用它的成员函数Send可以向目标通信对象所在的Render进程发送一个类型为ExtensionMsg_DeliverMessage的IPC消息。这个IPC消息封装了参数message描述的消息。

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

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

      从这里可以看到,ExtensionHelper类的成员函数OnMessageReceived将类型为ExtensionMsg_DeliverMessage的IPC消息分发给另外一个成员函数OnExtensionDeliverMessage处理,如下所示:

void ExtensionHelper::OnExtensionDeliverMessage(int target_id,
                                                const Message& message) {
  MessagingBindings::DeliverMessage(
      dispatcher_->script_context_set(), target_id, message, render_view());
}
       这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

       ExtensionHelper类的成员变量dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象在当前Render进程中是唯一的。调用这个Dispatcher对象的成员函数script_context_set可以获得一个V8 Script Context集合,其中的每一个V8 Script Context都对应有一个Page。由于当前Render进程可能加载有多个Page,每一个Page也可能会创建多个V8 Script Context,因此这里获得的V8 Script Context的个数可能大于1。

       此外,在当前Render进程加载的每一个Page都对应有一个ExtensionHelper对象。对于当前正在处理的ExtensionHelper对象来说,它对应的Page可以通过它的调用成员函数render_view获得。ExtensionHelper类的成员函数OnExtensionDeliverMessage所要做的事情就是将参数message描述的消息分发给当前正在处理的ExtensionHelper对象对应的Page处理。确切地说,是将分发给该Page创建的所有V8 Script Context进行处理。这是通过调用MessagingBindings类的静态成员函数DeliverMessage实现的,如下所示:

void MessagingBindings::DeliverMessage(
    const ScriptContextSet& context_set,
    int target_port_id,
    const Message& message,
    content::RenderView* restrict_to_render_view) {
  ......

  context_set.ForEach(
      restrict_to_render_view,
      base::Bind(&DeliverMessageToScriptContext, message.data, target_port_id));
}
      这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

      MessagingBindings类的静态成员函数DeliverMessage所做的事情就是遍历参数context_set描述的V8 Script Context集合中的所有V8 Script Context。对于每一个V8 Script Context,都检查它们对应的Page是否就是参数restrict_to_render_view描述的Page。如果是的话,那么就会调用函数DeliverMessageToScriptContext将参数message描述的消息分给它处理,如下所示:

void DeliverMessageToScriptContext(const std::string& message_data,
                                   int target_port_id,
                                   ScriptContext* script_context) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope handle_scope(isolate);

  // Check to see whether the context has this port before bothering to create
  // the message.
  v8::Handle<v8::Value> port_id_handle =
      v8::Integer::New(isolate, target_port_id);
  v8::Handle<v8::Value> has_port =
      script_context->module_system()->CallModuleMethod(
          "messaging", "hasPort", 1, &port_id_handle);

  CHECK(!has_port.IsEmpty());
  if (!has_port->BooleanValue())
    return;

  std::vector<v8::Handle<v8::Value> > arguments;
  arguments.push_back(v8::String::NewFromUtf8(isolate,
                                              message_data.c_str(),
                                              v8::String::kNormalString,
                                              message_data.size()));
  arguments.push_back(port_id_handle);
  script_context->module_system()->CallModuleMethod(
      "messaging", "dispatchOnMessage", &arguments);
}
      这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

      函数DeliverMessageToScriptContext首先是检查在参数script_context描述的V8 Script Context中,是否存在一个ID等于参数target_port_id的Port。如果存在,那么就会将参数message_data描述的消息分发给它处理。这是通过调用名称为"messaging"的Module导出的JS函数dispatchOnMessage(即messaging.dispatchOnMessage)实现的。

      JS函数messaging.dispatchOnMessage的定义如下所示:

  // Called by native code when a message has been sent to the given port.
  function dispatchOnMessage(msg, portId) {
    var port = ports[portId];
    if (port) {
      if (msg)
        msg = $JSON.parse(msg);
      port.onMessage.dispatch(msg, port);
    }
  };
       这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。

       JS函数messaging.dispatchOnMessage首先根据参数portId描述的Port ID找到对应的Port。每一个Port都有一个onMessage属性。这个onMessage属性描述的是一个Event对象。这个Event对象内部维护有一个Listener列表。列表中的每一个Listener就是参数msg描述的消息的接收者。通过调用这个Event对象的成员函数dispatch即可以将参数msg描述的消息分发给它内部维护的Listener对象处理。

       这样,我们就分析完成了chrome.runtime.sendMessage这个API接口的实现。与此同时,我们也分析完成了Extension的Page与Page之间,以及Page与Content Script之间的通信机制。概括来说,就是Page与Page之间通过直接访问对方定义的变量或者函数完成通信过程,而Page与Content Script之间通过消息完成通信过程。

       至此,我们就分析完成了Chromium的Extension机制。从分析的过程可以知道,Extension相当于是运行在Chromium环境中的一种App。这种App由Page和Content Script组成。Page与Page之间,以及Page与Content Script之间,可以相互通信。其中,Content Script可以注入在Chromium加载的网页中执行,从而可以增加网页的功能。此外,这种App的开发语言是JavaScript,UI是HTML页面,并且通过Chromium提供的API完成自身的功能。重新学习Chromium的Extension机制,可以参考前面Chromium扩展(Extension)机制简要介绍和学习计划一文。更多的信息,也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

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

Chromium插件(Plugin)机制简要介绍和学习计划

$
0
0

       在Chromium中,除了可以使用Extension增强浏览器功能,还可以使用Plugin。两者最大区别是前者用JS开发,后者用C/C++开发。这意味着Plugin以Native Code运行,在性能上要优于Extension,适合执行计算密集型工作。不过,以Native Code运行,使得Plugin在安全上面临更大挑战。本文接下来对Chromium的Plugin机制进行简要介绍和制定学习计划。

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

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

       Chromium最初支持两种类型的Plugin:NPAPI Plugin和PPAPI Plugin。NPAPI的全称是Netscape Plugin Application Programming Interface,PPAPI的全称是Pepper Plugin Application Programming Interface。从表面上看,两者的区别在于使用了不同的API规范。其中,NPAPI来自于Mozilla,而PPAPI来自于Google。但实际上,PPAPI与另外一种称为Native Client(NaCl)的技术是紧密联系在一起的。

       Native Client是一个由编译工具链(Toolchain)和运行时(Runtime)组成的集合。Toolchain包含在NaCl SDK中。此外,NaCl SDK还提供了PPAPI接口。也就是,通过NaCl SDK,我们可以开发PPAPI Plugin。这些PPAPI Plugin要使用NaCl SDK提供的Toolchain进行编译。Toolchain使用了修改过的GCC对PPAPI Plugin进行编译。这个修改过的GCC为PPAPI Plugin生成的每一条跳转指令的目标地址都是32位对齐的,并且PPAPI Plugin的第一条指令的地址也是32位对齐的。对齐的目的是出于安全考虑,防止恶意代码跳转到任意地址执行指令。同时,有些被认为是危险的指令是禁止使用的,例如ret指令。

       除此之外,这个修改过的GCC还会设置一个系统API白名单。在这个白名单里面的系统API,被认为是安全的,也就是对系统不会造成威胁和破坏,它们可以被PPAPI Plugin调用。不在这个白名单内的系统API,将会被禁止调用。这意味着,我们在开发PPAPI Plugin的时候,只可以使用由Chromium提供的PPAPI,以及位于白名单内的系统API。

       对PPAPI Plugin的CPU指令以及API调用进行限制,目的都是为了安全。这些安全理论基础可以参考Native Client: A Sandbox for Portable, Untrusted x86 Native CodeGoogle Native Client: Analysis Of A Secure Browser Plugin Sandbox这两个PDF文档,我们在这里就不展开来讲了。

       单单靠Toolchain对PPAPI Plugin的CPU指令以及API调用进行限制是不足够的,因为黑客可以对编译后的PPAPI Plugin进行修改。因此,就需要有一套Runtime,在PPAPI Plugin加载时进行验证。这套Runtime由一个NaCl PPAPI Plugin和一个Service Runtime组成,如图1所示:


图1 NaCl Architecture

       NaCl PPAPI Plugin是一个内置在Chromium中的PPAPI Plugin,它属于一个Trusted Plugin,运行在Render进程中。Chromium还内置了一个PPAPI Flash,它同样也是一个Trusted Plugin,运行在Render进程中。此外,负责解析网页的WebKit,以及运行网页中的JS的V8引擎,也是运行在Render进程中的。

       注意,内置的PPAPI Plugin不需要使用NaCl技术,因为它们是Trusted Code。为了区分使用了NaCl技术的PPAPI Plugin和内置的PPAPI Plugin,我们将前者称为NaCl Module。NaCl Module是Untrusted Code,它们以两种形式存在:Portable Executable(PEXE)和Native Executable(NEXE)。PEXE是跨平台的,编译一次,可以在所有平台运行。NEXE是平台相关的,只可以运行在指定的平台。实际上,PEXE在加载之前,会先被翻译成NEXE再加载和执行。

       在网页中,可以通过<embed>标签使用一个NaCl模块,如下所示:

<embed name="NaCl_module"
  id="hello_world"
  width=200 height=200
  src="hello_world.nmf"
  type="application/x-NaCl" />
       其中,src属性指定了一个NaCl Module清单文件。这个清单文件以json格式描述了它由哪些文件组成,如下所示:

{ "files": {
  "libgcc_s.so.1": { "x86-32": { "url": "lib32/libgcc_s.so.1" } },
  "main.nexe": { "x86-32": { "url": "hw.nexe" } },
  "libc.so.3c8d1f2e": { "x86-32": { "url": "lib32/libc.so.3c8d1f2e" } },
  "libpthread.so.3c8d1f2e": { "x86-32": { "url": "lib32/libpthread.so.3c8d1f2e" } } },
  "program": { "x86-32": { "url": "lib32/runnable-ld.so" } }
}
       WebKit会将<embed>标签当作是一个Plugin,于是就会请求Chromium加载这个Plugin。Chromium发现要加载的Plugin的MIME Type为“application/x-NaCl”,于是就会在当前的Render进程中创建一个NaCl PPAPI Plugin。这个NaCl PPAPI Plugin将会负责去加载真正的NaCl Module,也就是<embed>标签的src属性指定的NaCl Module。

       NaCl PPAPI Plugin首先会解析要加载的NaCl Module的清单文件,然后通过Chromium提供的PPAPI接口去下载清单文件里面指定的文件。随后,NaCl PPAPI Plugin又会请求Chromium的Browser进程(即图1中的Broker进程)为它创建一个sel_ldr进程。这个sel_ldr进程与Render进程一样,运行在一个同样的Sandbox中。这个Sandbox称为Outer Sandbox。

       上述sel_ldr进程启动起来之后,会加载一个Service Runtime。这个Service Runtime会加载真正的NaCl Module。不过,在加载之前,Service Runtime会对NaCl Module的指令及其调用的API进行验证。如果NaCl Module违反规则,那么它就不会被加载。这样就可以解决我们前面提到的NaCl Module使用修改过的GCC编译之后又被Hacked的问题。

       Service Runtime作为NaCl技术的一部分,它是Trusted Code。NaCl Module是Untrusted Code。Service Runtime除了负责加载NaCl Module之外,还会为NaCl Module提供一些服务。例如,NaCl Module在调用白名单允许的系统API时,它不是直接调用的,而是要通过Service Runtime进行调用。尽管NaCl Module与Service Runtime在同一个进程中,但是NaCl Module是不可以直接执行Service Runtime的代码的。

       当NaCl Module要执行Service Runtime的代码时,需要通过Trampoline和Springboard间接执行。Trampoline和Springboard的作用有点类似x86的调用门(Call Gate),目的就是为了保护Service Runtime的代码不会被随意执行,就像User Space的代码不能随意执行Kernel Space的代码一样。这套保护机制,以及前面提到的对NaCl Module的指令限制和系统API调用限制,称为Inner Sandbox。

       NaCl Module是以间接方式调用Chromium提供的PPAPI接口的,表现它要通过NaCl PPAPI Plugin来调用Chromium提供的PPAPI接口。这意味着NaCl Module与NaCl PPAPI Plugin之间需要有一种通信机制。这种通信机制使用的协议称为Simple-RPC(Remote Procedure Call)协议,简称SRPC。SRPC协议建立在Inter-Module Communication(IMC)之上。IMC又是什么呢?实际上就是Unix Socket。总结来说,就是NaCl Module通过Unix Socket请求NaCl PPAPI Plugin为它调用Chromium提供的PPAPI接口。当然,这些远程请求会被NaCl SDK封装为简单的函数调用,这也是SRPC的由来。

       从前面的分析就可以知道,一个使用了NaCl技术的PPAPI Plugin在执行时,被限制在两个Sandbox中。一个是Inner Sandbox,另一个是Outer Sandbox。其中,Inner Sandbox施加了强有力的保护。即使这层强有力的保护被突破,还有Outer Sandbox继续保护着。这样就可以很大程度上保证系统的安全。与此相反的是,NPAPI Plugin缺乏Inner Sandbox这样强有力的保护,就会很容易出现安全问题。同样,IE浏览器的ActiveX Plugin,也面临着NPAPI Plugin同样的安全问题。

       虽然前面我们一直把PPAPI Plugin和NaCl联系在一起,但事实上,PPAPI Plugin也可以完全脱离NaCl而存在,例如,图1所示的内置在Chromium中的NaCl PPAPI Plugin和PPAPI Flash Plugin。Chromium提供了一个PPAPI SDK。通过这个SDK,就可以开发与NaCl技术无关的PPAPI Plugin。不过,这些PPAPI Plugin是不能通过Web Store分发的,只能由用户在本地安装。

       接下来,基于越简单越容易理解的原则,我们将忽略掉NaCl技术,分析Chromium的PPAPI Plugin机制,并且假设它们是运行在独立的Plugin进程中的,就如我们在前面Chromium的Plugin进程启动过程分析一文所述的。

       从前面Chromium的Plugin进程启动过程分析一文可以知道,WebKit在解析网页时碰到<embed>标签时,就会请求Browser进程启动一个Plugin进程加载该<embed>标签描述的Plugin。Plugin进程启动起来之后,会通过Unix Socket分别与Browser进程以及负责加载网页的Render进程建立通信通道,如图2所示:


图2 Plugin进程、Render进程和Browser进程的关系

       Plugin进程启动之后,主要涉及到的是它与Render进程之间的通信。首先,Render进程会请求Plugin进程加载指定的Plugin Module。Plugin Module加载完成之后,Render进程再请求Plugin进程创建Plugin Instance。Plugin Instance创建出来之后,就会进入运行状态。在运行的过程中,它会不断地通过Chromium提供的PPAPI接口请求Render进程执行相应的操作,从而完成自身的功能。

      要理解Chromium的Plugin机制,重点在于掌握前面我们提到的两个基本概念Plugin Module和Plugin Instance,以及Plugin Instance调用PPAPI接口的过程。为了更好地理解Chromium的Plugin机制,我们结合Chromium在源码中提供的一个GLES2 Example进行说明。

      每一个Plugin都对应有一个Module,每一个Module又可以创建多个Instance,如图3所示:


图3 Plugin Module与Plugin Instance的关系

       网页中的每一个<embed>标签在Render进程中都对应有一个PepperPluginInstanceImpl对象。这些PepperPluginInstanceImpl对象在Plugin进程中又都会有一个对应的pp::Instance对象。这些PepperPluginInstanceImpl对象和pp::Instance对象就是用来描述Plugin Instance的。

       当PepperPluginInstanceImpl与pp::Instance需要通信时,就会通过图2所示的Unix Socket相互发送IPC消息。这些IPC消息的发送与接收在Render进程一侧由一个HostDispatcher负责,而在Plugin进程一侧由一个PluginDispatcher负责。

       具有相同src值的<embed>标签对应的Plugin Instance属于同一个Plugin Module。Render进程为<embed>标签创建Plugin Instance时,首先会检查要创建的Plugin Instance所对应的Plugin Module是否已经加载。如果还没有加载,那么就会按照图4所示的流程进行加载:


图4 Plugin Module的加载流程

       为<embed>标签创建Plugin Instance的请求是由WebKit发起的。这个请求会交给Content层处理。Content层检测到要创建的Plugin Instance对应的Plugin Module还没有加载时,就会创建一个Out-of-Process Plugin Module。这里我们假设<embed>标签描述的Plugin是一个非内置Plugin,因此需要为它创建一个Out-of-Process Plugin Module。

       Content层在创建Out-of-Process Plugin Module的过程中,会请求Browser进程创建一个Plugin进程,并且请求在该Plugin进程中加载Plugin Module。Plugin Module实际上就是一个SO文件。这个SO文件加载完成后,Plugin进程会调用由它导出的一个函数PPP_InitializeModule,用来对刚刚加载的Plugin Module进行初始化。

       其中的一个初始化操作就是创建一个pp::Module对象。每一个Plugin都要在自己的Module文件中自定义一个pp::Module类。例如,GLES2 Example自定义的pp::Module类为GLES2DemoModule,如下所示:

// This object is the global object representing this plugin library as long
// as it is loaded.
class GLES2DemoModule : public pp::Module {
 public:
  GLES2DemoModule() : pp::Module() {}
  virtual ~GLES2DemoModule() {}

  virtual pp::Instance* CreateInstance(PP_Instance instance) {
    return new GLES2DemoInstance(instance, this);
  }
};
      这个类定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

      同时,每一个Plugin还必须要在自己的Module文件中定义一个函数CreateModule。这个函数就是用来创建自定义的pp::Module对象的。例如,GLES2 Example定义的函数CreateModule的实现如下所示:

namespace pp {
// Factory function for your specialization of the Module object.
Module* CreateModule() {
  return new GLES2DemoModule();
}
}  // namespace pp
     这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

     有了这个自定义的pp::Module对象之后,以后就可以调用它的成员函数CreateInstance创建Plugin Instance了。GLES2 Example用GLES2DemoInstance类来描述Plugin Instance。这个GLES2DemoInstance类必须要继承于pp::Instance类,它的定义如下所示:

class GLES2DemoInstance : public pp::Instance,
                          public pp::Graphics3DClient {

 public:
  GLES2DemoInstance(PP_Instance instance, pp::Module* module);
  virtual ~GLES2DemoInstance();

  ......
};
       这个类定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

       Plugin Module的其它初始化工作还包括初始化一系列的PPAPI接口。这些PPAPI接口可以被自定义的pp::Instance调用。

       回到Render进程中,当Plugin Module加载完成后,Render进程就会基于刚刚加载的Plugin Module创建一个Plugin Instance,也就是一个PepperPluginInstanceImpl对象。这个PepperPluginInstanceImpl对象接下来会被初始化。在初始化的过程中,它会请求Plugin进程创建一个对应的pp::Instance对象,如图5所示:

图5 Plugin Instance的创建过程

       Render进程存在一个PPP_INSTANCE_INTERFACE_1_1接口,PepperPluginInstanceImpl对象会通过该接口请求Plugin进程创建一个对应的pp::Instance对象。

       PPP_INSTANCE_INTERFACE_1_1接口定义了一个DidCreate操作。执行该操作的时候,Render进程就会向Plugin进程发送一个类型PpapiMsg_PPPInstance_DidCreate的IPC消息。该IPC消息携带了一个API_ID_PPP_INSTANCE参数,表示需要将它分发给一个PPP_Instance_Proxy对象处理。

       Plugin进程也存在一个PPP_INSTANCE_INTERFACE_1_1接口。PPP_Instance_Proxy对象接收到类型PpapiMsg_PPPInstance_DidCreate的IPC消息时,就会执行该接口的DidCreate操作。这个操作在执行的过程中,就会调用前面在加载Plugin Module时创建的自定义pp::Module对象的成员函数CreateInstance,结果就会获得一个自定义的pp::Instance对象。

      从前面的分析可以知道,对于GLES2 Example来说,它自定义的pp::Instance对象是一个GLES2DemoInstance对象。这个GLES2DemoInstance对象会通过Chromium提供的OpenGL ES 2.0 PPAPI接口为<embed>标签上绘制内容。

      Plugin进程在加载Module的时候,会初始化一个PPB_OPENGLES_INTERFACE接口。该接口描述的就是一个OpenGL ES 2.0 PPAPI接口,它定义了一系列的OpenGL操作。每一个操作都对应一个OpenGL函数,如图6所示:


图6 OpenGL ES 2.0 PPAPI接口的调用过程

       这些OpenGL操作又是通过调用GLES2Implementation类的同名成员函数实现的。例如,PPB_OPENGLES_INTERFACE接口定义的ActiveTexture操作,是通过调用GLES2Implementation类的成员函数ActiveTexture实现的。

       从前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文可以知道,GLES2Implementation类描述的是一个Comand Buffer OpenGL接口,它的成员函数被调用的时候,实际上只是将要执行的OpenGL命令写入到一个Command Buffer中去,然后通知GPU进程执行该Command Buffer中的OpenGL命令。

       不过,图6所示的GLES2Implementation对象并不是将Command Buffer的OpenGL命令直接传递给GPU进程执行的,而通过一个PpapCommandBufferProxy对象传递给Render进程中的一个CommandBufferProxyImpl对象处理。这个CommandBufferProxyImpl对象又会将传递给它的OpenGL命令写入到自己的Command Buffer中去,然后再通知GPU进程中的一个GpuCommandBufferStub对象执行该Command Buffer中的OpenGL命令。

       由此可见,Plugin通过PPB_OPENGLES_INTERFACE接口执行OpenGL命令时,要执行的OpenGL命令会先传递给Render进程,Render进程再传递给GPU进程执行。为什么Plugin不直接将OpenGL命令传递给GPU进程执行呢?这是因为Plugin在运行的过程中,只能够与Render进程交互。因此,当它需要请求其它进程执行某一个操作时,就需要通过Render进程间接执行。

       Plugin在执行OpenGL命令之前,首先要初始化一个OpenGL环境。这个初始化操作发生在Plugin对应的<embed>标签的视图创建完成时。这时候<embed>标签在Render进程中对应的PepperPluginInstanceImpl对象就会通过PPP_INSTANCE_INTERFACE_1_1接口向它在Plugin进程中对应的pp::Instance对象发出通知,如图7所示:

图7 Plugin初始化OpenGL环境的过程

       PPP_INSTANCE_INTERFACE_1_1接口定义了一个DidChangeView操作。当<embed>标签的视图第一次创建或者以后大小发生变化时,该操作都会被执行。在执行的时候,它会向Plugin进程发送一个类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息。这个IPC消息携带了一个APP_ID_PPP_INSTANCE参数,表示需要将它分发给一个PPP_Instance_Proxy对象处理。

       前面提到,Plugin进程也存在一个PPP_INSTANCE_INTERFACE_1_1接口。PPP_Instance_Proxy对象接收到类型PpapiMsg_PPPInstance_DidChangeView的IPC消息时,就会执行该接口的DidChangeView操作。这个操作在执行的过程中,又会找到之前加载Plugin Instance时创建的一个自定义pp::Instance对象,并且调用它的成员函数DidChangeView,用来通知它<embed>标签的视图大小发生了变化。

       在GLES2 Example中,自定义的pp::Instance对象是一个GLES2DemoInstance对象。当这个GLES2DemoInstance对象的成员函数DidChangeView第一次被调用的时候,会做两件事情。第一件事情是创建一个Graphics3D对象。这个Graphics3D对象在Plugin进程中用来描述一个OpenGL环境。第二件事情是将前面创建的OpenGL环境与当前正在处理的Plugin进行绑定。这两件事情完成之后,一个Plugin的OpenGL环境就创建和初始化完成了。

       Plugin的OpenGL环境的创建过程如图8所示:


图8 Plugin的OpenGL环境的创建过程

       Plugin进程存在一个PPB_GRAPHICS_3D_INTERFACE_1_0接口。PPB_GRAPHICS_3D_INTERFACE_1_0接口定义了一个Create操作。在Plugin进程中创建OpenGL环境,也就是一个Graphics3D对象的时候,上述的Create操作会被执行。在执行的过程中,它会通过一个APP_ID_RESOURCE_CREATION参数找到一个ResourceCreationProxy对象,然后再通过这个ResourceCreationProxy对象向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息。这个IPC消息携带了一个APP_ID_PPB_GRAPHICS_3D参数,表示要将它分发给一个PPB_Graphics3D_Proxy对象处理。

       PPB_Graphics3D_Proxy对象在处理类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息时,会创建一个PPB_Graphics3D_Impl对象。这个PPB_Graphics3D_Impl对象在创建的过程中,会与GPU进程建立一个GPU通道。这个GPU通道建立起来之后,就会在GPU进程中获得一个OpenGL环境。这个OpenGL环境就是用来执行Plugin发出的OpenGL命令的。Render进程与GPU进程建立GPU通道的过程,可以参考前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文。

       给Plugin绑定OpenGL环境的过程如图9所示:


图9 Plugin绑定OpenGL环境的过程

       Plugin进程存在一个PPB_INSTANCE_INTERFACE_1_1接口。PPP_INSTANCE_INTERFACE_1_1接口定义了一个BindGraphics操作。该操作在执行的时候,会通过一个APP_ID_PPB_INSTANCE参数找到一个PPB_Instance_Proxy对象,然后再通过这个PPB_Instance_Proxy对象向Render进程发送一个类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息。这个IPC消息携带了一个APP_ID_PPB_INSTANCE参数,表示要将它分发给一个PPB_Instance_Proxy对象处理。

       PPB_Instance_Proxy对象在处理类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息,会找到要绑定OpenGL环境的Plugin Instance,也就是一个PepperPluginInstanceImpl对象,然后调用这个PepperPluginInstanceImpl对象的成员函数BindToInstance,将其绑定到指定的OpenGL环境中去,也就是与前面创建的一个PPB_Graphics3D_Impl对象建立关联。

       这样,我们就介绍完了Chromium的Plugin机制。由于Chromium已不再支持NPAPI Plugin,这里我们说的Plugin,指的是PPAPI Plugin,也称为Pepper Plugin。Pepper Plugin可以通过NaCl SDK开发,也可以通过PPAPI SDK开发。通过NaCl SDK开发的Pepper Plugin在NaCl环境中运行,受到两个Sandbox保护,因此它的安全性是非常高的。在Web Store中分发的Pepper Plugin必须要通过NaCl SDK开发,以及运行在NaCl环境中。内置的Pepper Plugin,或者仅仅在本地安装的Pepper Plugin,可以不运行在NaCl环境中,因此它们可以通过PPAPI SDK进行开发。

       不管是否运行在NaCl环境中,Pepper Plugin使用Chromium提供的PPAPI接口的方式都是一样的。换句话说,就是NaCl环境对Pepper Plugin来说是透明的。为了简单起见,在接下来的文章中,我们假设Pepper Plugin是基于PPAPI SDK开发的,也就是它没有运行在NaCl环境中。

       接下来,我们将结合文中提到的GLES2 Example,按照以下三个情景,对Chromium的Pepper Plugin机制进行更详细的分析:

       1. Plugin Module的加载过程

       2. Plugin Instance的创建过程

       3. Pugin Instance的3D渲染过程

       其中,前面两个情景有助于我们理清Plugin Module与Plugin Instance的关系,第三个情景有助于我们理解Plugin调用PPAPI接口的过程,也就是Plugin与浏览器的交互过程。理解了这三个情景之后,我们就可以深刻理解Chromium的Pepper Plugin机制了。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/10/24 0:59:13 原文链接
阅读:36175 评论:4 查看评论

Chromium插件(Plugin)模块(Module)加载过程分析

$
0
0

       在Chromium中,每一个Plugin都对应一个Module,称为Plugin Module。一个Plugin Module可创建多个Plugin Instance。每一个Plugin Instance对应于网页中的一个<embed>标签。在为<embed>标签创建Plugin Instance之前,先要加载其对应的Plugin Module。本文接下来分析Plugin Module的加载过程。

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

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

       Plugin Module的加载过程如图1所示:


图1 Plugin Module的加载过程

       WebKit请求Content层为网页中的<embed>标签创建Plugin Instance时,Content层会检查要创建的Plugin Instance对应的Plugin Module是否已经加载。如果还没有加载,那么通常就会创建一个Out-of-Process Plugin Module。这里说通常,是因为大部分Plugin都是运行在一个独立的进程中,只有内置Plugin才允许运行在Render进程中。本文我们只讨论Out-of-Process Plugin Module的情形。

       Content层在创建Out-of-Process Plugin Module的过程中,会请求Browser进程创建一个Plugin进程,并且请求该Plugin进程加载指定的Plugin Module。每一个Plugin Module都会有一个导出函数PPP_InitializeModule。Plugin Module加载完成之后,它导出的函数PPP_InitializeModule就会被调用,用来执行初始化工作。

       Plugin进程启动起来之后,它与Render进程之间的通信是通过一个PluginDispatcher对象进行的。与此同时,Render进程也会通过一个HostDispatcher对象与Plugin进程进行通信。例如,Content层请求Plugin进程加载指定的Plugin Module,就是通过Render进程中的HostDispatcher对象向Plugin进程中的PluginDispatcher对象发送IPC消息进行的。

       Content层在请求Browser进程创建一个Plugin进程加载一个Plugin Module之前,必须要知道这个Plugin Module的信息,例如它的SO文件路径。用户当前安装的所有Plugin是由Chromium中的一个Plugin Service进行管理的,因此Content层可以通过这个Plugin Service获得要加载的Plugin Module的信息。

       在分析Plugin Module的加载之前,我们先分析Plugin Service的启动过程。在启动的过程中,它就会注册所有内建(Built-In)的Plugin以及用户安装的Plugin在内部的一个List中。从前面Chromium扩展(Extension)加载过程分析一文可以知道,Chromium的Browser进程在启动的时候,会调用BrowserMainLoop类的成员函数CreateStartupTasks创建一系列的Startup Task。其中的一个Startup Task就是用来启动Plugin Service的,如下所示:

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_create_threads =
        base::Bind(&BrowserMainLoop::PreCreateThreads, base::Unretained(this));
    startup_task_runner_->AddTask(pre_create_threads);

    ......
  }
#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中。

       这个Startup Task绑定了BrowserMainLoop类的成员函数PreCreateThreads,在创建各种Browser线程之前执行,执行过程如下所示:

int BrowserMainLoop::PreCreateThreads() {
  ......

#if defined(ENABLE_PLUGINS)
  // Prior to any processing happening on the io thread, we create the
  // plugin service as it is predominantly used from the io thread,
  // but must be created on the main thread. The service ctor is
  // inexpensive and does not invoke the io_thread() accessor.
  {
    TRACE_EVENT0("startup", "BrowserMainLoop::CreateThreads:PluginService");
    PluginService::GetInstance()->Init();
  }
#endif

  ......
  return result_code_;
}
      这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

      从这里可以看到,只有在编译时定义了宏ENABLE_PLUGINS,Chromium才会支持Plugin机制。在这种情况下,BrowserMainLoop类的成员函数PreCreateThreads首先会调用PluginService类的静态成员函数GetInstance获得一个PluginServiceImpl对象,如下所示:

PluginService* PluginService::GetInstance() {
  return PluginServiceImpl::GetInstance();
}
       这个函数定义在文件external/chromium_org/content/browser/plugin_service_impl.cc中。

       PluginService的静态成员函数GetInstance又是通过调用PluginServiceImpl类的静态成员函数GetInstance获得一个PluginServiceImpl对象的,如下所示:

PluginServiceImpl* PluginServiceImpl::GetInstance() {
  return Singleton<PluginServiceImpl>::get();
}
       这个函数定义在文件external/chromium_org/content/browser/plugin_service_impl.cc中。

       从这里可以看到,PluginServiceImpl类的静态成员函数GetInstance返回的PluginServiceImpl对象在当前进程(即Browser进程)是唯一的。这个PluginServiceImpl对象返回给BrowserMainLoop类的成员函数PreCreateThreads之后,后者会调用它的成员函数Init执行初始化工作,如下所示:

void PluginServiceImpl::Init() {
  ......

  RegisterPepperPlugins();

  ......
}

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

       PluginServiceImpl类的成员函数Init会调用另外一个成员函数RegisterPepperPlugins注册那些Built-In Plugin以及用户安装的Plugin到Plugin Service中去,如下所示:

void PluginServiceImpl::RegisterPepperPlugins() {
  ComputePepperPluginList(&ppapi_plugins_);
  for (size_t i = 0; i < ppapi_plugins_.size(); ++i) {
    RegisterInternalPlugin(ppapi_plugins_[i].ToWebPluginInfo(), true);
  }
}
       这个函数定义在文件external/chromium_org/content/browser/plugin_service_impl.cc中。

       PluginServiceImpl类的成员函数RegisterPepperPlugins首先调用函数ComputePepperPluginList获得那些Built-In Plugin和用户安装的Plugin,如下所示:

void ComputePepperPluginList(std::vector<PepperPluginInfo>* plugins) {
  GetContentClient()->AddPepperPlugins(plugins);
  ComputePluginsFromCommandLine(plugins);
}
      这个函数定义在文件external/chromium_org/content/common/pepper_plugin_list.cc中。

      这里我们假设当前分析的是Chrome浏览器。在这种情况下,PluginServiceImpl类的成员函数RegisterPepperPlugins调用函数GetContentClient获得的是一个ChromeContentClient对象。调用这个ChromeContentClient对象的成员函数AddPepperPlugin即可以获得Built-In Plugin,如下所示:

void ChromeContentClient::AddPepperPlugins(
    std::vector<content::PepperPluginInfo>* plugins) {
  ComputeBuiltInPlugins(plugins);
  AddPepperFlashFromCommandLine(plugins);

  content::PepperPluginInfo plugin;
  if (GetBundledPepperFlash(&plugin))
    plugins->push_back(plugin);
}

      这个函数定义在文件external/chromium_org/chrome/common/chrome_content_client.cc中。

      ChromeContentClient类的成员函数AddPepperPlugin首先调用函数ComputeBuiltInPlugins获得一些重要的Built-In Plugin,例如NaCl Plugin和PDF Plugin,如下所示:

void ComputeBuiltInPlugins(std::vector<content::PepperPluginInfo>* plugins) {
  // PDF.
  //
  // Once we're sandboxed, we can't know if the PDF plugin is available or not;
  // but (on Linux) this function is always called once before we're sandboxed.
  // So the first time through test if the file is available and then skip the
  // check on subsequent calls if yes.
  static bool skip_pdf_file_check = false;
  base::FilePath path;
  if (PathService::Get(chrome::FILE_PDF_PLUGIN, &path)) {
    if (skip_pdf_file_check || base::PathExists(path)) {
      content::PepperPluginInfo pdf;
      pdf.path = path;
      pdf.name = ChromeContentClient::kPDFPluginName;
      if (CommandLine::ForCurrentProcess()->HasSwitch(
              switches::kOutOfProcessPdf)) {
        pdf.is_out_of_process = true;
        content::WebPluginMimeType pdf_mime_type(kPDFPluginOutOfProcessMimeType,
                                                 kPDFPluginExtension,
                                                 kPDFPluginDescription);
        pdf.mime_types.push_back(pdf_mime_type);
        // TODO(raymes): Make print preview work with out of process PDF.
      } else {
        content::WebPluginMimeType pdf_mime_type(kPDFPluginMimeType,
                                                 kPDFPluginExtension,
                                                 kPDFPluginDescription);
        content::WebPluginMimeType print_preview_pdf_mime_type(
            kPDFPluginPrintPreviewMimeType,
            kPDFPluginExtension,
            kPDFPluginDescription);
        pdf.mime_types.push_back(pdf_mime_type);
        pdf.mime_types.push_back(print_preview_pdf_mime_type);
      }
      pdf.permissions = kPDFPluginPermissions;
      plugins->push_back(pdf);

      skip_pdf_file_check = true;
    }
  }

  // Handle Native Client just like the PDF plugin. This means that it is
  // enabled by default for the non-portable case.  This allows apps installed
  // from the Chrome Web Store to use NaCl even if the command line switch
  // isn't set.  For other uses of NaCl we check for the command line switch.
  // Specifically, Portable Native Client is only enabled by the command line
  // switch.
  static bool skip_nacl_file_check = false;
  if (PathService::Get(chrome::FILE_NACL_PLUGIN, &path)) {
    if (skip_nacl_file_check || base::PathExists(path)) {
      content::PepperPluginInfo nacl;
      nacl.path = path;
      nacl.name = ChromeContentClient::kNaClPluginName;
      content::WebPluginMimeType nacl_mime_type(kNaClPluginMimeType,
                                                kNaClPluginExtension,
                                                kNaClPluginDescription);
      nacl.mime_types.push_back(nacl_mime_type);
      if (!CommandLine::ForCurrentProcess()->HasSwitch(
              switches::kDisablePnacl)) {
        content::WebPluginMimeType pnacl_mime_type(kPnaclPluginMimeType,
                                                   kPnaclPluginExtension,
                                                   kPnaclPluginDescription);
        nacl.mime_types.push_back(pnacl_mime_type);
      }
      nacl.permissions = kNaClPluginPermissions;
      plugins->push_back(nacl);

      skip_nacl_file_check = true;
    }
  }

  ......
}
       这个函数定义在文件external/chromium_org/chrome/common/chrome_content_client.cc。

       除了NaCl Plugin和PDF Plugin,还有一个重析Built-In Plugin,就是Flash Plugin。回到ChromeContentClient对象的成员函数AddPepperPlugin中,它首先调用函数AddPepperFlashFromCommandLine检查Browser进程的命令行参数是否包含了switches::kPpapiFlashPath(“ppapi-flash-path”)启动选项。如果包含了,那么该选项的值就指定了一个Flash Plugin作为Chromium默认使用的Flash Plugin,如下所示:

void AddPepperFlashFromCommandLine(
    std::vector<content::PepperPluginInfo>* plugins) {
  const CommandLine::StringType flash_path =
      CommandLine::ForCurrentProcess()->GetSwitchValueNative(
          switches::kPpapiFlashPath);
  if (flash_path.empty())
    return;

  // Also get the version from the command-line. Should be something like 11.2
  // or 11.2.123.45.
  std::string flash_version =
      CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kPpapiFlashVersion);

  plugins->push_back(
      CreatePepperFlashInfo(base::FilePath(flash_path), flash_version));
}
       这个函数定义在文件external/chromium_org/chrome/common/chrome_content_client.cc。

       如果Browser进程的命令行参数没有包含switches::kPpapiFlashPath启动选项,那么ChromeContentClient类的成员函数AddPepperPlugin将会调用另外一个函数GetBundledPepperFlash获得一个由Chromium自己实现的Flash Plugin,如下所示:

bool GetBundledPepperFlash(content::PepperPluginInfo* plugin) {
#if defined(FLAPPER_AVAILABLE)
  CommandLine* command_line = CommandLine::ForCurrentProcess();

  // Ignore bundled Pepper Flash if there is Pepper Flash specified from the
  // command-line.
  if (command_line->HasSwitch(switches::kPpapiFlashPath))
    return false;

  ......

  base::FilePath flash_path;
  if (!PathService::Get(chrome::FILE_PEPPER_FLASH_PLUGIN, &flash_path))
    return false;

  *plugin = CreatePepperFlashInfo(flash_path, FLAPPER_VERSION_STRING);
  return true;
#else
  return false;
#endif  // FLAPPER_AVAILABLE
} 
       这个函数定义在文件external/chromium_org/chrome/common/chrome_content_client.cc。

       这个Flash Plugin称为Flapper。只有在编译Chromium时,定义了宏FLAPPER_AVAILABLE,Chromium才会包含这个名为Flapper的Flash Plugin。并且只有在Browser进程的命令行参数没有包含switches::kPpapiFlashPath启动选项时,这个名为Flapper的Flash Plugin才会被使用。

       这一步执行完成后,Chromium就获得了所有的Built-In Plugin。回到前面分析的函数ComputePepperPluginList中,它接下来调用函数ComputePluginsFromCommandLine获得用户安装的Plugin,如下所示:

void ComputePluginsFromCommandLine(std::vector<PepperPluginInfo>* plugins) {
  ......

  bool out_of_process = true;
  if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kPpapiInProcess))
    out_of_process = false;

  const std::string value =
      CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kRegisterPepperPlugins);
  if (value.empty())
    return;

  // FORMAT:
  // command-line = <plugin-entry> + *( LWS + "," + LWS + <plugin-entry> )
  // plugin-entry =
  //    <file-path> +
  //    ["#" + <name> + ["#" + <description> + ["#" + <version>]]] +
  //    *1( LWS + ";" + LWS + <mime-type> )
  std::vector<std::string> modules;
  base::SplitString(value, ',', &modules);
  ......

  for (size_t i = 0; i < plugins_to_register; ++i) {
    std::vector<std::string> parts;
    base::SplitString(modules[i], ';', &parts);
    ......

    std::vector<std::string> name_parts;
    base::SplitString(parts[0], '#', &name_parts);

    PepperPluginInfo plugin;
    plugin.is_out_of_process = out_of_process;  
#if defined(OS_WIN)
    // This means we can't provide plugins from non-ASCII paths, but
    // since this switch is only for development I don't think that's
    // too awful.
    plugin.path = base::FilePath(base::ASCIIToUTF16(name_parts[0]));
#else
    plugin.path = base::FilePath(name_parts[0]);
#endif

    ......

    plugins->push_back(plugin);
  }
}
       这个函数定义在文件external/chromium_org/content/common/pepper_plugin_list.cc中。

       如果Chromium允许加载Plugin,那么它的Browser进程在启动的时候,会将用户安装的Plugin收集起来,并且会将收集到的信息设置在一个switches::kRegisterPepperPlugins启动选项中。函数函数ComputePluginsFromCommandLine通过解析这个启动选项,就可以获得用户安装的Plugin了。

       在默认情况下,用户安装的Plugin都是要运行在一个独立的Plugin进程中的。不过,如果Browser进程的命令行参数包含了switches::kPpapiInProcess启动选项,那么它们就不会运行在一个独立的Plugin进程中,而是直接运行在Render进程中。

       这一步执行完成之后,Chromium就获得了所有的Built-In Plugin以及用户安装的Plugin。回到PluginServiceImpl类的成员函数RegisterPepperPlugins中,它接下来就会将前面获得每一个Built-In Plugin以及用户安装的Plugin注册在Plugin Service内部的一个List中。这是通过调用PluginServiceImpl类的成员函数RegisterInternalPlugin实现的,如下所示:

void PluginServiceImpl::RegisterInternalPlugin(
    const WebPluginInfo& info,
    bool add_at_beginning) {
  ......
  PluginList::Singleton()->RegisterInternalPlugin(info, add_at_beginning);
}
       这个函数定义在文件external/chromium_org/content/browser/plugin_service_impl.cc中。

       参数info指向的一个WebPluginInfo对象描述的就是前面获得的一个Built-In Plugin或者用户安装的Plugin。Browser进程存在一个PluginList单例对象。这个PluginList单例对象描述的就是Plugin Service内部用来保存Plugin信息的一个List。因此,调用这个PluginList单例对象的成员函数RegisterInternalPlugin就可以将参数info描述的Plugin注册在Plugin Service的内部,如下所示:

void PluginList::RegisterInternalPlugin(const WebPluginInfo& info,
                                        bool add_at_beginning) {
  base::AutoLock lock(lock_);

  internal_plugins_.push_back(info);
  if (add_at_beginning) {
    // Newer registrations go earlier in the list so they can override the MIME
    // types of older registrations.
    extra_plugin_paths_.insert(extra_plugin_paths_.begin(), info.path);
  } else {
    extra_plugin_paths_.push_back(info.path);
  }
}
      这个函数定义在文件external/chromium_org/content/common/plugin_list.cc中。

      从前面的分析可以知道,参数info描述的不管是Built-In Plugin还是用户安装的Plugin,它们都是Pepper Plugin。Pepper Plugin在PluginList类中称为Internal Plugin。这些Internal Plugin将会保存在成员变量internal_plugins_描述的一个std::vector中。如果Chromium支持NPAPI Plugin,则它们的信息就不会保存在这个std::vector中,而是保存在另外一个成员变量extra_plugin_paths_描述的一个std::vector中。

      注意,Pepper Plugin的信息同样会保存在成员变量extra_plugin_paths_描述的std::vector中。这样,当Chromium需要获得一个Plugin的信息时,不管它是Pepper Plugin,还是NPAPI Plugin,均可以通过成员变量extra_plugin_paths_描述的std::vector获得。

       这一步执行完成后,Chromium的Browser进程就初始化好了一个Plugin Service,并且将所有的Built-In Plugin和用户安装的Plugin注册在了这个Plugin Service内部的一个List中。这个List称为Plugin List。接下来,我们就继续分析Plugin Module的加载过程。

       根据前面Chromium网页DOM Tree创建过程分析一文的分析,我们可以知道,WebKit在解析网页的时候,如果碰到一个<embed>标签,那么就会为它创建一个HTMLEmbedElement对象,作为网页DOM Tree中的一个DOM节点。当需要为<embed>标签创建Plugin Instance时,前面为其创建的HTMLEmbedElement对象的成员函数loadPlugin就会被调用。

       HTMLEmbedElement类的成员函数loadPlugin是从父类HTMLPlugInElement继承下来的,它的实现如下所示:

bool HTMLPlugInElement::loadPlugin(const KURL& url, const String& mimeType, const Vector<String>& paramNames, const Vector<String>& paramValues, bool useFallback, bool requireRenderer)
{
    LocalFrame* frame = document().frame();
    ......

    RefPtr<Widget> widget = m_persistedPluginWidget;
    if (!widget) {
        ......
        widget = frame->loader().client()->createPlugin(this, url, paramNames, paramValues, mimeType, loadManually, policy);
    }

    ......

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

       HTMLEmbedElement类的成员函数loadPlugin首先调用成员函数document获得当前网页的Document对象。有了这个Document对象之后,就可以调用它的成员函数frame获得一个LocalFrame对象。调用这个LocalFrame对象的成员函数loader又可以获得一个FrameLoader对象。调用这个FrameLoader对象的成员函数client又可以获得一个FrameLoaderClientImpl对象。通过这个FrameLoaderClientImpl对象,WebKit可以请求它的使用者,即Chromium的Content层,执行指定的操作。例如,HTMLEmbedElement类的成员函数loadPlugin就是通过调用这个FrameLoaderClientImpl的成员函数createPlugin请求Content层为当前正在处理的<embed>标签创建一个Plugin Instance的。

       注意,当HTMLEmbedElement类的成员变量m_persistedPluginWidget的值不等于NULL时,它指向的是一个Widget对象。在这种情况下,就表示WebKit已经为当前正在处理的<embed>标签创建过Plugin Instance了。这时候就不要重复创建。

       接下来,我们继续分析WebKit为<embed>标签创建Plugin Instance的过程,也就是FrameLoaderClientImpl类的成员函数createPlugin的实现,如下所示:

PassRefPtr<Widget> FrameLoaderClientImpl::createPlugin(
    HTMLPlugInElement* element,
    const KURL& url,
    const Vector<String>& paramNames,
    const Vector<String>& paramValues,
    const String& mimeType,
    bool loadManually,
    DetachedPluginPolicy policy)
{
    ......

    WebPluginParams params;
    params.url = url;
    params.mimeType = mimeType;
    params.attributeNames = paramNames;
    params.attributeValues = paramValues;
    params.loadManually = loadManually;

    WebPlugin* webPlugin = m_webFrame->client()->createPlugin(m_webFrame, params);
    if (!webPlugin)
        return nullptr;

    // The container takes ownership of the WebPlugin.
    RefPtr<WebPluginContainerImpl> container =
        WebPluginContainerImpl::create(element, webPlugin);

    if (!webPlugin->initialize(container.get()))
        return nullptr;

    ......

    return container;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FrameLoaderClientImpl.cpp中。

       FrameLoaderClientImpl类的成员变量m_webFrame指向的是一个WebLocalFrameImpl对象。调用这个WebLocalFrameImpl对象的成员函数client可以获得一个WebFrameClient接口。这个WebFrameClient接口由WebKit的使用者实现的。在我们这个情景中,WebKit的使用者即为Chromium的Content层。调用这个WebFrameClient接口的成员函数createPlugin即可为参数element描述的<embed>标签创建一个Plugin Instance。

       Content层的RenderFrameImpl类实现了WebFrameClient接口。因此,WebKit实际上是通过调用RenderFrameImpl类的成员函数createPlugin为网页的<embed>标签创建Plugin Instance。在创建Plugin Instance的过程中,如果发现Plugin Module还没有加载,那么就会进行加载。我们假设这个Plugin Module是加载在一个独立的Plugin进程中的。这时候这个Plugin Module在Render进程中使用一个PluginModule对象描述,而在Plugin进程中使用一个pp::Module对象描述。这个加载过程可以参考前面Chromium的Plugin进程启动过程分析一文。

       Plugin Module加载完成之后,Content层就会请求创建它的一个Instance。从前面Chromium的Plugin进程启动过程分析一文可以知道,这个Plugin Instance在Render进程中使用一个PepperWebPluginImpl对象描述。

       有了上述的PepperWebPluginImpl对象之后, FrameLoaderClientImpl类的成员函数createPlugin接下来会调用它的成员函数initialize对它进行初始化。在初始化的过程中,Render进程会请求Plugin进程创建一个真正的Plugin Instance。从前面Chromium插件(Plugin)机制简要介绍和学习计划一文可以知道,这个Plugin Instance使用一个pp::Instance对象描述。

       在调用PepperWebPluginImpl类的成员函数initialize请求Plugin进程创建Plugin Instance之前,FrameLoaderClientImpl类的成员函数createPlugin会为参数element描述的<embed>标签创建一个类型为WebPluginContainerImpl的Widget。这个Widget用作<embed>标签的视图,最终会返回给FrameLoaderClientImpl类的成员函数createPlugin的调用者,即HTMLEmbedElement类的成员函数loadPlugin。

       从前面Chromium的Plugin进程启动过程分析一文可以知道,Plugin进程在加载了Plugin Module之后,会对该Plugin Module进行初始化,这是通过调用Plugin Module导出的函数PPP_InitializeModule实现的。

       函数PPP_InitializeModule的实现如下所示:

static pp::Module* g_module_singleton = NULL;
......

PP_EXPORT int32_t PPP_InitializeModule(PP_Module module_id,
                                       PPB_GetInterface get_browser_interface) {
  pp::Module* module = pp::CreateModule();
  if (!module)
    return PP_ERROR_FAILED;

  if (!module->InternalInit(module_id, get_browser_interface)) {
    delete module;
    return PP_ERROR_FAILED;
  }
  g_module_singleton = module;
  return PP_OK;
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/ppp_entrypoints.cc中。

       Chromium要求每一个Pepper Plugin都要导出一个pp::CreateModule函数。例如,我们在Chromium插件(Plugin)机制简要介绍和学习计划一文提到的GLES2 Example导出的函数pp::CreateModule的实现如下所示:

namespace pp {
// Factory function for your specialization of the Module object.
Module* CreateModule() {
  return new GLES2DemoModule();
}
}  // namespace pp
       这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

       这个导出的函数pp::CreateModule所要做的事情就是创建一个自定义的pp::Module。对于GLES2 Example,它自定义的pp::Module为GLES2DemoModule,也就是GLES2DemoModule类是从pp::Module类继承下来的。

       回到前面分析的函数PPP_InitializeModule中,通过Pepper Plugin导出的函数pp::CreateModule函数创建的自定义pp::Module将会保存在一个全局变量g_module_singleton中,以后通过这个全局变量就可以访问到Pepper Plugin自定义的pp::Module了。

       函数PPP_InitializeModule在将自定义pp::Module保存在全局变量g_module_singleton之前,还会对其执行初始化操作。这是通过调用它从父类pp::Module继承下来的成员函数InternalInit实现的。初始化过程如下所示:

bool Module::InternalInit(PP_Module mod,
                          PPB_GetInterface get_browser_interface) {
  pp_module_ = mod;
  get_browser_interface_ = get_browser_interface;

  // Get the core interface which we require to run.
  const PPB_Core* core = reinterpret_cast<const PPB_Core*>(GetBrowserInterface(
      PPB_CORE_INTERFACE));
  if (!core)
    return false;
  core_ = new Core(core);

  return Init();
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/module.cc中。

       参数mod的类型为PP_Module。PP_Module定义为一个int32_t,它是用来描述一个Plugin Module ID的。也就是说,每一个Plugin Module都有一个ID。这个ID将会保存在pp::Module类的成员变量pp_module_中。

       从前面Chromium的Plugin进程启动过程分析一文可以知道,参数get_browser_interface指向的是一个函数。这个函数为PluginDispatcher类的静态成员函数GetBrowserInterface,它将会保存在pp::Module类的成员变量get_browser_interface_中。以后通过这个函数可以获得Chromium提供给Plugin使用的接口。例如,pp::Module类的成员函数InternalInit接下来就会通过这个函数获得一个类型为PPB_CORE_INTERFACE的接口。这个接口会封装在一个Core对象中,并且这个Core对象会保存在pp::Module类的成员变量core_中。

       接下来,我们就继续分析Plugin获得类型为PPB_CORE_INTERFACE的接口的过程。从这个过程我们还可以看到Chromium提供给Plugin使用的其它类型的接口。pp::Module类的成员函数InternalInit获得类型为PPB_CORE_INTERFACE的接口是通过调用成员函数GetBrowserInterface实现的,如下所示:

const void* Module::GetBrowserInterface(const char* interface_name) {
  return get_browser_interface_(interface_name);
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/module.cc中。

       pp:Module类的成员函数GetBrowserInterface通过调用成员变量get_browser_interface_描述的函数获取类型为PPB_CORE_INTERFACE的接口。前面提到,pp::Module类的成员变量get_browser_interface_为PluginDispatcher类的静态成员函数GetBrowserInterface。因此,接下来我们继续分析PluginDispatcher类的静态成员函数GetBrowserInterface的实现,如下所示:

// static
const void* PluginDispatcher::GetBrowserInterface(const char* interface_name) {
  ......

  return InterfaceList::GetInstance()->GetInterfaceForPPB(interface_name);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/plugin_dispatcher.cc中。

       PluginDispatcher类的静态成员函数GetBrowserInterface首先调用InterfaceList类的静态成员函数GetInstance获得当前Plugin进程中的一个InterfaceList单例对象,如下所示:

// static
InterfaceList* InterfaceList::GetInstance() {
  return Singleton<InterfaceList>::get();
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

       InterfaceList类的静态成员函数GetInstance通过模板类Singleton<InterfaceList>的静态成员函数获得当前Plugin进程中的InterfaceList单例对象。这个InterfaceList单例对象如果还没有创建,那么此时就会进行创建。在创建的过程中,它将会初始化一系列的接口。这些接口都是Chromium提供给Plugin使用的,其中就包括前面提到的类型为PPB_CORE_INTERFACE的接口。

       接下来,我们就继续分析InterfaceList单例对象在创建过程中初始化接口的过程,如下所示:

InterfaceList::InterfaceList() {
  ......

  // Register the API factories for each of the API types. This calls AddProxy
  // for each InterfaceProxy type we support.
  #define PROXIED_API(api_name) \
      AddProxy(PROXY_API_ID(api_name), &PROXY_FACTORY_NAME(api_name));
  ......

  #define PROXIED_IFACE(iface_str, iface_struct) \
      AddPPB(iface_str, \
             INTERFACE_THUNK_NAME(iface_struct)(), \
             current_required_permission);
  ......

  {
    Permission current_required_permission = PERMISSION_NONE;
    ......
    #include "ppapi/thunk/interfaces_ppb_public_stable.h"
  }

  AddProxy(API_ID_RESOURCE_CREATION, &ResourceCreationProxy::Create);
  ......
  AddPPB(PPB_CORE_INTERFACE_1_0,
         PPB_Core_Proxy::GetPPB_Core_Interface(), PERMISSION_NONE);
  ......
  AddPPB(PPB_OPENGLES2_INTERFACE_1_0,
         PPB_OpenGLES2_Shared::GetInterface(), PERMISSION_NONE);
  ......
  AddProxy(API_ID_PPP_INSTANCE, &ProxyFactory<PPP_Instance_Proxy>);  

  ......
}
      这个函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      Chromium提供给Plugin使用的接口很多,这里我们只关注特定的几个接口。这几个接口与接下来我们分析Plugin Instance的创建以及Plugin的3D渲染有关。

      第一个接口是一个Instance通信接口,接口ID为API_ID_PPB_INSTANCE。当Plugin进程中的Plugin Instance要与Render进程中对应的Plugin Instance通信时,就需要调用到该接口。这个接口是通过include文件interfaces_ppb_public_stable.h定义的,如下所示:

PROXIED_API(PPB_Instance);
      这个接口定义在文件external/chromium_org/ppapi/thunk/interfaces_ppb_public_stable.h中。

      PROXIED_API是一个宏,定义在前面分析的InterfaceList类的构造函数中,展开后为:

AddProxy(PROXY_API_ID(PPB_Instance), &PROXY_FACTORY_NAME(PPB_Instance));
      PROXY_API_ID是一个宏,定义在为:

#define PROXY_API_ID(api_name) PROXY_CLASS_NAME(api_name)::kApiID

      这个宏定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      PROXY_FACTORY_NAME也是一个宏,定义为:

#define PROXY_FACTORY_NAME(api_name) ProxyFactory<PROXY_CLASS_NAME(api_name)>
      这个宏定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      对PROXY_API_ID和PROXY_FACTORY_NAME这两个宏进行展开,得到接口API_ID_PPB_INSTANCE的定义为:

AddProxy(PROXY_CLASS_NAME(PPB_Instance)::kApiID, &ProxyFactory<PROXY_CLASS_NAME(PPB_Instance)>)
      PROXY_CLASS_NAME是一个宏,定义为:

#define PROXY_CLASS_NAME(api_name) api_name##_Proxy
      这个宏定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      对PROXY_CLASS_NAME这个宏进行展开,得到接口API_ID_PPB_INSTANCE的定义为:

AddProxy(PPB_Instance_Proxy::kApiID, &ProxyFactory<PPB_Instance_Proxy>);
      PPB_Instance_Proxy::kApiID定义在PPB_Instance_Proxy类内部的一个常量,如下所示:

class PPB_Instance_Proxy : public InterfaceProxy,
                           public PPB_Instance_Shared {
  ......

  static const ApiID kApiID = API_ID_PPB_INSTANCE;

  ......
};
      这个常量定义在文件external/chromium_org/ppapi/proxy/ppb_instance_proxy.h中。

      对PPB_Instance_Proxy::kApiID进行常量替换后,得到接口API_ID_PPB_INSTANCE的定义为:

AddProxy(PPB_Instance_Proxy::kApiID, &ProxyFactory<PPB_Instance_Proxy>);
      实际上就是将其定义为一个模板函数ProxyFactory<PPB_Instance_Proxy>,以后调用这个模板函数将会得到一个PPB_Instance_Proxy对象,如下所示:

template<typename ProxyClass>
InterfaceProxy* ProxyFactory(Dispatcher* dispatcher) {
  return new ProxyClass(dispatcher);
}
      这个模板函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      注意,这时候模板参数ProxyClass指定为PPB_Instance_Proxy,因此在调用模板函数ProxyFactory<PPB_Instance_Proxy>时,将会得到一个PPB_Instance_Proxy对象。

      上述模板函数ProxyFactory<PPB_Instance_Proxy>将会通过InterfaceList类的成员函数AddProxy保存在内部一个InterfaceProxy::Factory数组中,如下所示:

void InterfaceList::AddProxy(ApiID id,
                             InterfaceProxy::Factory factory) {
  ......

  int index = static_cast<int>(id);
  ......

  id_to_factory_[index] = factory;
}

      这个函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      相当于执行了以下操作:

id_to_factory_[API_ID_PPB_INSTANCE] = &ProxyFactory<PPB_Instance_Proxy>;

      这样,我们就可以知道第一个接口API_ID_PPB_INSTANCE对应的函数为ProxyFactory<PPB_Instance_Proxy>,以后调用这个接口将会得到一个PPB_Instance_Proxy对象。

      第二个接口是一个3D图形接口,接口ID为PPB_GRAPHICS_3D_INTERFACE_1_0。当Plugin进行3D渲染时,就需要调用到这个接口。这个接口也是通过include文件interfaces_ppb_public_stable.h定义的,如下所示:

PROXIED_IFACE(PPB_GRAPHICS_3D_INTERFACE_1_0, PPB_Graphics3D_1_0)
      这个接口定义在文件external/chromium_org/ppapi/thunk/interfaces_ppb_public_stable.h中。

      PROXIED_IFACE是一个宏,定义在前面分析的InterfaceList类的构造函数中,展开后为:

AddPPB(PPB_GRAPHICS_3D_INTERFACE_1_0, INTERFACE_THUNK_NAME(PPB_Graphics3D_1_0)(), PERMISSION_NONE);
      INTERFACE_THUNK_NAME是一个宏,定义为:

#define INTERFACE_THUNK_NAME(iface_struct) thunk::Get##iface_struct##_Thunk
      这个宏定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

      对INTERFACE_THUNK_NAME这个宏进行展开,得到接口PPB_GRAPHICS_3D_INTERFACE_1_0的定义为:

AddPPB(PPB_GRAPHICS_3D_INTERFACE_1_0, thunk::GetPPB_Graphics3D_1_0_Thunk(), PERMISSION_NONE);
      它会调用函数GetPPB_Graphics3D_1_0_Thunk获得一个PPB_Graphics3D_1_0对象,如下所示:

const PPB_Graphics3D_1_0 g_ppb_graphics3d_thunk_1_0 = {
  &GetAttribMaxValue,
  &Create,
  &IsGraphics3D,
  &GetAttribs,
  &SetAttribs,
  &GetError,
  &ResizeBuffers,
  &SwapBuffers
};

......

PPAPI_THUNK_EXPORT const PPB_Graphics3D_1_0* GetPPB_Graphics3D_1_0_Thunk() {
  return &g_ppb_graphics3d_thunk_1_0;
}
      这个函数定义在文件external/chromium_org/ppapi/thunk/ppb_graphics_3d_thunk.cc中。

      从这里我们就可以看到,接口PPB_GRAPHICS_3D_INTERFACE_1_0提供给Plugin使用的函数,例如函数SwapBuffers,是Plugin执行3D渲染时用到的,用来实现eglSwapBuffers的功能。

      上述获得的PPB_Graphics3D_1_0对象将会通过InterfaceList类的成员函数AddPPB保存在内部一个std::map中,如下所示:

void InterfaceList::AddPPB(const char* name,
                           const void* iface,
                           Permission perm) {
  ......
  name_to_browser_info_[name] = InterfaceInfo(iface, perm);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

       相当于执行了以下操作:

name_to_browser_info_[PPB_GRAPHICS_3D_INTERFACE_1_0] = InterfaceInfo(g_ppb_graphics3d_thunk_1_0, PERMISSION_NONE);

       这样,我们就可以知道第二个接口PPB_GRAPHICS_3D_INTERFACE_1_0对应的是一个PPB_Graphics3D_1_0对象,以后调用这个接口将会执行该PPB_Graphics3D_1_0对象提供的相应函数。

       第三个接口是一个Plugin Instance相关的接口,接口ID为PPB_INSTANCE_INTERFACE_1_0,。当Plugin进行3D渲染时,就需要调用到这个接口来执行一个OpenGL上下文绑定操作。这个接口也是通过include文件interfaces_ppb_public_stable.h定义的,如下所示:

PROXIED_IFACE(PPB_INSTANCE_INTERFACE_1_0, PPB_Instance_1_0)
       这个接口定义在文件external/chromium_org/ppapi/thunk/interfaces_ppb_public_stable.h中。

       从这里可以看出,接口PPB_INSTANCE_INTERFACE_1_0与接口PPB_GRAPHICS_3D_INTERFACE_1_0一样,都是通过宏PROXIED_IFACE定义的。因此我们就可以容易知道,接口PPB_INSTANCE_INTERFACE_1_0是通过一个PPB_Instance_1_0对象实现的。这个PPB_Instance_1_0对象可以通过调用函数GetPPB_Instance_1_0_Thunk获得,如下所示:

const PPB_Instance_1_0 g_ppb_instance_thunk_1_0 = {
  &BindGraphics,
  &IsFullFrame
};

......

PPAPI_THUNK_EXPORT const PPB_Instance_1_0* GetPPB_Instance_1_0_Thunk() {
  return &g_ppb_instance_thunk_1_0;
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/ppb_instance_thunk.cc中。

       从这里我们就可以看到,接口PPB_INSTANCE_INTERFACE_1_0提供给Plugin使用的函数,例如函数BindGraphics,就是用来给一个Plugin Instance绑定OpenGL上下文的。

       第四个接口是一个资源创建接口,接口ID为API_ID_RESOURCE_CREATION。Plugin进行3D渲染之前,需要请求Chromium为其创建一个3D上下文。3D上下文是一个资源对象,这时候就需要调用到这个接口进行创建。这个接口是通过调用前面分析的InterfaceList类的成员函数AddProxy定义的,即:

AddProxy(API_ID_RESOURCE_CREATION, &ResourceCreationProxy::Create);
       相当于执行了以下操作:

id_to_factory_[API_ID_RESOURCE_CREATION] = &ResourceCreationProxy::Create;
       这样,我们就可以知道第三个接口API_ID_RESOURCE_CREATION对应的函数是ResourceCreationProxy类的静态成员函数Create,以后调用这个接口就会调用到ResourceCreationProxy类的静态成员函数Create。

       第五个接口提供给Plugin用来获取当前时间以及对资源进行管理等,接口ID为PPB_CORE_INTERFACE_1_0。这个接口是通过调用前面分析的InterfaceList类的成员函数AddPPB定义的,即:

AddPPB(PPB_CORE_INTERFACE_1_0, PPB_Core_Proxy::GetPPB_Core_Interface(), PERMISSION_NONE);

       它会调用PPB_Core_Proxy类的静态成员函数GetPPB_Core_Interface获得一个PPB_Core对象,如下所示:

const PPB_Core core_interface = {
  &AddRefResource,
  &ReleaseResource,
  &GetTime,
  &GetTimeTicks,
  &CallOnMainThread,
  &IsMainThread
};

......

const PPB_Core* PPB_Core_Proxy::GetPPB_Core_Interface() {
  return &core_interface;
}
      这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_core_proxy.cc中。

      从这里我们就可以看到,接口PPB_CORE_INTERFACE_1_0提供给Plugin使用的函数,例如函数GetTime,用来获得当前的时间。

      上述获得的PPB_Core对象将会通过前面分析的InterfaceList类的成员函数AddPPB保存在内部一个std::map中,即:

name_to_browser_info_[PPB_CORE_INTERFACE_1_0] = InterfaceInfo(core_interface, PERMISSION_NONE);
      第六个接口提供给Plugin执行OpenGL函数,接口ID为PPB_OPENGLES2_INTERFACE_1_0。这个接口是通过调用前面分析的InterfaceList类的成员函数AddPPB定义的,即: 

AddPPB(PPB_OPENGLES2_INTERFACE_1_0, PPB_OpenGLES2_Shared::GetInterface(), PERMISSION_NONE);
      它会调用PPB_OpenGLES2_Shared类的静态成员函数Get_Interface获得一个PPB_OpenGLES2对象,如下所示:

const PPB_OpenGLES2* PPB_OpenGLES2_Shared::GetInterface() {
  static const struct PPB_OpenGLES2 ppb_opengles2 = {
      &ActiveTexture,                       &AttachShader,
      &BindAttribLocation,                  &BindBuffer,
      &BindFramebuffer,                     &BindRenderbuffer,
      &BindTexture,                         &BlendColor,
      &BlendEquation,                       &BlendEquationSeparate,
      &BlendFunc,                           &BlendFuncSeparate,
      &BufferData,                          &BufferSubData,
      &CheckFramebufferStatus,              &Clear,
      ......
      &UseProgram,                          &ValidateProgram,
      &VertexAttrib1f,                      &VertexAttrib1fv,
      &VertexAttrib2f,                      &VertexAttrib2fv,
      &VertexAttrib3f,                      &VertexAttrib3fv,
      &VertexAttrib4f,                      &VertexAttrib4fv,
      &VertexAttribPointer,                 &Viewport};
  return &ppb_opengles2;
}
       这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppb_opengles2_shared.cc中。

       从这里我们就可以看到,接口PPB_OPENGLES2_INTERFACE_1_0提供给Plugin使用的函数,例如函数ActiveTexture,相当于就是OpenGL函数glActiveTexture。

       上述获得的PPB_OpenGLES2对象将会通过前面分析的InterfaceList类的成员函数AddPPB保存在内部一个std::map中,即:

name_to_browser_info_[PPB_OPENGLES2_INTERFACE_1_0] = InterfaceInfo(ppb_opengles2, PERMISSION_NONE);

       第七个接口是一个Plugin Instance通信接口,接口ID为API_ID_PPP_INSTANCE。通过这个接口,Render进程中的Plugin Instance可以它在Plugin进行中对应的Plugin Instance进行通信。这个接口是通过调用前面分析的InterfaceList类的成员函数AddProxy定义的,即:

AddProxy(API_ID_PPP_INSTANCE, &ProxyFactory<PPP_Instance_Proxy>);  
       从前面的分析可以知道,函数ProxyFactory<PPP_Instance_Proxy>是一个模板函数,当这个函数被调用的时候,将会返回一个PPP_Instance_Proxy对象。

       上述模板函数ProxyFactory<PPP_Instance_Proxy>将会通过前面分析的InterfaceList类的成员函数AddProxy保存在内部一个InterfaceProxy::Factory数组中,即:

id_to_factory_[API_ID_PPP_INSTANCE] = ProxyFactory<PPP_Instance_Proxy>;
       这样,我们就可以知道接口API_ID_PPP_INSTANCE对应的函数为ProxyFactory<PPB_Instance_Proxy>,以后调用这个接口将会得到一个PPP_Instance_Proxy对象。

       这一步执行完成后,Plugin进程中的InterfaceList单例对象就在创建过程中初始化好了一系列的接口。这些接口可以被Plugin调用,用来与Chromium交互。其中,我们重点分析了以下七个接口的定义:

       1. API_ID_PPB_INSTANCE

       2. PPB_GRAPHICS_3D_INTERFACE_1_0

       3. PPB_INSTANCE_INTERFACE_1_0

       4. API_ID_RESOURCE_CREATION

       5. PPB_CORE_INTERFACE_1_0

       6. PPB_OPENGLES2_INTERFACE_1_0

       7. API_ID_PPP_INSTANCE

       在接下来的两篇文章中,我们会看到这些接口的详细调用过程。通过这些调用过程,我们就会对Chromium的Plugin机制会更深刻的认识。

       现在回到前面分析的PluginDispatcher类的静态成员函数GetBrowserInterface,这时候它通过调用Plugin进程中的InterfaceList单例对象的成员函数GetInterfaceForPPB获得一个类型为PPB_CORE_INTERFACE的接口,如下所示:

const void* InterfaceList::GetInterfaceForPPB(const std::string& name) {
  NameToInterfaceInfoMap::iterator found =
      name_to_browser_info_.find(name);
  if (found == name_to_browser_info_.end())
    return NULL;

  if (g_process_global_permissions.Get().HasPermission(
          found->second.required_permission)) {
    ......
    return found->second.iface;
  }
  return NULL;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

       InterfaceList类的成员函数GetInterfaceForPPB首先在成员变量name_to_browser_info_描述的一个std::map检查是否存在一个类型为name的接口。如果存在,那么会继续检查当前进程是否具有该接口的使用权限。只有在具有权限的情况下,前面查找到的接口才会返回给调用者。

       在我们这个情景中,要获得的接口是PPB_CORE_INTERFACE。接口PPB_CORE_INTERFACE与前面我们分析的PPB_CORE_INTERFACE_1_0是一样的,这一点可以参考以下宏定义:

#define PPB_CORE_INTERFACE_1_0 "PPB_Core;1.0"
#define PPB_CORE_INTERFACE PPB_CORE_INTERFACE_1_0
       这两个宏定义在文件external/chromium_org/ppapi/c/ppb_core.h中。

       由于接口PPB_CORE_INTERFACE_1_0已经存在成员变量name_to_browser_info_描述的std::map中,并且它的使用权限为PERMISSION_NONE,即不需要请求权限,因此,InterfaceList类的成员函数GetInterfaceForPPB将会返回该接口给最初的调用者,即pp::Module类的成员函数InternalInit。后者获得这个接口后,会将其封装在一个Core对象中,并且将该Core对象保存在pp::Module类的成员变量core_中。

       InterfaceList类还提供了另外一个成员函数GetFactoryForID,用来获得前面我们分析的那些保存在其成员变量id_to_factory_描述的InterfaceProxy::Factory数组中的接口,如下所示:

InterfaceProxy::Factory InterfaceList::GetFactoryForID(ApiID id) const {
  int index = static_cast<int>(id);
  COMPILE_ASSERT(API_ID_NONE == 0, none_must_be_zero);
  if (id <= 0 || id >= API_ID_COUNT)
    return NULL;
  return id_to_factory_[index];
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/interface_list.cc中。

       这意味着Plugin可以通过调用InterfaceList类的成员函数GetInterfaceForPPB和GetFactoryForID获得Chromium提供给它们的调用接口,也就是Chromium提供给Plugin的API。有了这些API之后,Plugin就可以完成自己的功能了。

       至此,我们就分析完成了Plugin Module的加载过程和初始化过程。其中,加载过程的更详细分析可以参考前面Chromium的Plugin进程启动过程分析一文,初始化过程主要就是定义了一系列Plugin可以调用的API接口。在接下来的两篇文章中,我们将会逐步看到这些接口的调用过程。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/10/31 0:59:29 原文链接
阅读:48134 评论:3 查看评论
Viewing all 35570 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>