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

前端基础-07-函数及作用域

$
0
0

函数及作用域

1.函数的使用

#有名函数
定义函数:
function fn() {
    var box = document.getElementById("box");
    box.innerHTML = "Java";
} //函数定义 可以在定义前加括号执行,也可以在定义后加括号执行
#第一种调用方式:
fn();
#第二种调用方式:
document.onclick = function () {
    alert(1);
}

# 匿名函数
    没有名字的函数
    匿名函数不能单独出现 一般充当事件函数
        document.onclick = function () {
           alert(1);
        }

#函数表达式:
特点:可以在后面加括号立即执行
() + - ! ~ 可以将匿名函数变为函数表达式

#第一种:
var fn = function () {
    alert(1);
};//通过var 的函数,只能在后面运行
fn();

#第二种:
(function () {
    alert(1);
})();

#第三种:
(function () {
    alert(1);
}());
#第四种:
+function () {  //可以使用+ 、-、!、~
    alert(1);
}();

2.形参和实参

# 形参和实参
fn(20);//实参
function fn(x) {//形参
    alert(x);
}
#多个参数
 实参和形参个数不一定非得一样,但是不一样的时候要注意一一对应的关系
sum( 20 , 30);#传入参数少了,运算会返回NaN,未传的参数值是undefined

function sum( x , y ,z) {
    //一个函数里面,形参不要重复,也不要和内部var定义的变量重复
    alert( x +y+z );
}

#参数少了,设置默认值
sum( 20 , 32 );#传入参数少了

function sum( x , y , z) {
    //给形参设置默认值
    x = x || 0;
    y = y || 0;
    z = z || 0;
    alert(z);
    alert(x+y+z);
}

3.不定参数

sum(1, 2, 3, 4, 5, 6, 7, 8, 9);//实参
function sum(r, t) {//和有没有形参没关系
    //不定参 arguments,存储着所有 实参的集合
    var x = 0;
    var len = arguments.length;//
    for (var i = 0; i < len; i++) {
        x += arguments[i];
    }
    alert(x);
}

4.return 返回

函数未写return值,默认返回 的是 undefined
function fn() {
    var box = document.getElementById("box");
    box.innerHTML = "java";//过程
    return box;
}
var x = fn();
alert(x);

5.JS作用域

#JavaScript代码运行,先解析在运行

    1.(定义)解析顺序:var > function> 参数
    a.var只定义变量,后面的=赋值不解析
    b.函数只定义函数,函数的代码块不执行
    c.重名的只留一个,var 和函数重名  函数优先

    2.(运行)从上至下执行代码
#案例1:
    alert(a)  #未定义变量a,脚本运行报错

#案例2:
alert(b)  #此时b未赋值,只是解析了变量b,运行结果是undefined
alert(a)#此时解析了函数,运行返回的是函数体
var a=function (b) {  #先解析var命名的变量a,在解析function,在解析方法体里面参数
    alert("1")
    return b
}
var  b=5  #b赋值了
alert(b)  #运行结果返回是5
作者:lianjiaokeji 发表于2017/11/17 9:23:35 原文链接
阅读:33 评论:0 查看评论

Flowable中的多实例事件

$
0
0

在flowable6.3以后的版本中,支持了MULTI_INSTANCE_ACTIVITY_COMPLETED_WITH_CONDITION事件,这个事件顾名思义就是在多实例节点完成的时候,flowable引擎去发布这个完成事件信号。该事件同其他的事件一样,定义在FlowableEngineEventType.java类中。

关于多实例所支持的事件类型如下所示:

MULTI_INSTANCE_ACTIVITY_COMPLETED(多实例完成),

 MULTI_INSTANCE_ACTIVITY_COMPLETED_WITH_CONDITION(多实例正常完成),

  MULTI_INSTANCE_ACTIVITY_CANCELLED(多实例取消),

不管是什么事件,我们在开发的过程中往往只会关心事件的类型以及事件所产生的数据而已。现在既然我们知道了新增的事件类型是

MULTI_INSTANCE_ACTIVITY_COMPLETED,

 MULTI_INSTANCE_ACTIVITY_COMPLETED_WITH_CONDITION,

  MULTI_INSTANCE_ACTIVITY_CANCELLED,

三个,那接下来,我们看一下这些事件所对应的事件处理类,具体细节在AbstractFlowableEngineEventListener类中如下所示:

 case MULTI_INSTANCE_ACTIVITY_COMPLETED:
                       multiInstanceActivityCompleted((FlowableMultiInstanceActivityCompletedEvent) flowableEngineEvent);
            break;
  case MULTI_INSTANCE_ACTIVITY_COMPLETED_WITH_CONDITION:
                       multiInstanceActivityCompletedWithCondition((FlowableMultiInstanceActivityCompletedEvent) flowableEngineEvent);
           break;
  case MULTI_INSTANCE_ACTIVITY_CANCELLED:
                          multiInstanceActivityCancelled((FlowableMultiInstanceActivityCancelledEvent) flowableEngineEvent)
 protected void multiInstanceActivityCompleted(FlowableMultiInstanceActivityEvent event) {}
    protected void multiInstanceActivityCompleted(FlowableMultiInstanceActivityCompletedEvent event) {}

    protected void multiInstanceActivityCompletedWithCondition(FlowableMultiInstanceActivityCompletedEvent event) {}
  
      protected void multiInstanceActivityCancelled(FlowableMultiInstanceActivityCancelledEvent event) {}

MULTI_INSTANCE_ACTIVITY_COMPLETED事件对应的事件类为:FlowableMultiInstanceActivityCompletedEvent

MULTI_INSTANCE_ACTIVITY_COMPLETED_WITH_CONDITION事件对应的事件类为:FlowableMultiInstanceActivityCompletedEvent。

MULTI_INSTANCE_ACTIVITY_CANCELLED事件对应的事件类为:FlowableMultiInstanceActivityCancelledEvent。
由上面的源码可以知道 MULTI_INSTANCE_ACTIVITY_COMPLETED与MULTI_INSTANCE_ACTIVITY_COMPLETED_WITH_CONDITION事件对应的事件类是相同的,都是FlowableMultiInstanceActivityCompletedEvent。

MULTI_INSTANCE_ACTIVITY_CANCELLED事件对应的事件类为:FlowableMultiInstanceActivityCancelledEvent。

通过FlowableMultiInstanceActivityCompletedEvent类我们可以获取到的属性信息如下:

numberOfInstances(多实例的个数)

numberOfActiveInstances(多实例活动的个数)

numberOfCompletedInstances(多实例已经完成的活动个数)

关于更多的事件以及事件监听器可以参考Activiti权威指南一书 。

作者:qq_30739519 发表于2017/11/17 9:24:44 原文链接
阅读:23 评论:0 查看评论

第21章 多播

$
0
0

1·多播(组播)地址:224.0.0.0~139.255.255.255

2·发送多播数据报无需任何特殊处理,发送应用程序不必为此加入多播组

3·多播相对于广播的优势在于不会对多播分组不感兴趣的主机增加额外负担

#include "../Gnet.h"

int main(int argc, const char* argv[])
{
    int sendfd, recvfd;
    const int on = 1;
    //const int off = 0;
    struct sockaddr_in sockaddr;
    socklen_t addrlen;
    struct ip_mreq mreq;
    int nrecv;
    char buf[MAX_LINE];
    char addrstr[INET_ADDRSTRLEN];

    if(argc != 2)
        perr_exit("usage: mcast <IP-multicast-address>");

    sendfd = Socket(AF_INET, SOCK_DGRAM, 0);
    recvfd = Socket(AF_INET, SOCK_DGRAM, 0);

    if(setsockopt(recvfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        perr_exit("setsockopt error. 22");

    sockaddr.sin_family = AF_INET;
    //sockaddr.sin_port = htons(SERVER_PORT);
    if(inet_pton(AF_INET, argv[1], &sockaddr.sin_addr) < 0)
        perr_exit("inet_pton error.27");
    addrlen = sizeof(sockaddr);
    Bind(recvfd, (struct sockaddr*)&sockaddr, addrlen);//接收套接字绑定多播地址

    mreq.imr_multiaddr = sockaddr.sin_addr;
    //inet_pton(AF_INET, "192.168.17.129", &mreq.imr_interface.s_addr);
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);

    if(setsockopt(recvfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) <0)//接收套接字加入多播组
        perr_exit("setsockopt error.35");
    //setsockopt(sendfd, IPPROTO_IP, IP_MULTICAST_LOOP, &off, sizeof(off));
    if(fork() > 0)
    {
        while(1)
        {
            nrecv = recvfrom(recvfd, buf, MAX_LINE, 0, (struct sockaddr*)&sockaddr, &addrlen); 
            printf("from %s: %s\n", 
                   inet_ntop(AF_INET, &sockaddr, addrstr, sizeof(addrstr)), buf);
        }
    }
    else
    {
        while(1)
        {
            sendto(sendfd, "test data", strlen("test data")+1, 0, (struct sockaddr*)&sockaddr, addrlen);
            sleep(2);
        }
    }

    return 0;
}


github:https://github.com/gongluck/unp-notes

作者:gongluck93 发表于2017/11/17 9:30:29 原文链接
阅读:27 评论:0 查看评论

【蓝桥杯】【丢手帕问题】

$
0
0

题目
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。
例如N=6,M=5,被杀掉的人的序号为5,4,6,2,3。最后剩下1号。

输入两个整数N,M
比如 6 5
输出被杀掉的人的序号
比如5 4 6 2 3 1
再比如:
输入 11 3
输出 3 6 9 1 5 10 4 11 8 2 7

分析
可以使用数组来模拟(猴子选大王)
也可以使用集合来模拟,这里采用集合模拟思路
建立一个整数集合,对其赋值,1~N
每次递增M-1,注意这里不是递增M
如果递增后数字的范围超过了集合的范围就对集合大小取模
删除掉相应的第M个人

源码

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int N = sc.nextInt();
        int M = sc.nextInt();
        sc.close();
        //建立集合
        LinkedList<Integer> list = new LinkedList<Integer>();
        //为每个人编号1~N
        for (int i = 1; i <= N; i++) {
            list.add(i);
        }

        //标记变量,用来标记第M个人
        int a = 0;

        //开始循环模拟
        while (list.size() > 1) {
            //递增M-1,找到第M个人,这个数值有可能超出集合的上限范围
            a += M-1;
            //用a对集合的大小取模运算
            a = a % list.size();
            //打印并删除掉第M个人
            System.out.print(list.remove(a)+" ");
        }
        //把最后一个人打印并删除
        System.out.println(list.remove(0));

    }

结果
11 3
3 6 9 1 5 10 4 11 8 2 7

作者:bear_huangzhen 发表于2017/11/17 10:29:09 原文链接
阅读:14 评论:0 查看评论

itext7学习笔记——第5章

$
0
0

前言

    在之前的第1到第3章,我们总是从头开始用iText创建一个新的PDF文档。在第4章的最后几个例子中,我们使用了一个现有的PDF文档,利用现有的PDF来读取表单并填写了自己的表单或者预填充定义的表单。在本章,我们会使用PdfReader读取一个存在的PDF文件,或者使用PdfWriter对象来创建一个新的PdfDocument

添加注释和内容

    在前面的章节中,我们读取了一个带有表单的PDF文档,然后填充了里面的内容。在这个章节中,我们会进行深入的操作。我们首先添加一个文档注释,一些文本和一个新的复选框,如下图:

itext5-1

    接下来就是我们在第四章里面添加注释和内容里面的代码一样了:

PdfDocument pdfDoc =
    new PdfDocument(new PdfReader(src), new PdfWriter(dest));
// add content
pdfDoc.close();

    我们在//add content部分添加相应的内容,首先,我们通过PdfDocument实例来向一个页面里面添加注释:

PdfAnnotation ann = new PdfTextAnnotation(new Rectangle(400, 795, 0, 0))
    .setTitle(new PdfString("iText"))
    .setContents("Please, fill out the form.")
    .setOpen(true);
pdfDoc.getFirstPage().addAnnotation(ann);

    如果我们想添加内容到内容流里面,我们需要创建一个PdfCancas对象。我们可以传入一个PdfPage对象传入PdfCanvas的构造函数:

PdfCanvas canvas = new PdfCanvas(pdfDoc.getFirstPage());
canvas.beginText().setFontAndSize(
        PdfFontFactory.createFont(FontConstants.HELVETICA), 12)
        .moveText(265, 597)
        .showText("I agree to the terms and conditions.")
        .endText();

    添加文本的代码和我们在第2章写的代码差不多。无论您是从头开始创建文档还是将内容添加到现有文档,都可以使用我们写的代码。当然,向PdfAcroForm实例添加字段也是跟之前的例子一样的:

PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
PdfButtonFormField checkField = PdfFormField.createCheckBox(
        pdfDoc, new Rectangle(245, 594, 15, 15),
        "agreement", "Off", PdfFormField.TYPE_CHECK);
checkField.setRequired(true);
form.addField(checkField);

    最后,既然我们已经添加了一个多余的字段,我们想要改变reset的动作:

form.getField("reset").setAction(PdfAction.createResetForm(
    new String[]{"name", "language", "experience1", "experience2",
    "experience3", "shift", "info", "agreement"}, 0))

改变表单字段的属性

    之前的第四章的例子中,我们是填充了表单的内容。在这里,我们可以尝试改变字段的属性,添加或者删除。

PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
Map<String, PdfFormField> fields = form.getFormFields();
fields.get("name").setValue("James Bond").setBackgroundColor(Color.ORANGE);
fields.get("language").setValue("English");
fields.get("experience1").setValue("Yes");
fields.get("experience2").setValue("Yes");
fields.get("experience3").setValue("Yes");
List<PdfString> options = new ArrayList<PdfString>();
options.add(new PdfString("Any"));
options.add(new PdfString("8.30 am - 12.30 pm"));
options.add(new PdfString("12.30 pm - 4.30 pm"));
options.add(new PdfString("4.30 pm - 8.30 pm"));
options.add(new PdfString("8.30 pm - 12.30 am"));
options.add(new PdfString("12.30 am - 4.30 am"));
options.add(new PdfString("4.30 am - 8.30 am"));
PdfArray arr = new PdfArray(options);
fields.get("shift").setOptions(arr);
fields.get("shift").setValue("Any");
PdfFont courier = PdfFontFactory.createFont(FontConstants.COURIER);
fields.get("info")
    .setValue("I was 38 years old when I became a 007 agent.", courier, 7);

    在这里我们关注下面的几行:

  • 行3:我们设置name字段的值为James Bond,同时我们设置这个字段的背景色为Color.ORANGE
  • 行9-17:创建了一个Java List来添加更多的选项(行8-15),然后把这个List转换成PdfArray(行16),最后使用这个array来更新shift字段
  • 行19-21:我们创建了一个新的PdfFont,然后使用这个新的字体和新的字体大小作为info字段的设置值方法的参数。

    让我们看看加入这些代码以后是否有所改变,如图:

itext5-2

    我们可以看到shift字段有了更多的选项,但是我们没有看见name字段的背景,我们是不能清楚info字段是否改变。是否发生错误了呢?其实是没有错误的,所有字段当前是被突出显示的而且蓝色的突出色覆盖了背景颜色,我们可以点击Highlight Existing Fields来看看还会发生什么,如图:

itext5-3

    一切跟我们期望的一样。但是我们没有遇到这样一种情况:在关闭PdfDocument之前,我们加入了form.flattenFields();语句,这时候表单就锁定,我们就没有了表单,针对这种情况,我们会在下一章探讨。现在,我们来看看我们是不是还能对没有表单的pdf文档进行一些其他的操作。

添加页眉、页脚和水印

    我们回忆在一下第三章里面的UFO信息的PDF,我们会创建一个相似的ufo.pdf文件:

itext5-4

    可以看出我们创建的ufo.pdf并没有第三章创建的那样炫酷,如果我们往里面添加一个页眉和一个页脚(显示第几页/总页数)还有水印,该如何做呢,如下图:

itext5-5

    在上图中,我们不像第三章那样在添加页脚的时候不知道总的页数,现在我们知道了总的页数,可以添加”1” of “4”,”2” of “4”等

从头创建文档时,可以为总页数创建一个占位符(placeholder)。一旦创建了所有的页面,我们就可以将该页面的总数添加到该占位符,但这不在本介绍性教程的范围之内。

    如下代码展示了如何在已知的文档的每一页中添加内容:

PdfDocument pdfDoc =
    new PdfDocument(new PdfReader(src), new PdfWriter(dest));
Document document = new Document(pdfDoc);
Rectangle pageSize;
PdfCanvas canvas;
int n = pdfDoc.getNumberOfPages();
for (int i = 1; i <= n; i++) {
    PdfPage page = pdfDoc.getPage(i);
    pageSize = page.getPageSize();
    canvas = new PdfCanvas(page);
    // add new content
}
pdfDoc.close();

    我们使用pdfDoc对象来创建一个Document实例。我们将要使用这个document对象来添加内容。我们同时也会使用pdfDoc对象来找到原始PDF的总页数。我们遍历所有所有的页数,获得每一页的PdfPage对象。以下代码是//add new content所做得东西:

//Draw header text
canvas.beginText().setFontAndSize(
        PdfFontFactory.createFont(FontConstants.HELVETICA), 7)
        .moveText(pageSize.getWidth() / 2 - 24, pageSize.getHeight() - 10)
        .showText("I want to believe")
        .endText();
//Draw footer line
canvas.setStrokeColor(Color.BLACK)
        .setLineWidth(.2f)
        .moveTo(pageSize.getWidth() / 2 - 30, 20)
        .lineTo(pageSize.getWidth() / 2 + 30, 20).stroke();
//Draw page number
canvas.beginText().setFontAndSize(
        PdfFontFactory.createFont(FontConstants.HELVETICA), 7)
        .moveText(pageSize.getWidth() / 2 - 7, 10)
        .showText(String.valueOf(i))
        .showText(" of ")
        .showText(String.valueOf(n))
        .endText();
//Draw watermark
Paragraph p = new Paragraph("CONFIDENTIAL").setFontSize(60);
canvas.saveState();
PdfExtGState gs1 = new PdfExtGState().setFillOpacity(0.2f);
canvas.setExtGState(gs1);
document.showTextAligned(p,
        pageSize.getWidth() / 2, pageSize.getHeight() / 2,
        pdfDoc.getPageNumber(page),
        TextAlignment.CENTER, VerticalAlignment.MIDDLE, 45);
canvas.restoreState();

    总共的步骤分为四部:

  • 页眉(行2-6):我们使用第二章谈及的低等级的文本api来在每一页的最上方添加"I want to believe"
  • 页脚线(行8-11):我们使用低等级的画图api来在每一页的底部画线
  • 包含页码的页脚(行13-19):我们使用低等级的文本api在每一页底部添加当前页号,"of",以及总的页数。
  • 水印(行21-28):我们创建一个带有文本的Paragrah来当做水印。然后我们改变画布的透明性。最后我们添加这个Paragrah到文档中,使用showTextAligned()方法使水印定位在每一页的中间,并45度倾斜。

    在这里,我们添加水印的时候做了一些特殊的工作。我们改变了canvas对象的图像状态,然后我们在document的每一页中添加了文本,在内部,iText会发现我们已经使用了当前页的PafCanvas实例,同时showTextAligned会想同一个canvas写入内容,这种方式,我们可以使用低级api和一些简单方法(如高级api)的结合(可以和第三章的添加水印相比较,确实是方便了不少)。在本章最后的例子中,我们会改变页面的大小和upf.pdf的每一页的方向。

改变页面大小和方向

    新的pdf文档效果如下:

itext5-6

    我们可以发现每一页面变得更大了,而且第二页的颠倒了,向下了。让我们来看看代码:

PdfDocument pdfDoc =
    new PdfDocument(new PdfReader(src), new PdfWriter(dest));
float margin = 72;
for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
    PdfPage page = pdfDoc.getPage(i);
    // change page size
    Rectangle mediaBox = page.getMediaBox();
    Rectangle newMediaBox = new Rectangle(
            mediaBox.getLeft() - margin, mediaBox.getBottom() - margin,
            mediaBox.getWidth() + margin * 2, mediaBox.getHeight() + margin * 2);
    page.setMediaBox(newMediaBox);
    // add border
    PdfCanvas over = new PdfCanvas(page);
    over.setStrokeColor(Color.GRAY);
    over.rectangle(mediaBox.getLeft(), mediaBox.getBottom(),
            mediaBox.getWidth(), mediaBox.getHeight());
    over.stroke();
    // change rotation of the even pages
    if (i % 2 == 0) {
        page.setRotation(180);
    }
}
pdfDoc.close();

    可以发现,这里我们不需要Document实例,我们只需要PdfDocument实例就够啦。我们遍历所有的页面(行4)和获取每一页的PdfPage实例(行5):

  • 一个页面可以有不同的边界,其中/MediaBox基本上就是页面大小了(详情了可以百度PDF MediaBox),然后我们获取一个Rectangle作为原页面的边界(行7),根据这个边界来创建一英尺更大的边Rectangle来当做新的页面边界大小,最后我们使用setMediaBox()来改变页面大小
  • 我们创了页面的PdfCanvas对象,然后使用灰色画笔画出了mediaBox的边界(行14-17)
  • 偶数页,我们把页面旋转180度(行19)

    改变已知存在的PDF文档需要关于PDF的很多知识,比如/MediaBox的概念。我们为了使举的例子看起来简单,我们会规避一些其他的细节,例如在上面一个例子中,我们没有检查文档中有CropBox对象,如果有这个对象的话,使/MediaBox变大不会带来任何视觉上的变换。所以将会花上更多篇章来深入讨论这些问题。

总结

    在之前的篇章中,我们学习了交互式的PDF表单。在本章中,我们继续操作这些表单:添加注释,一些文本和额外的字段,我们在填充表单的同时还改变了字段的一些属性。
    然后我们讨论了那些没有交互的PDF文档,首先,我们添加了页眉、页脚和水印,然后我们改变了已知文档的页面大小和方向。
    在下一个章节中,我们会缩放和平铺已知文档,然后我们会学会如何将多个文档组合成一个PDF

PS:我的itext7专栏上线啦,里面都是我精心翻译和整理的关于itext7的知识,求大家关注一波,之前也收到了一些打赏和鼓励,我会继续更新itext7,分享最新的知识!

作者:u012397189 发表于2017/11/17 10:48:57 原文链接
阅读:2 评论:0 查看评论

Python垃圾回收机制

$
0
0

Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题。通过“分代回收”(generation collection)以空间换取时间来进一步提高垃圾回收的效率。

一、引用计数

在Python中,大多数对象的生命周期都是通过对象的引用计数来管理的。从广义上来讲,引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。

原理:当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1;当对象的引用计数减少为0时,就意味着对象已经没有被任何人使用了,可以将其所占用的内存释放了。

虽然引用计数必须在每次分配和释放内存的时候加入管理引用计数的动作,然而与其他主流的垃圾收集技术相比,引用计数有一个最大的有点,即“实时性”,任何内存,一旦没有指向它的引用,就会立即被回收。而其他的垃圾收集计数必须在某种特殊条件下(比如内存分配失败)才能进行无效内存的回收。

引用计数机制执行效率问题:引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的。而这点相比其他主流的垃圾回收机制,比如“标记-清除”,“停止-复制”,是一个弱点,因为这些技术所带来的额外操作基本上只是与待回收的内存数量有关。

如果说执行效率还仅仅是引用计数机制的一个软肋的话,那么很不幸,引用计数机制还存在着一个致命的弱点,正是由于这个弱点,使得侠义的垃圾收集从来没有将引用计数包含在内,能引发出这个致命的弱点就是循环引用(也称交叉引用)。

问题说明:

循环引用可以使一组对象的引用计数不为0,然而这些对象实际上并没有被任何外部对象所引用,它们之间只是相互引用。这意味着不会再有人使用这组对象,应该回收这组对象所占用的内存空间,然后由于相互引用的存在,每一个对象的引用计数都不为0,因此这些对象所占用的内存永远不会被释放。比如:

a=[]
b=[]
a.append(b)
b.append(b)
printa
[[[…]]]
printb
[[[…]]]

这一点是致命的,这与手动进行内存管理所产生的内存泄露毫无区别。要解决这个问题,Python引入了其他的垃圾收集机制来弥补引用计数的缺陷:“标记-清除”,“分代回收”两种收集技术。

二、标记-清除

“标记-清除”是为了解决循环引用的问题。可以包含其他对象引用的容器对象(比如:list,set,dict,class,instance)都可能产生循环引用。

我们必须承认一个事实,如果两个对象的引用计数都为1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被回收的,也就是说,它们的引用计数虽然表现为非0,但实际上有效的引用计数为0。我们必须先将循环引用摘掉,那么这两个对象的有效计数就现身了。假设两个对象为A、B,我们从A出发,因为它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,因为B有一个对A的引用,同样将A的引用减1,这样,就完成了循环引用对象间环摘除。

但是这样就有一个问题,假设对象A有一个对象引用C,而C没有引用A,如果将C计数引用减1,而最后A并没有被回收,显然,我们错误的将C的引用计数减1,这将导致在未来的某个时刻出现一个对C的悬空引用。这就要求我们必须在A没有被删除的情况下复原C的引用计数,如果采用这样的方案,那么维护引用计数的复杂度将成倍增加。

原理:“标记-清除”采用了更好的做法,我们并不改动真实的引用计数,而是将集合中对象的引用计数复制一份副本,改动该对象引用的副本。对于副本做任何的改动,都不会影响到对象生命走起的维护。

这个计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合之后,首先将现在的内存链表一分为二,一条链表中维护root object集合,成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。之所以要剖成两个链表,是基于这样的一种考虑:现在的unreachable可能存在被root链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。

三、分代回收

背景:分代的垃圾收集技术是在上个世纪80年代初发展起来的一种垃圾收集机制,一系列的研究表明:无论使用何种语言开发,无论开发的是何种类型,何种规模的程序,都存在这样一点相同之处。即:一定比例的内存块的生存周期都比较短,通常是几百万条机器指令的时间,而剩下的内存块,起生存周期比较长,甚至会从程序开始一直持续到程序结束。

从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。为了提高垃圾收集的效率,采用“空间换时间的策略”。

原理:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减小。也就是说,活得越长的对象,就越不可能是垃圾,就应该减少对它的垃圾收集频率。那么如何来衡量这个存活时间:通常是利用几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,可以得出:该对象存活时间就越长。

作者:qq_29287973 发表于2017/11/17 11:11:10 原文链接
阅读:1 评论:0 查看评论

python: hasattr()、setattr()、getattr()、delattr() 内建函数

$
0
0

Func

Func 语法 作用 retype
hasattr hasattr(object, name) 判断 对象中是否含有 该属性。 True / False
setattr setattr(object, name, values) 给对象的属性 赋值,若属性不存在,先创建再赋值。 None
getattr getattr(object, name[,default]) 获取 属性数值。 属性存在时,返回 属性数值;否则根据 默认输出值 返回 或 报 AttributeError。
delattr delattr(object, name) 删除属性。 属性存在则无返回,否则报 AttributeError。

实验代码

# -*- coding: utf-8 -*-

class test():
    city = 'Nanjing'
    def run(self):
        return 'Hello Nanjing'

t = test()


############################################################
#                   hasattr(object, name)
############################################################
# hasattr 只单纯地 返回 True 或 False。用于判断 对象中是否含有 该属性。
assert hasattr(t, 'city') == True
assert hasattr(t, 'capital') == False


############################################################
#               setattr(object, name, values)
############################################################
# setattr 没有 返回。给对象的属性赋值,若属性不存在,先创建再赋值。
assert not hasattr(t, 'capital')
setattr(t, 'capital', 'Jiangshu')
assert hasattr(t, 'capital')


############################################################
#               getattr(object, name[,default])
############################################################
# getattr 在属性 存在 时,返回 属性数值
assert getattr(t, 'city') == 'Nanjing'
assert getattr(t, 'run')

# getattr 在属性 不存在 时,有 指定默认输出值 的情况下,返回 默认输出值
assert getattr(t, 'year', 'Not found') == 'Not found'

# getattr 在属性 不存在 时,没有 指定默认输出值 的情况下,返回 AttributeError
try:
    getattr(t, 'year')
except AttributeError:
    pass

# getattr 和 setattr 的联合使用
assert getattr(t, 'year', setattr(t, 'year', '2017')) == '2017'
assert getattr(t, 'year') == '2017'


############################################################
#               delattr(object, name)
############################################################
# delattr 没有 返回。若属性 存在,则删除之。
assert hasattr(t, 'year')
delattr(t, 'year')
assert not hasattr(t, 'year')

# delattr 没有 返回。若属性 不存在,则报 AttributeError。
try:
    delattr(t, 'year')
except AttributeError:
    pass


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

[python爬虫] BeautifulSoup和Selenium简单爬取知网信息测试

$
0
0

作者最近在研究复杂网络和知识图谱内容,准备爬取知网论文相关信息进行分析,包括标题、摘要、出版社、年份、下载数和被引用数、作者信息等。但是在爬取知网论文时,遇到问题如下:
  1.爬取内容总为空,其原因是采用动态加载的数据,无法定位,然后作者重新选取了CNKI3.0知网进行了爬取;
  2.但却不含作者信息,需要定位到详情页面,再依次获取作者信息,但是又遇到了新的问题。


一. 网站定位分析

知网网站如下:http://nvsm.cnki.net/kns/brief/default_result.aspx
比如搜索Python关键字,网页反馈内容如下所示,2681篇文章。


但是使用Selenium定位爬取的论文内容总为空,后来网上看到qiuqingyun大神的博客,发现另一个知网接口(CNKI3.0 知识搜索:http://search.cnki.net/)。
强烈推荐大家阅读他的原文:http://qiuqingyu.cn/2017/04/27/python实现CNKI知网爬虫/
搜索python的的图片如下,共3428篇论文。


接下来简单讲述分析的过程,方法都类似,通过DOM树节点分析定位元素。右键浏览器审查元素如下所示,每页包括15篇论文,标签位于<div class="wz_tab">下。


点击具体一条内容,如下所示,定位方法如下:
  1.标题定位<div class="wz_content">下的<h3>标签,并且可以获取URL;
  2.摘要定位<div class="width715">内容;
  3.出处定位<span class="year-count">节点下的title,年份通过正则表达式提取数据;
  4.下载次数和被引用数定位<span class="count">,提取数字第一个和第二个。



接下来直接讲述BeautifulSoup和Selenium两种方式的爬虫。



二. BeautifulSoup爬虫

BeautifulSoup完整代码如下:

# -*- coding: utf-8 -*-
import time              
import re
import urllib   
from bs4 import BeautifulSoup
      
    
#主函数
if __name__ == '__main__':

    url = "http://search.cnki.net/Search.aspx?q=python&rank=relevant&cluster=all&val=&p=0"
    content = urllib.urlopen(url).read()
    soup = BeautifulSoup(content,"html.parser")

    #定位论文摘要
    wz_tab = soup.find_all("div",class_="wz_tab")
    num = 0
    for tab in wz_tab:
        #标题
        title = tab.find("h3")
        print title.get_text()
        urls = tab.find("h3").find_all("a")
        #详情超链接
        flag = 0
        for u in urls:
            if flag==0: #只获取第一个URL
                print u.get('href')
                flag += 1
        #摘要
        abstract = tab.find(attrs={"class":"width715"}).get_text()
        print abstract
        #获取其他信息
        other = tab.find(attrs={"class":"year-count"})
        content = other.get_text().split("\n")
        """
            由于无法分割两个空格,如:《怀化学院学报》  2017年 第09期
            故采用获取标题titile内容为出版杂志
            <span title="北方文学(下旬)">《北方文学(下旬)》  2017年 第06期</span>
        """
        #出版杂志+年份
        cb_from = other.find_all("span")
        flag = 0 
        for u in cb_from:
            if flag==0: #获取标题
                print u.get("title")
                flag += 1
        mode = re.compile(r'\d+\.?\d*')
        number = mode.findall(content[0])
        print number[0] #年份
        
        #下载次数 被引次数
        mode = re.compile(r'\d+\.?\d*')
        number = mode.findall(content[1])
        if len(number)==1:
            print number[0]
        elif len(number)==2:
            print number[0], number[1]

        num = num + 1
输出如下图所示:


但是爬取的URL无法跳转,总是显示登录页面,比如“http://epub.cnki.net/kns/detail/detail.aspx?filename=DZRU2017110705G&dbname=CAPJLAST&dbcode=cjfq”,而能正确显示的是的“http://www.cnki.net/KCMS/detail/detail.aspx?filename=DZRU2017110705G&
dbname=CAPJLAST&dbcode=CJFQ&urlid=&yx=&v=MTc2ODltUm42ajU3VDN
mbHFXTTBDTEw3UjdxZVlPZHVGeTdsVXJ6QUpWZz1JVGZaZbzlDWk81NFl3OU16”。
显示如下图所示:


解决方法:这里我准备采用Selenium技术定位超链接,再通过鼠标点击进行跳转,从而去到详情页面获取作者或关键词信息。


三. Selenium爬虫

爬取代码如下:

# -*- coding: utf-8 -*-
import time              
import re              
import sys    
import codecs    
import urllib   
from selenium import webdriver            
from selenium.webdriver.common.keys import Keys            
  
      
#主函数
if __name__ == '__main__':

    url = "http://search.cnki.net/Search.aspx?q=python&rank=relevant&cluster=all&val=&p=0"
    driver = webdriver.Firefox()
    driver.get(url)
    #标题
    content = driver.find_elements_by_xpath("//div[@class='wz_content']/h3")
    #摘要
    abstracts = driver.find_elements_by_xpath("//div[@class='width715']")
    #出版杂志+年份
    other = driver.find_elements_by_xpath("//span[@class='year-count']/span[1]")
    mode = re.compile(r'\d+\.?\d*')
    #下载次数 被引次数
    num = driver.find_elements_by_xpath("//span[@class='count']")

    #获取内容
    i = 0
    for tag in content:
        print tag.text
        print abstracts[i].text
        print other[i].get_attribute("title")
        number = mode.findall(other[i].text)
        print number[0] #年份
        number = mode.findall(num[i].text)
        if len(number)==1: #由于存在数字确实 如(100) ()
            print number[0]
        elif len(number)==2:
            print number[0],number[1]
        print ''
        
        i = i + 1
        tag.click()
        time.sleep(1)
输出如下所示:
>>> 
网络资源辅助下的Python程序设计教学 
本文对于Python学习网络资源做了归纳分类,说明了每类资源的特点,具体介绍了几个有特色的学习网站,就网络资源辅助下的Python学习进行了讨论,阐释了利用优质网络资源可以提高课堂教学效果,增加教学的生动性、直观性和交互性。同时说明了这些资源的利用能够方便学生的编程训练,使学生有更多的时间和机会动手编程,实现编程教学中...
电子技术与软件工程
2017
11 0

Python虚拟机内存管理的研究 
动态语言的简洁性,易学性缩短了软件开发人员的开发周期,所以深受研发人员的喜爱。其在机器学习、科学计算、Web开发等领域都有广泛的应用。在众多的动态语言中,Python是用户数量较大的动态语言之一。本文主要研究Python对内存资源的管理。Python开发效率高,但是运行效率常为人诟病,主要原因在于一切皆是对象的语言实现...
南京大学
2014
156 0
接下来是点击详情页面,窗口转化捕获信息,代码如下:
# -*- coding: utf-8 -*-
import time              
import re              
import sys    
import codecs    
import urllib   
from selenium import webdriver            
from selenium.webdriver.common.keys import Keys            
  
      
#主函数
if __name__ == '__main__':

    url = "http://search.cnki.net/Search.aspx?q=python&rank=relevant&cluster=all&val=&p=0"
    driver = webdriver.Firefox()
    driver.get(url)
    #标题
    content = driver.find_elements_by_xpath("//div[@class='wz_content']/h3")
    #摘要
    abstracts = driver.find_elements_by_xpath("//div[@class='width715']")
    #出版杂志+年份
    other = driver.find_elements_by_xpath("//span[@class='year-count']/span[1]")
    mode = re.compile(r'\d+\.?\d*')
    #下载次数 被引次数
    num = driver.find_elements_by_xpath("//span[@class='count']")

    #获取当前窗口句柄  
    now_handle = driver.current_window_handle

    #获取内容
    i = 0
    for tag in content:
        print tag.text
        print abstracts[i].text
        print other[i].get_attribute("title")
        number = mode.findall(other[i].text)
        print number[0] #年份
        number = mode.findall(num[i].text)
        if len(number)==1: #由于存在数字确实 如(100) ()
            print number[0]
        elif len(number)==2:
            print number[0],number[1]
        print ''
        
        i = i + 1
        tag.click()
        time.sleep(2)

        #跳转 获取所有窗口句柄  
        all_handles = driver.window_handles  
      
        #弹出两个界面,跳转到不是主窗体界面  
        for handle in all_handles:  
            if handle!=now_handle:     
                #输出待选择的窗口句柄  
                print handle  
                driver.switch_to_window(handle)  
                time.sleep(1)  
  
                print u'弹出界面信息'  
                print driver.current_url  
                print driver.title  
  
                #获取登录连接信息  
                elem_sub = driver.find_element_by_xpath("//div[@class='summary pad10']")  
                print u"作者", elem_sub.text   
                print ''  
  
                #关闭当前窗口  
                driver.close()  
              
        #输出主窗口句柄  
        print now_handle  
        driver.switch_to_window(now_handle) #返回主窗口 开始下一个跳转
        
但部分网站还是出现无法访问的问题,如下所示:


最后作者拟爬取万方数据进行分析。
最后希望文章对你有所帮助,如果错误或不足之处,请海涵~
(By:Eastmount 2017-11-17 深夜12点  http://blog.csdn.net/eastmount/ )



作者:Eastmount 发表于2017/11/17 11:16:57 原文链接
阅读:2 评论:1 查看评论

无域控Always On实战教学 (一)

$
0
0

前言

最近正好给客户安装部署无域控的Always On 。详细说明下安装部署的细节和碰到的坑.欢迎各位同学拍砖和评论,有什么问题都可以在下面留言。


什么是Always On

SQL Server 2012 引入了高可用性组AlwaysOn,用来代替数据库镜像(SQL Server 2005 SP1开始支持的),但是它有一个巨大的局限性:承载副本的节点必须是在同一Windows故障转移群集中。Windows故障转移群集中的安装部署又需要域控。这台域控必须是单独一台或者两台(主备)服务器。这无形中增加了投入的成本。而且对于不熟悉域控的人来说,域控可以说是非常麻烦。出现问题的时候很难排查。好消息是,从Windows Server 2016 开始我们就在需要域控了。在SQL Server 2016之前,高可用性组只能在SQL Server企业版的才能使用。与数据库镜像相比,这是另一个缺点(因为即使在标准版中也有同步镜像)。在SQL Server 2016中,Microsoft现在为SQL Server的标准版提供了基本可用性组,它提供了与数据库镜像相同的功能:

只有2个副本
同步提交
每个可用性组1个数据库
没有可读的副本
使用SQL Server 2016和Windows Server 2016,Always On可以真正的替代数据库镜像,该技术也适用于SQL Server的标准版。好了,前面的大多都是废话,不多说,开整。


环境准备


(由于客户的真实环境不好截图,我这里的截图,都是使用的本地的截图,但是环境都是一样的)

2台数据库服务器    192.168.1.57   192.168.1.59 

操作系统版本 


数据库版本



安装WSFC (Windows 故障转移集群)

打开服务器管理器


添加角色和功能


一直点下一步,,直到下面这个界面:


然后点下一步,安装,然后重启服务器。完成安装。到这来故障转移集群功能安装完成。

作者:z10843087 发表于2017/11/17 11:23:46 原文链接
阅读:1 评论:0 查看评论

Linux学习总结(43)——企业运维最常用的150个Linux命令

$
0
0
一、线上查询及帮助命令 (2 个)
man
查看命令帮助,命令的词典,更复杂的还有 info,但不常用。
help
查看 Linux 内置命令的帮助,比如 cd 命令。


二、文件和目录操作命令 (18 个)
ls
全拼 list,功能是列出目录的内容及其内容属性信息。
cd
全拼 change directory,功能是从当前工作目录切换到指定的工作目录。
cp
全拼 copy,其功能为复制文件或目录。
find
查找的意思,用于查找目录及目录下的文件。
mkdir
全拼 make directories,其功能是创建目录。
mv
全拼 move,其功能是移动或重命名文件。
pwd
全拼 print working directory,其功能是显示当前工作目录的绝对路径。
rename
用于重命名文件。
rm
全拼 remove,其功能是删除一个或多个文件或目录。
rmdir
全拼 remove empty directories,功能是删除空目录。
touch
创建新的空文件,改变已有文件的时间戳属性。
tree
功能是以树形结构显示目录下的内容。
basename
显示文件名或目录名。
dirname
显示文件或目录路径。
chattr
改变文件的扩展属性。
lsattr
查看文件扩展属性。
file
显示文件的类型。
md5sum
计算和校验文件的 MD5 值。


三、查看文件及内容处理命令(21 个)
cat
全拼 concatenate,功能是用于连接多个文件并且打印到屏幕输出或重定向到指定文件中。
tac
tac 是 cat 的反向拼写,因此命令的功能为反向显示文件内容。
more
分页显示文件内容。
less
分页显示文件内容,more 命令的相反用法。
head
显示文件内容的头部。
tail
显示文件内容的尾部。
cut
将文件的每一行按指定分隔符分割并输出。
split
分割文件为不同的小片段。
paste
按行合并文件内容。
sort
对文件的文本内容排序。
uniq
去除重复行。oldboy
wc
统计文件的行数、单词数或字节数。
iconv
转换文件的编码格式。
dos2unix
将 DOS 格式文件转换成 UNIX 格式。
diff
全拼 difference,比较文件的差异,常用于文本文件。
vimdiff
命令行可视化文件比较工具,常用于文本文件。
rev
反向输出文件内容。
grep/egrep
过滤字符串,三剑客老三。
join
按两个文件的相同字段合并。
tr
替换或删除字符。
vi/vim
命令行文本编辑器。


四、文件压缩及解压缩命令(4 个)
tar
打包压缩。oldboy
unzip
解压文件。
gzip
gzip 压缩工具。
zip
压缩工具。


五、信息显示命令(11 个)
uname
显示操作系统相关信息的命令。
hostname
显示或者设置当前系统的主机名。
dmesg
显示开机信息,用于诊断系统故障。
uptime
显示系统运行时间及负载。
stat
显示文件或文件系统的状态。
du
计算磁盘空间使用情况。
df
报告文件系统磁盘空间的使用情况。
top
实时显示系统资源使用情况。
free
查看系统内存。
date
显示与设置系统时间。
cal
查看日历等时间信息。


六、搜索文件命令(4 个)
which
查找二进制命令,按环境变量 PATH 路径查找。
find
从磁盘遍历查找文件或目录。
whereis
查找二进制命令,按环境变量 PATH 路径查找。
locate
从数据库 (/var/lib/mlocate/mlocate.db) 查找命令,使用 updatedb 更新库。


七、用户管理命令(10 个)
useradd
添加用户。
usermod
修改系统已经存在的用户属性。
userdel
删除用户。
groupadd
添加用户组。
passwd
修改用户密码。
chage
修改用户密码有效期限。
id
查看用户的 uid,gid 及归属的用户组。
su
切换用户身份。
visudo
编辑 / etc/sudoers 文件的专属命令。
sudo
以另外一个用户身份(默认 root 用户)执行事先在 sudoers 文件允许的命令。


八、基础网络操作命令(11 个)
telnet
使用 TELNET 协议远程登录。
ssh
使用 SSH 加密协议远程登录。
scp
全拼 secure copy,用于不同主机之间复制文件。
wget
命令行下载文件。
ping
测试主机之间网络的连通性。
route
显示和设置 linux 系统的路由表。
ifconfig
查看、配置、启用或禁用网络接口的命令。
ifup
启动网卡。
ifdown
关闭网卡。
netstat
查看网络状态。
ss
查看网络状态。


九、深入网络操作命令(9 个)
nmap
网络扫描命令。
lsof
全名 list open files,也就是列举系统中已经被打开的文件。
mail
发送和接收邮件。
mutt
邮件管理命令。
nslookup
交互式查询互联网 DNS 服务器的命令。
dig
查找 DNS 解析过程。
host
查询 DNS 的命令。
traceroute
追踪数据传输路由状况。
tcpdump
命令行的抓包工具。


十、有关磁盘与文件系统的命令(16 个)
mount
挂载文件系统。
umount
卸载文件系统。
fsck
检查并修复 Linux 文件系统。
dd
转换或复制文件。
dumpe2fs
导出 ext2/ext3/ext4 文件系统信息。
dump
ext2/3/4 文件系统备份工具。
fdisk
磁盘分区命令,适用于 2TB 以下磁盘分区。
parted
磁盘分区命令,没有磁盘大小限制,常用于 2TB 以下磁盘分区。
mkfs
格式化创建 Linux 文件系统。
partprobe
更新内核的硬盘分区表信息。
e2fsck
检查 ext2/ext3/ext4 类型文件系统。
mkswap
创建 Linux 交换分区。
swapon
启用交换分区。
swapoff
关闭交换分区。
sync
将内存缓冲区内的数据写入磁盘。
resize2fs
调整 ext2/ext3/ext4 文件系统大小。


十一、系统权限及用户授权相关命令(4 个)

chmod
改变文件或目录权限。
chown
改变文件或目录的属主和属组。
chgrp
更改文件用户组。
umask
显示或设置权限掩码。


十二、查看系统用户登陆信息的命令(7 个)
whoami
显示当前有效的用户名称,相当于执行 id -un 命令。
who
显示目前登录系统的用户信息。
w
显示已经登陆系统的用户列表,并显示用户正在执行的指令。
last
显示登入系统的用户。
lastlog
显示系统中所有用户最近一次登录信息。
users
显示当前登录系统的所有用户的用户列表。
finger
查找并显示用户信息。


十三、内置命令及其它(19 个)
echo
打印变量,或直接输出指定的字符串
printf
将结果格式化输出到标准输出。
rpm
管理 rpm 包的命令。
yum
自动化简单化地管理 rpm 包的命令。
watch
周期性的执行给定的命令,并将命令的输出以全屏方式显示。
alias
设置系统别名。
unalias
取消系统别名。
date
查看或设置系统时间。
clear
清除屏幕,简称清屏。
history
查看命令执行的历史纪录。
eject
弹出光驱。
time
计算命令执行时间。
nc
功能强大的网络工具。
xargs
将标准输入转换成命令行参数。
exec
调用并执行指令的命令。
export
设置或者显示环境变量。
unset
删除变量或函数。
type
用于判断另外一个命令是否是内置命令。
bc
命令行科学计算器


十四、系统管理与性能监视命令 (9 个)
chkconfig
管理 Linux 系统开机启动项。
vmstat
虚拟内存统计。
mpstat
显示各个可用 CPU 的状态统计。
iostat
统计系统 IO。
sar
全面地获取系统的 CPU、运行队列、磁盘 I/O、分页(交换区)、内存、 CPU 中断和网络等性能数据。
ipcs
用于报告 Linux 中进程间通信设施的状态,显示的信息包括消息列表、共享内存和信号量的信息。
ipcrm
用来删除一个或更多的消息队列、信号量集或者共享内存标识。
strace
用于诊断、调试 Linux 用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。
ltrace
命令会跟踪进程的库函数调用, 它会显现出哪个库函数被调用。


十五、关机 / 重启 / 注销和查看系统信息的命令(6 个)
shutdown
关机。
halt
关机。
poweroff
关闭电源。
logout
退出当前登录的 Shell。
exit
退出当前登录的 Shell。
Ctrl+d
退出当前登录的 Shell 的快捷键。


十六、进程管理相关命令(15 个)
bg
将一个在后台暂停的命令,变成继续执行  (在后台执行)。
fg
将后台中的命令调至前台继续运行。
jobs
查看当前有多少在后台运行的命令。
kill
终止进程。
killall
通过进程名终止进程。
pkill
通过进程名终止进程。
crontab
定时任务命令。
ps
显示进程的快照。
pstree
树形显示进程。
nice/renice
调整程序运行的优先级。
nohup
忽略挂起信号运行指定的命令。
pgrep
查找匹配条件的进程。
runlevel
查看系统当前运行级别。
init
切换运行级别。
service
启动、停止、重新启动和关闭系统服务,还可以显示所有系统服务的当前状态。
作者:u012562943 发表于2017/11/17 11:25:27 原文链接
阅读:0 评论:0 查看评论

itext7学习笔记——第5章实践&example

$
0
0

    本章的例子,请参考我翻译的博文:itext7学习笔记——第5章,里面有详细的解释,有什么不懂得也可以评论或者私信我!

## 例子1:添加注释和内容

    我们读取一个带有表单的PDF文档,往里面添加文档注释、一些文本和一个新的复选框,代码如下:

import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfButtonFormField;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.io.font.FontConstants;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfTextAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import java.io.File;
import java.io.IOException;

/**
 * Simple adding annotations example.
 */
public class C05E01_AddAnnotationsAndContent {

    public static final String SRC = "src/main/resources/pdf/job_application.pdf";
    public static final String DEST = "results/chapter05/edited_job_application.pdf";

    public static void main(String args[]) throws IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();
        new C05E01_AddAnnotationsAndContent().manipulatePdf(SRC, DEST);
    }

    public void manipulatePdf(String src, String dest) throws IOException {

        //Initialize PDF document
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(src), new PdfWriter(dest));

        //Add text annotation
        PdfAnnotation ann = new PdfTextAnnotation(new Rectangle(400, 795, 0, 0))
                .setTitle(new PdfString("iText"))
                .setContents("Please, fill out the form.")
                .setOpen(true);
        pdfDoc.getFirstPage().addAnnotation(ann);

        PdfCanvas canvas = new PdfCanvas(pdfDoc.getFirstPage());
        canvas.beginText().setFontAndSize(PdfFontFactory.createFont(FontConstants.HELVETICA), 12)
                .moveText(265, 597)
                .showText("I agree to the terms and conditions.")
                .endText();

        //Add form field
        PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
        PdfButtonFormField checkField = PdfFormField.createCheckBox(pdfDoc, new Rectangle(245, 594, 15, 15),
                "agreement", "Off", PdfFormField.TYPE_CHECK);
        checkField.setRequired(true);
        form.addField(checkField);

        //Update reset button
        form.getField("reset").setAction(PdfAction.createResetForm(new String[]{"name", "language",
                "experience1", "experience2", "experience3", "shift", "info", "agreement"}, 0));

        pdfDoc.close();

    }
}

## 例子2:改变表单字段属性

    我们读取一个带有表单的PDF文档,然后改变字段的属性,添加属性或者删除属性,代码如下:

import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.io.font.FontConstants;
import com.itextpdf.kernel.color.Color;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.*;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Simple filling out form example.
 */
public class C05E02_FillAndModifyForm {

    public static final String SRC = "src/main/resources/pdf/job_application.pdf";
    public static final String DEST = "results/chapter05/filled_out_job_application.pdf";

    public static void main(String args[]) throws IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();
        new C05E02_FillAndModifyForm().manipulatePdf(SRC, DEST);
    }

    public void manipulatePdf(String src, String dest) throws IOException {

        //Initialize PDF document
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(src), new PdfWriter(dest));


        PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
        Map<String, PdfFormField> fields = form.getFormFields();

        fields.get("name").setValue("James Bond").setBackgroundColor(Color.ORANGE);
        fields.get("language").setValue("English");

        fields.get("experience1").setValue("Yes");
        fields.get("experience2").setValue("Yes");
        fields.get("experience3").setValue("Yes");

        List<PdfString> options = new ArrayList<PdfString>();
        options.add(new PdfString("Any"));
        options.add(new PdfString("8.30 am - 12.30 pm"));
        options.add(new PdfString("12.30 pm - 4.30 pm"));
        options.add(new PdfString("4.30 pm - 8.30 pm"));
        options.add(new PdfString("8.30 pm - 12.30 am"));
        options.add(new PdfString("12.30 am - 4.30 am"));
        options.add(new PdfString("4.30 am - 8.30 am"));
        PdfArray arr = new PdfArray(options);
        fields.get("shift").setOptions(arr);
        fields.get("shift").setValue("Any");

        PdfFont courier = PdfFontFactory.createFont(FontConstants.COURIER);
        fields.get("info").setValue("I was 38 years old when I became an MI6 agent.", courier, 7f);

        pdfDoc.close();

    }
}

## 例子3:添加页眉、页脚和水印

    我们读取一个带有ufo信息的PDF文档,往里面添加页眉、页脚和水印,代码如下:

import com.itextpdf.io.font.FontConstants;
import com.itextpdf.kernel.color.Color;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.extgstate.PdfExtGState;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.property.TextAlignment;
import com.itextpdf.layout.property.VerticalAlignment;

import java.io.File;
import java.io.IOException;

/**
 * Simple adding content example.
 */
public class C05E03_AddContent {

    public static final String SRC = "src/main/resources/pdf/ufo.pdf";

    public static final String DEST = "results/chapter05/add_content.pdf";

    public static void main(String args[]) throws IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();
        new C05E03_AddContent().manipulatePdf(SRC, DEST);
    }

    public void manipulatePdf(String src, String dest) throws IOException {

        //Initialize PDF document
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(src), new PdfWriter(dest));

        Document document = new Document(pdfDoc);
        Rectangle pageSize;
        PdfCanvas canvas;
        int n = pdfDoc.getNumberOfPages();
        for (int i = 1; i <= n; i++) {
            PdfPage page = pdfDoc.getPage(i);
            pageSize = page.getPageSize();
            canvas = new PdfCanvas(page);
            //Draw header text
            canvas.beginText().setFontAndSize(PdfFontFactory.createFont(FontConstants.HELVETICA), 7)
                    .moveText(pageSize.getWidth() / 2 - 24, pageSize.getHeight() - 10)
                    .showText("I want to believe")
                    .endText();
            //Draw footer line
            canvas.setStrokeColor(Color.BLACK)
                    .setLineWidth(.2f)
                    .moveTo(pageSize.getWidth() / 2 - 30, 20)
                    .lineTo(pageSize.getWidth() / 2 + 30, 20).stroke();
            //Draw page number
            canvas.beginText().setFontAndSize(PdfFontFactory.createFont(FontConstants.HELVETICA), 7)
                    .moveText(pageSize.getWidth() / 2 - 7, 10)
                    .showText(String.valueOf(i))
                    .showText(" of ")
                    .showText(String.valueOf(n))
                    .endText();
            //Draw watermark
            Paragraph p = new Paragraph("CONFIDENTIAL").setFontSize(60);
            canvas.saveState();
            PdfExtGState gs1 = new PdfExtGState().setFillOpacity(0.2f);
            canvas.setExtGState(gs1);
            document.showTextAligned(p,
                    pageSize.getWidth() / 2, pageSize.getHeight() / 2,
                    pdfDoc.getPageNumber(page),
                    TextAlignment.CENTER, VerticalAlignment.MIDDLE, 45);
            canvas.restoreState();
        }

        pdfDoc.close();

    }
}

## 例子4:改变页面大小和方向

    我们读取一个带有ufo信息的PDF文档,改变每一页的大小,并使偶数页颠倒,代码如下:

import com.itextpdf.kernel.color.*;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.*;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;

import java.io.File;
import java.io.IOException;

/**
 * Simple changing page properties example.
 */
public class C05E04_ChangePage {

    public static final String SRC = "src/main/resources/pdf/ufo.pdf";
    public static final String DEST = "results/chapter05/change_page.pdf";

    public static void main(String args[]) throws IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();
        new C05E04_ChangePage().manipulatePdf(SRC, DEST);
    }

    public void manipulatePdf(String src, String dest) throws IOException {

        //Initialize PDF document
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(src), new PdfWriter(dest));


        float margin = 72;
        for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
            PdfPage page = pdfDoc.getPage(i);
            // change page size
            Rectangle mediaBox = page.getMediaBox();
            Rectangle newMediaBox = new Rectangle(mediaBox.getLeft() - margin, mediaBox.getBottom() - margin,
                    mediaBox.getWidth() + margin * 2, mediaBox.getHeight() + margin * 2);
            page.setMediaBox(newMediaBox);
            // add border
            PdfCanvas over = new PdfCanvas(page);
            over.setStrokeColor(Color.GRAY);
            over.rectangle(mediaBox.getLeft(), mediaBox.getBottom(), mediaBox.getWidth(), mediaBox.getHeight());
            over.stroke();
            // change rotation of the even pages
            if (i % 2 == 0) {
                page.setRotation(180);
            }
        }

        pdfDoc.close();

    }
}

Example代码下载

    本章代码可在如下地址下载(IDEA工程):iText7——第五章源代码工程

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

Quartz-JobListener解读

$
0
0

概述

在某个所关注事件发生时,监听器提供了一种方便且非侵入性的机制来获得这一通知。Quartz 提供了三种类型的监听器:监听 Job 的,监听 Trigger 的,和监听 Scheduler 自已的。

本博文阐述如何应用每一种类型来更好的管理你的 Quartz 应用,并获悉到什么事件正在发生。

参考官方Demo:
http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/tutorial-lesson-07.html


步骤简述

要创建一个Listener, 只需要创建一个实现了org.quartz.TriggerListener或org.quartz.JobListener接口的对象即可。

在运行的时候,将Listeners注册进scheduler, 而且必须给一个name(可以通过他们的getName()方法获取Listener的name)。

除了继承接口,类也可以继承JobListenerSupport或TriggerListenerSupport,重写你感兴趣的event。

Listener通过scheduler的ListenerManager来注册,其中的Matcher 里描述哪个Jobs、Triggers需要被监听。

注意:
LListeners在运行的时候被注册进scheduler, 而不是保存在JobStore。Listener是和你的应用集成在一起的,这样每次你的应用运行的时候,都会在scheduler中重新注册listeners。


全局/非全局监听器

JobListener 和 TriggerListener 可被注册为全局或非全局监听器。

  • 全局监听器能接收到所有的 Job/Trigger 的事件通知。

  • 非全局监听器(或者说是一个标准的监听器) 只能接收到那些在其上已注册了监听器的 Job 或 Triiger 的事件。

你要注册你的监听器为全局或非全局的需依据你特定的应用需要。

全局监听器是主动意识的,它们为了执行它们的任务而热切的去寻找每一个可能的事件。通常全局监听器要做的工作不用指定到特定的 Job 或 Trigger。非全局监听器一般是被动意识的,它们在所关注的 Trigger 激发之前或是 Job 执行之前什么事也不做。因此,非全局的监听器比起全局监听器而言更适合于修改或增加 Job 执行的工作。这有点像知名的装饰设计模式的装饰器。

全局 监听器

scheduler.addGlobalTriggerListener(new SimpleMyTriggerListener());

非全局监听器

scheduler.addTriggerListener( triggerListener );
trigger.addTriggerListener( triggerListener.getName() );

JobListener 任务监听器 示例

这里写图片描述

JobListener源码

我们先来看下JobListener的源码:

这里写图片描述

  • getName() :返回一个字符串用以说明 JobListener 的名称。对于注册为全局的监听器,getName()主要用于记录日志,对于由特定 Job 引用的JobListener,注册在 JobDetail 上的监听器名称必须匹配从监听器getName() 方法的返回值。

  • jobToBeExecuted() :Scheduler 在 JobDetail 将要被执行时调用这个方法。

  • jobExecutionVetoed() :Scheduler 在 JobDetail 即将被执行,但又被 TriggerListener否决了时调用这个方法。

  • jobWasExecuted() :Scheduler 在 JobDetail 被执行之后调用这个方法。


自定义监听器

import org.quartz.*;

public class MyJobListener implements JobListener {

    @Override
    public String getName() {
        return "MyJobListener";
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext arg0) {
        System.out.println("Job监听器:MyJobListener.jobExecutionVetoed()");
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext arg0) {
        System.out.println("Job监听器:MyJobListener.jobToBeExecuted()");

    }

    @Override
    public void jobWasExecuted(JobExecutionContext arg0,
                               JobExecutionException arg1) {
        System.out.println("Job监听器:MyJobListener.jobWasExecuted()");

    }

}

注册监听器

MyJobListener myJobListener=new MyJobListener(); 

// 添加一个特定的job
scheduler.getListenerManager().addJobListener(myJobListener, KeyMatcher.jobKeyEquals(new JobKey("myJobName", "myJobGroup")));

上面的代码就可以变成:

scheduler.getListenerManager().addJobListener(myJobListener, jobKeyEquals(jobKey("myJobName", "myJobGroup")));

// 添加特定组的所有jobs
scheduler.getListenerManager().addJobListener(myJobListener, jobGroupEquals("myJobGroup"));

// 添加多个特定组的所有jobs
scheduler.getListenerManager().addJobListener(myJobListener, or(jobGroupEquals("myJobGroup"), jobGroupEquals("yourGroup")));

// 添加所有jobs
scheduler.getListenerManager().addJobListener(myJobListener, allJobs());

有了Listeners以后,当应用需要在某些事件发生以后去通知你的应用,这时就不需要Job去明确地去告知你的应用了。


完整示例

添加一个jobListener,监听到job1执行后,再触发一个job2任务

Job1.java

package com.xgj.quartz.quartzItself.listener.jobListener;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;

public class Job1 implements Job {

    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        JobKey jobKey = context.getJobDetail().getKey();

        System.out.println("\nJob1 - 任务key "
                + jobKey
                + "执行时间:"
                + sdf.format(new Date()));
    }
}

Job2.java

package com.xgj.quartz.quartzItself.listener.jobListener;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;

public class Job2 implements Job {

    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        JobKey jobKey = context.getJobDetail().getKey();

        System.err.println("\nJob2 - 任务key "
                + jobKey
                + "执行时间:"
                + sdf.format(new Date()));
    }
}

自定义JobListener

package com.xgj.quartz.quartzItself.listener.jobListener;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;


/**
 * 
 * 
 * @ClassName: MyJobListener
 * 
 * @Description: 自定义Job监听器
 * 
 *               getName() :返回一个字符串用以说明 JobListener 的名称。对于注册为全局的监听器,getName()
 *               主要用于记录日志,对于由特定 Job 引用的 JobListener,注册在 JobDetail
 *               上的监听器名称必须匹配从监听器上 getName() 方法的返回值.
 * 
 *               jobToBeExecuted() :Scheduler 在 JobDetail 将要被执行时调用这个方法。
 * 
 *               jobExecutionVetoed() :Scheduler 在 JobDetail 即将被执行,但又被
 *               TriggerListener 否决了时调用这个方法。
 * 
 *               jobWasExecuted() :Scheduler 在 JobDetail 被执行之后调用这个方法。
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年11月16日 下午3:57:35
 */
public class MyJobListener implements JobListener {

    @Override
    public String getName() {
        return "MyJobListerner";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println("Job监听器:MyJobListener.jobToBeExecuted()");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println("Job监听器:MyJobListener.jobExecutionVetoed()");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context,
            JobExecutionException jobException) {

        System.out.println("Job监听器:MyJobListener.jobWasExecuted()");

         // 设置另外一个job执行
        JobDetail job2 = JobBuilder.newJob(Job2.class).withIdentity("job2")
                .build();

        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("job2Trigger").startNow().build();

        try {
            context.getScheduler().scheduleJob(job2, trigger);
        } catch (SchedulerException e) {
            System.err.println("无法安排job2!");
            e.printStackTrace();
        }


    }

}

调度类

package com.xgj.quartz.quartzItself.listener.jobListener;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.quartz.Matcher;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SchedulerMetaData;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;


public class JobListenerDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("------- 初始化 ----------------------");

        // Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();

        // Job
        JobDetail job = newJob(Job1.class).withIdentity("job1", "group1")
                .build();

        // Tirgger 
        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow()
                .build();

        // 设置监听器
        JobListener jobListener = new MyJobListener();
        Matcher<JobKey> matcher = KeyMatcher.keyEquals(job.getKey());
        scheduler.getListenerManager().addJobListener(jobListener, matcher);

        // 将job任务加入到调度器
        scheduler.scheduleJob(job, trigger);

        // 开始任务
        System.out.println("------- 开始执行调度器 Scheduler ----------------");
        scheduler.start();

        try {
            System.out.println("------- 等待 30 秒... --------------");
            Thread.sleep(30L * 1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }

        scheduler.shutdown(true);
        System.out.println("------- 关闭调度器 -----------------");

        SchedulerMetaData metaData = scheduler.getMetaData();
        System.out.println("~~~~~~~~~~  执行了 "
                + metaData.getNumberOfJobsExecuted() + " 个 jobs.");

    }

}

运行结果

------- 初始化 ----------------------
INFO  StdSchedulerFactory - Using default implementation for ThreadExecutor
INFO  SimpleThreadPool - Job execution threads will use class loader of thread: main
INFO  SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
INFO  QuartzScheduler - Quartz Scheduler v.2.2.3 created.
INFO  RAMJobStore - RAMJobStore initialized.
INFO  QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.3) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

INFO  StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
INFO  StdSchedulerFactory - Quartz scheduler version: 2.2.3
------- 开始执行调度器 Scheduler ----------------
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
------- 等待 30 秒... --------------
Job监听器:MyJobListener.jobToBeExecuted()

Job1 - 任务key group1.job1执行时间:2017-11-16 17:58:54
Job监听器:MyJobListener.jobWasExecuted()

Job2 - 任务key DEFAULT.job2执行时间:2017-11-16 17:58:54
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down.
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED paused.
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutdown complete.
------- 关闭调度器 -----------------
~~~~~~~~~~  执行了 2 个 jobs.

示例源码

代码已托管到Github—> https://github.com/yangshangwei/SpringMaster

作者:yangshangwei 发表于2017/11/17 5:45:07 原文链接
阅读:19 评论:0 查看评论

Quartz-TriggerListener解读

$
0
0

概述

Quartz-JobListener解读

与 JobListener 有所不同的是, TriggerListener 接口还有关于 Trigger 实例生命周期的方法。


TriggerListener接口方法

我们先看下TriggerListener的源码

import org.quartz.Trigger.CompletedExecutionInstruction;

public interface TriggerListener {

    String getName();

    void triggerFired(Trigger var1, JobExecutionContext var2);

    boolean vetoJobExecution(Trigger var1, JobExecutionContext var2);

    void triggerMisfired(Trigger var1);

    void triggerComplete(Trigger var1, JobExecutionContext var2, CompletedExecutionInstruction var3);
}
  • getName():和前面的 JobListener 一样,TriggerListner 接口的 getName()返回一个字符串用以说明监听器的名称。对于非全局的 TriggerListener,在 addTriggerListener()方法中给定的名称必须与监听器的 getName() 方法返回值相匹配。

  • triggerFired() :当与监听器相关联的 Trigger 被触发,Job 上的 execute()方法将要被执行时,Scheduler 就调用这个方法。在全局 TriggerListener 情况下,这个方法为所有 Trigger被调用。

  • vetoJobExecution():在 Trigger 触发后,Job 将要被执行时由 Scheduler调用这个方法。TriggerListener 给了一个选择去否决 Job 的执行。假如这个方法返回 true,这个 Job 将不会为此次Trigger 触发而得到执行。

  • triggerMisfired():Scheduler 调用这个方法是在 Trigger 错过触发时。如这个方法的 JavaDoc所指出的,你应该关注此方法中持续时间长的逻辑:在出现许多错过触发的 Trigger 时,长逻辑会导致骨牌效应。你应当保持这上方法尽量的小。

  • triggerComplete():Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用这个方法。这不是说这个Trigger 将不再触发了,而仅仅是当前 Trigger 的触发(并且紧接着的 Job 执行) 结束时。这个 Trigger也许还要在将来触发多次的。


示例

这里写图片描述

Job

package com.xgj.quartz.quartzItself.listener.triggerListener;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;

public class SimpleJob1 implements Job {

    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        JobKey jobKey = context.getJobDetail().getKey();

        System.out.println("\nJob1 - 任务key "
                + jobKey
                + "执行时间:"
                + sdf.format(new Date()));
    }
}

Job

package com.xgj.quartz.quartzItself.listener.triggerListener;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;

public class SimpleJob2 implements Job {

    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        JobKey jobKey = context.getJobDetail().getKey();

        System.err.println("\nJob2 - 任务key "
                + jobKey
                + "执行时间:"
                + sdf.format(new Date()));
    }
}

自定义TriggerListener

package com.xgj.quartz.quartzItself.listener.triggerListener;

import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;

public class MyTriggerListener implements TriggerListener {
    @Override
    public String getName() {
        return "MyTriggerListener";
    }

    /**
     * (1) Trigger被激发 它关联的job即将被运行
     *
     * @param trigger
     * @param context
     */
    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        System.out.println("MyTrigger监听器:" + trigger.getJobKey()
                + ",进入方法 triggerFired()");
    }

    /**
     * (2) Trigger被激发 它关联的job即将被运行,先执行(1),在执行(2) 如果返回TRUE 那么任务job会被终止
     *
     * @param trigger
     * @param context
     * @return
     */
    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        System.out.println("MyTrigger监听器:" + trigger.getJobKey()
                + ",vetoJobExecution()");
        return false;
    }

    /**
     * (3) 当Trigger错过被激发时执行,比如当前时间有很多触发器都需要执行,但是线程池中的有效线程都在工作,那么有的触发器就有可能超时,
     * 错过这一轮的触发。
     *
     * @param trigger
     */
    @Override
    public void triggerMisfired(Trigger trigger) {
        System.out.println("MyTrigger监听器:" + trigger.getJobKey()
                + ",triggerMisfired()");
    }

    /**
     * (4) 任务完成时触发
     *
     * @param trigger
     * @param jobExecutionContext
     * @param completedExecutionInstruction
     */
    @Override
    public void triggerComplete(Trigger trigger,
            JobExecutionContext jobExecutionContext,
            Trigger.CompletedExecutionInstruction completedExecutionInstruction) {
        System.out.println("MyTrigger监听器:" + trigger.getJobKey()
                + ",triggerComplete()");
    }
}

调度类

package com.xgj.quartz.quartzItself.listener.triggerListener;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

import org.quartz.JobDetail;
import org.quartz.Matcher;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SchedulerMetaData;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.TriggerListener;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;


public class TriggerListenerDemo {
    public static void main(String[] args) throws Exception {

        System.out.println("------- 初始化 ----------------------");

        // Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();

        // Job
        JobDetail job = newJob(SimpleJob1.class).withIdentity("job1",
                "group1")
                .build();

        // Tirgger
        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow().build();

        // 设置监听器
        TriggerListener triggerListener = new MyTriggerListener();
        Matcher<TriggerKey> matcher = KeyMatcher.keyEquals(trigger.getKey());
        scheduler.getListenerManager().addTriggerListener(triggerListener,
                matcher);

        // 将job任务加入到调度器
        scheduler.scheduleJob(job, trigger);

        // 开始任务
        System.out.println("------- 开始执行调度器 Scheduler ----------------");
        scheduler.start();

        try {
            System.out.println("------- 等待 30 秒... --------------");
            Thread.sleep(30L * 1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }

        scheduler.shutdown(true);
        System.out.println("------- 关闭调度器 -----------------");

        SchedulerMetaData metaData = scheduler.getMetaData();
        System.out.println("~~~~~~~~~~  执行了 "
                + metaData.getNumberOfJobsExecuted() + " 个 jobs.");

    }

}

运行结果

------- 初始化 ----------------------
INFO  StdSchedulerFactory - Using default implementation for ThreadExecutor
INFO  SimpleThreadPool - Job execution threads will use class loader of thread: main
INFO  SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
INFO  QuartzScheduler - Quartz Scheduler v.2.2.3 created.
INFO  RAMJobStore - RAMJobStore initialized.
INFO  QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.3) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

INFO  StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
INFO  StdSchedulerFactory - Quartz scheduler version: 2.2.3
------- 开始执行调度器 Scheduler ----------------
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
------- 等待 30 秒... --------------
MyTrigger监听器:group1.job1,进入方法 triggerFired()
MyTrigger监听器:group1.job1,vetoJobExecution()

Job1 - 任务key group1.job1执行时间:2017-11-16 21:52:50
MyTrigger监听器:group1.job1,triggerComplete()
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down.
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED paused.
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutdown complete.
------- 关闭调度器 -----------------
~~~~~~~~~~  执行了 1 个 jobs.

示例源码

代码已托管到Github—> https://github.com/yangshangwei/SpringMaster

作者:yangshangwei 发表于2017/11/17 9:56:02 原文链接
阅读:3 评论:0 查看评论

Quartz-SchedulerListener解读

$
0
0

概述

SchedulerListener 是在 Scheduler 级别的事件产生时得到通知,不管是增加还是移除 Scheduler 中的 Job,或者是 Scheduler 遭遇到了严重的错误时。那些事件多是关于对 Scheduler 管理的,而不是专注于 Job 或 Trigger 的。

org.quartz.SchedulerListener 接口包含了一系列的回调方法,它们会在 Scheduler 的生命周期中有关键事件发生时被调用。


SchedulerListener接口方法解读

我们先看下SchedulerListener的源码

public interface SchedulerListener {     
   public void jobScheduled(Trigger trigger);     
    public void jobUnscheduled(String triggerName, String triggerGroup);     
    public void triggerFinalized(Trigger trigger);     
    public void triggersPaused(String triggerName, String triggerGroup);     
    public void triggersResumed(String triggerName,String triggerGroup);     
    public void jobsPaused(String jobName, String jobGroup);     
    public void jobsResumed(String jobName, String jobGroup);     
    public void schedulerError(String msg, SchedulerException cause);     
    public void schedulerShutdown();     
}
  • jobScheduled() 和 jobUnscheduled():Scheduler 在有新的 JobDetail 部署或卸载时调用这两个中的相应方法。

  • triggerFinalized() :当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。

  • triggersPaused():Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。

  • triggersResumed():Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,triggerName 参数将为 null。

  • jobsPaused():当一个或一组 JobDetail 暂停时调用这个方法。

  • jobsResumed():当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。

  • schedulerError():在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。错误的类型会各式的,但是下面列举了一些错误例子:

初始化 Job 类的问题
试图去找到下一 Trigger 的问题
JobStore 中重复的问题
数据存储连接的问题

你可以使用 SchedulerException 的 getErrorCode() 或者 getUnderlyingException() 方法或获取到特定错误的更详尽的信息。

  • schedulerShutdown():Scheduler 调用这个方法用来通知 SchedulerListener Scheduler 将要被关闭。

示例

Job复用TriggerListener中的SimpleJob1

详见 Quartz-TriggerListener解读

自定义SchedulerListener

package com.xgj.quartz.quartzItself.listener.schedulerListener;

import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.Trigger;
import org.quartz.TriggerKey;

public class MySchedulerListener implements SchedulerListener {

    @Override
    public void jobScheduled(Trigger trigger) {
        System.out.println("MySchedulerListener jobScheduled trigger");

    }

    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
        System.out.println("MySchedulerListener jobScheduled triggerKey");

    }

    @Override
    public void triggerFinalized(Trigger trigger) {
        System.out.println("MySchedulerListener triggerFinalized");

    }

    @Override
    public void triggerPaused(TriggerKey triggerKey) {
        System.out.println("MySchedulerListener triggerPaused");

    }

    @Override
    public void triggersPaused(String triggerGroup) {
        System.out.println("MySchedulerListener triggersPaused");

    }

    @Override
    public void triggerResumed(TriggerKey triggerKey) {
        System.out.println("MySchedulerListener triggerResumed triggerKey");

    }

    @Override
    public void triggersResumed(String triggerGroup) {
        System.out.println("MySchedulerListener triggerResumed triggerGroup");

    }

    @Override
    public void jobAdded(JobDetail jobDetail) {
        System.out.println("MySchedulerListener jobAdded");
    }

    @Override
    public void jobDeleted(JobKey jobKey) {
        System.out.println("MySchedulerListener jobDeleted");

    }

    @Override
    public void jobPaused(JobKey jobKey) {
        System.out.println("MySchedulerListener jobPaused jobKey");

    }

    @Override
    public void jobsPaused(String jobGroup) {
        System.out.println("MySchedulerListener jobsPaused jobGroup");

    }

    @Override
    public void jobResumed(JobKey jobKey) {
        System.out.println("MySchedulerListener jobResumed  jobKey");

    }

    @Override
    public void jobsResumed(String jobGroup) {
        System.out.println("MySchedulerListener jobsResumed  jobGroup");

    }

    @Override
    public void schedulerError(String msg, SchedulerException cause) {
        System.out.println("MySchedulerListener schedulerError");

    }

    @Override
    public void schedulerInStandbyMode() {
        System.out.println("MySchedulerListener schedulerInStandbyMode");

    }

    @Override
    public void schedulerStarted() {
        System.out.println("MySchedulerListener schedulerStarted");

    }

    @Override
    public void schedulerStarting() {
        System.out.println("MySchedulerListener schedulerStarting");

    }

    @Override
    public void schedulerShutdown() {
        System.out.println("MySchedulerListener schedulerShutdown");

    }

    @Override
    public void schedulerShuttingdown() {
        System.out.println("MySchedulerListener schedulerShuttingdown");

    }

    @Override
    public void schedulingDataCleared() {
        System.out.println("MySchedulerListener schedulingDataCleared");

    }

}

调度类

package com.xgj.quartz.quartzItself.listener.schedulerListener;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SchedulerListener;
import org.quartz.SchedulerMetaData;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import com.xgj.quartz.quartzItself.listener.triggerListener.SimpleJob1;

public class SchedulerListenerDemo {

    public static void main(String[] args) throws Exception {

        System.out.println("------- 初始化 ----------------------");

        // Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();

        // 添加监听器
        SchedulerListener schedulerListener = new MySchedulerListener();
        scheduler.getListenerManager().addSchedulerListener(schedulerListener);

        // Job
        JobDetail job = newJob(SimpleJob1.class).withIdentity("job1", "group1")
                .build();

        // Tirgger
        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow().build();

        // 将job任务加入到调度器
        scheduler.scheduleJob(job, trigger);

        // 开始任务
        System.out.println("------- 开始执行调度器 Scheduler ----------------");
        scheduler.start();

        try {
            System.out.println("------- 等待 30 秒... --------------");
            Thread.sleep(30L * 1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }

        scheduler.shutdown(true);
        System.out.println("------- 关闭调度器 -----------------");

        SchedulerMetaData metaData = scheduler.getMetaData();
        System.out.println("~~~~~~~~~~  执行了 "
                + metaData.getNumberOfJobsExecuted() + " 个 jobs.");

    }
}

运行结果

------- 初始化 ----------------------
INFO  StdSchedulerFactory - Using default implementation for ThreadExecutor
INFO  SimpleThreadPool - Job execution threads will use class loader of thread: main
INFO  SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
INFO  QuartzScheduler - Quartz Scheduler v.2.2.3 created.
INFO  RAMJobStore - RAMJobStore initialized.
INFO  QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.3) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

INFO  StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
INFO  StdSchedulerFactory - Quartz scheduler version: 2.2.3
MySchedulerListener jobAdded
MySchedulerListener jobScheduled trigger
------- 开始执行调度器 Scheduler ----------------
MySchedulerListener schedulerStarting
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
MySchedulerListener schedulerStarted
------- 等待 30 秒... --------------

Job1 - 任务key group1.job1执行时间:2017-11-16 22:14:33
MySchedulerListener triggerFinalized
MySchedulerListener jobDeleted
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down.
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED paused.
MySchedulerListener schedulerInStandbyMode
MySchedulerListener schedulerShuttingdown
MySchedulerListener schedulerShutdown
INFO  QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutdown complete.
------- 关闭调度器 -----------------
~~~~~~~~~~  执行了 1 个 jobs.

示例源码

代码已托管到Github—> https://github.com/yangshangwei/SpringMaster

作者:yangshangwei 发表于2017/11/17 10:34:43 原文链接
阅读:4 评论:0 查看评论

Rosonblatt线性感知器

$
0
0

前叙

读前简介

机器学习的流派很多,现在比较流行的便是联结学派,其计算的重点在于权重更新,而其它学派比如贝叶斯学派,基于统计学,进化学派则注重结构学习.

本篇博客以线性感知器为基础,将会对神经网络与一些机器学习算法进行介绍,如果你只想简单的了解,那么可以浏览一遍即可,当然你也可以花费些时间读这篇文章,那么你也可以受益许多.

神经网络与联结学派

神经网络就是联结学派的”作品”,从最基本的线性感知器到现在的深度学习都隶属于联结学派,其实现人工智能的思想在于利用计算机模拟生物神经网络,而计算过程最后则转化为求最优解问题,计算过程的核心则在于迭代与权值更新,主要陷阱为局部最优解陷阱等等.

而联结学派的核心观点即为:智能的本质是连接机制,神经网络是一个由大量简单的处理单元组成的高度复杂的大规模非线性自适应系统.联结主义模拟人脑智能行为四个层面为: 
a.物理结构; 
b.计算模拟 
c.存储与操作 
d.训练

神经网络图解:

神经网络图解

Rosonblatt感知器图解

Rosonblatt感知器图解

基本计算过程与权值修正

基本计算过程:

  1. 数据输入
  2. 计算诱导局部域,计算公式如下 
    这里写图片描述
    最后求出的超平面: 
    Resenblatt感知器中的超平面
  3. 诱导局部域输入输出函数(硬限幅函数sgn()),并输出最后结果
# 硬限幅函数 python语言
def sgn(v):
    if v> 0:
        return 1
    else:
        return -1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

权值更新(图片来自他处):

感知器收敛算法

代码

# 单样本感知器算法
import numpy as np
b = 1 # 偏置
# 该数据量下 初始权值为 0 时 Rosebblatt感知器 训练失败
a = 0.3 # 学习率
x = np.array([[1,1,3],[1,2,5],[1,1,8],[1,2,15],[1,3,7],[1,4,29]])
d = np.array([1,1,-1,-1,1,-1])
w = np.array([b,0,0])
def sgn(v):
    if v> 0:
        return 1
    else:
        return -1
def comy(myw,myx):
    return  sgn(np.dot(myw.T,myx))
def neww(oldw,myd,myx,a):
    print("comy:",comy(oldw,myx))
    return oldw+a*(myd - comy(oldw,myx))*myx
i = 0
for xn in x:
    print("wn:",xn)
    w =neww(w,d[i],xn,a)
    i+=1
    print("w:",w)
for xn in x:
    print("%d or %d => %d"%(xn[1],xn[2],comy(w,xn)))
test = np.array([b,9,19])
print("%d or %d => %d"%(test[1],test[2],comy(w,test)))
test = np.array([b,9,64])
print("%d or %d => %d"%(test[1],test[2],comy(w,test)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

梯度下降与批量修正算法

公式如下(图片来自书籍):

这里写图片描述

具体算法过程如下: 
1) 初始化权值.学习率,以及期望误差率 
2) 读取所有样本数据 
3) 依次对样本进行训练.更新权值,其更新策略如(8-1)所示 
4) 检查误差率是否小于指定误差率,或者训练次数已达到,否则转到第二步执行

code

import numpy as np
b = 1 # 偏置
# 该数据量下 初始权值为 0 时 Rosebblatt感知器 训练失败
a = 0.5 # 学习率
x = np.array([[1,1,3],[1,2,5],[1,1,8],[1,2,15]])
d = np.array([1,1,-1,-1])
w = np.array([b,0,0])
wucha = 0
ddcount = 50
def sgn(v):
    if v> 0:
        return 1
    else:
        return -1
def comy(myw,myx):
    return  sgn(np.dot(myw.T,myx))
# 权值更新策略
def tiduxz(myw,myx,mya):
    i = 0
    sum_x =np.array([0,0,0])
    for xn in myx:
        if comy(myw,xn)!=d[i]:
            sum_x+=d[i]*xn
        i+=1
    return mya*sum_x
i = 0
while True:
    tdxz = tiduxz(w,x,a)
    print('tdxz:',tdxz)
    w = w+tdxz
    print("w:",w)
    i = i+1
    if abs(tdxz.sum())<=wucha or i >= ddcount:break
test = np.array([1,9,19])
print("%d or %d => %d"%(test[1],test[2],comy(w,test)))
test = np.array([1,3,22])
print("%d or %d => %d"%(test[1],test[2],comy(w,test)))
print()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

Rosonblatt感知器 优化

LMS(最小均方算法:Least-Mean-Square)

均方误差MSE:参数估计中均方误差是指参数估计值与参数真值之差平方的期望值,即样本预测输出值与实际输出值之差平方的期望值.

而MSE的策略则是使MSE最小, 
代价函数: 
LMS的代价函数 
在之前的例子中继续使用批量修正算法,并在其误差信号基础上计算梯度向量: 
在其误差信号基础上计算梯度向量 
权值生成方案: 
权值生成方案

# 代码来自<机器学习实践指南2>
import numpy as np
# LMS算法实现逻辑或运算
b = 1 # 偏置
# 该数据量下 初始权值为 0 时 Rosebblatt感知器 训练失败
a = 0.1 # 学习率
x = np.array([[1,1,1],[1,1,0],[1,0,1],[1,0,0]])
d = np.array([1,1,1,0])
w = np.array([b,0,0])
expect_e=0.005
maxtrycount=20
def sgn(v):
    if v> 0:
        return 1
    else:
        return 0
def get_v(myw,myx):
    return sgn(np.dot(myw.T,myx))
def neww(oldw,myd,myx,a):
    mye = get_e(oldw,myx,myd)
    return (oldw+a*mye*myx,mye)
def get_e(myw,myx,myd):
    return myd-get_v(myw,myx) # 实际输出值 - 样本预测输出值
mycount = 0
while True:
    mye = 0
    i = 0
    for xn in x:
        w,e = neww(w, d[i], xn, a)
        i+=1
        mye+=pow(e,2) # 在 ANN 领域 MSE(均方误差)是指样本预测输出至于实际输出值之差平方的期望
    mye/=float(i)
    mycount+=1
    print(u"第 %d 次调整后的权值:"%mycount)
    print("w:",w)
    print(u"误差: %f "%mye)
    if abs(mye) < expect_e or mycount > maxtrycount:break
for xn in x:
    print("%d or %d => %d"%(xn[1],xn[2],get_v(w,xn)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

LMS退火算法

LMS算法的一个限制在于学习率的设定: 
当学习率较大时,收敛速度较快 
当学习率较小时才能保证权值的修正是再让代价函数一步比一步小 
因而比较科学的方法应该是学习率随着权值的不断修改而不断减小,于是便有了LMS退火算法,其公式如下: 
LMS退火算法中学习率的变化公式

退火算法原理源于固体退火原理,其为基于蒙特卡罗迭代求解法的一种启发式随机搜索过程

修改之前的例子:

a0 = 0.1
a = 0.0
def neww(oldw,myd,myx,a):
    mye = get_e(oldw,myx,myd)
    a=a0/(1+float(mycount)/r)
    return (oldw+a*mye*myx,mye)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Rosonblatt感知机的局限

Rosonblatt感知机的局限在于其本质为一个线性感知机,因此当样本线性不可分时则使用delta法则进行感知机的训练,而delta的关键思想在于使用梯度下降来搜索可能的权向量的假设空间,已找到最佳拟合训练样本的权向量.

通过这种方式可以一定程度上对非线性数据进行分类,但想要解决Rosonblatt感知器无法处理非线性关系(无法学习”异或”逻辑运算)的问题,需要的是双层感知机,或者多层感知机(也就是神经网络),实际上在历史上正是因为Rosonblatt无法学习线性关系才导致了神经网络十几年的低潮,也正是双层感知机找到了异或的学习方法以及传统冯诺依曼计算机模拟智能遇到了不可逾越的瓶颈才再一次掀起了神经网络的热潮.

参考资料

  • [1] 麦好 <机器学习实践指南 案例应用解析 第二版> 机械工业出版社 2017.7
作者:FontThrone 发表于2017/11/17 12:02:18 原文链接
阅读:1 评论: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 原文链接
阅读:39456 评论: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 原文链接
阅读:38801 评论: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 原文链接
阅读:43103 评论: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 原文链接
阅读:37596 评论: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 原文链接
阅读:38421 评论:15 查看评论
Viewing all 35570 articles
Browse latest View live


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