引言
随着HTML5中不断加入图形和多媒体方面的功能,例如canvas2D、WebGL、CSS 3D和视频等,这对渲染引擎使用图形库的性能提出了极大的要求。在webkit渲染基础上,本章着重描述webkit为了引入硬件加速机制而引入了哪些内部结构,以及chromium如何在这些设施上实现特殊的硬件加速机制,这些机制的引入极大地提升了webkit引擎的渲染性能。
硬件加速基础
概念
这里说的硬件加速技术是指利用GPU的硬件能力来帮助渲染网页,因为GPU的主要作用就是用来绘制3D图形并且性能很好。
对GPU而言,计算更新部分更耗费时间。如果没有分层,一旦有请求更新,就会重新渲染所有区域。
每个RenderLayer对象都有一个后端存储对应。这样的好处在于,当每一层更新时,webkit只需要更新这个RenderLayer包含的节点即可。这是理想情况,实际上却不一定如此,主要原因是硬件能力和资源有限。
为了节省GPU的内存,硬件加速机制在RenderLayer树建立后需要做三件事来完成网页的渲染:
-
合成层:webkit决定将哪些RenderLayer对象组合在一起,形成一个新的层,用于之后的合成,这里称之为合成层。
- 每个新层都有一个或多个后端存储。
- 这里的后端存储可能是GPU的内存。
- 对于一个RenderLayer对象,如果没有后端存储的新层,那就使用他的父亲使用的合成层。
-
绘制:将每个合成层包含的RenderLayer内容绘制在后端存储中。
- 这里的绘制可能是软件绘制,也可能是硬件绘制。
-
合成:由合成器将多个合成层合并在一起,形成网页的最终可视化结果,实际就是一张图片。
webkit硬件加速设施
每个合成层都有一个RenderLayerBacking对象,RenderLayerBacking负责管理RenderLayer所有的后端存储。因为RenderLayer可能需要多个存储空间。
在webkit中,存储空间用GraphicsLayer类表示。图8-1描述了这些类以及它们之间的关系。
图中上半部分是webCore部分的基础类:
- RenderLayer和RenderLayerBacking已做过介绍
- GraphicsLayer表示后端存储。
- 每个GraphicsLayer都是用一个GraphicsLayerClient对象。
- 该对象能够收到GraphicsLayer的一些状态更新信息,并且包含一个绘制该GraphicsLayer对象的方法。 - RenderLayerBacking继承于此类。
- GraphicsLayer主要定义一套标准接口,在不同移植中有不同的子类及其实现。如图下半部分是两个不同移植的具体实现类。
哪些RenderLayer对象可以是合成层呢?如果一个RenderLayer对象具有以下特征之一,那它就是合成层。
- RenderLayer具有CSS 3D属性或CSS透视效果。
- RenderLayer包含的RenderObject节点表示的是,使用硬件加速的视频解码技术的HTML5 video元素。
- RenderLayer包含的RenderObject节点表示的是,使用硬件加速的canvas 2D元素或webgl技术。
- RenderLayer使用了CSS透明效果或CSS变换的动画。
- RenderLayer使用了硬件加速的CSS filter技术。
- RenderLayer使用了剪裁(clip)或反射(reflection)属性,并且后代中包含一个合成层。
- RenderLayer有一个Z坐标比自己小的兄弟节点,且该节点是合成层。
这么做的原因有三:
- 合并一些层,可以减少内存的使用量。
- 在合并后,尽量减少合并带来的重绘性能和处理上的困难。
- 对于使用单独层能够显著提升性能的RenderLayer对象,可以继续使用这些好处。如使用webgl技术的canvas元素。
图8-2描述了RenderLayer树、RenderLayerBacking对象和GraphicsLayer树这些硬件加速基础设施的对应关系。每个RenderLayerBacking对象,至少需要一个GraphicsLayer对象。
图8-3描述了一个RenderLayerBacking对象可能包括众多的GraphicsLayer,它们表示不同的含义。
为什么一个RenderLayerBacking对象需要这么多层呢?原因有很多,如:
- webkit需要将滚动条独立开称为一个层。
- 需要两个容器层表示RenderLayer对应的Z坐标为正数的子女和Z坐标为负数的子女。
- 需要滚动的内容建立新层。
- 可能需要剪裁层和反射层。
那么这些层是如何被组织以及他们被绘制的顺序是如何呢?
图8-4的树状结构描述了所有层的绘制顺序,按照先根顺序遍历的结果就是绘制顺序。
- 图中每个层就是一个GraphicsLayer对象。
- 对于RenderLayerBacking对象来说,主层一定存在,其他不一定,因为不一定用得到。
管理这些合成层等工作的是RenderLayerCompositor类,不仅计算和决定哪些RenderLayer对象是合成层,还有合成层创建GraphicsLayer对象,如图8-5.
每个RenderView对象包含一个RenderLayerCompositor,这些对象仅在硬件加速机制下才会被创建。
RenderLayerCompositor类本身类似于一个RenderLayerBacking类,即,它包含一些GraphicsLayer对象,这些对象对应的是整个网页所需要的后端存储。
硬件渲染过程
介绍完硬件加速机制使用的内部设施后,下面详细分析硬件加速机制过程。
渲染的一般过程,在本章最开始介绍过(确定合成层、绘制、合成),这里介绍webkit是如何具体实现这一过程的。
首先看webkit是如何确定并计算合成层的。 图8-6描述了webkit如何决定哪些是合成层并为他们分配后端存储的过程。
图中主要包含两个部分,都是RenderLayerCompositor类的函数:
- 检查RenderLayer对象是否是合成层,如果是,则为它创建RenderLayerBacking对象。
- 根据更新的合成层来更改合成层树,并修改RenderLayerBacking对象的设置信息。
除了上图之外,当RenderLayer对象被创建时,网页还有一些其他情况也可能需要创建RenderLayerBacking对象。具体过程如下:(书中仅简单介绍,知道即可)
-
- 由RenderLayerModelObject::styleDidChanged()函数调用 RenderLayer::styleChanged()函数来触发。
- webkit调用RenderLayerCompositor::updateLayerCompositingState()函数为RenderLayerModelObject对象所在的RenderLayer层创建后端存储对象。
<html>
<style>
div {
-webkit-transform: rotateY (10deg) ;
}
</style>
<body>
<p>test text</p>
<div›css 3d transform</div>
<canvas id="webgl" width="80" height="80"></canvas>
<video width="400" height="300' controls="controls">
‹source sre="test.ogg" type="video/ogg">
</video>
<script type="text/iavascript">
var canvas = document.getElementBvId("webgl");
var gl = canvas.getContext ("experimental-webgl");
gl.clearColor (0.0, 1.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
</script>
</body>
</html>
图8-7主要描述webkit为示例代码8-1建立的合成层以及对应的RenderLayerBacking对象。
webkit为网页中的5个DOM节点创建了RenderLayer对象。分别是HTMLDocument对象、HTMLHtmlDocument对象、HTMLDivDocument对象、HTMLCanvasDocument对象和HTMLVideoDocument对象。其中HTMLHtmlDocument对象对应的RenderLayer对象不满足规则,因此没有自己的RenderLayerBacking对象。
其次,webkit需要遍历和绘制每一个合成层。每个合成层可能有一个或多个RenderLayer对象,可能包含至少四种情形:
- 第一种情形是HTMLDocument节点。
第一种情形
webkit绘制该节点所在合成层,需要遍历两个RenderLayer对象所包含的子树,与其他绘制的内容的调用过程非常相似。该合成层也需要一个用于2D图形的图形上下文对象。该对象的内部实现有各个一直来决定。该层的调用过程如图8-8所示,与软件渲染非常类似,只是递归过程稍微不同。
在软件渲染过程中,paintLayer()函数被递归调用。也就是,从RenderLayer根节点开始,直到所有的RenderLayer节点都被遍历为止。
在硬件加速机制中,情况有所不同,由于引入合成层的概念,每个RenderLayer对象被绘制到祖先链中最近的合成层。
示例代码8-2 WebKit的RenderLayer::paintLayer()函数的条件判断部分的代码,用来检查是否在父节点所在的后端存储中绘制当前节点。
如果它不是合成层,就继续绘制该层;如果是,直接返回。
在之后的逻辑中,webkit会重新为每一个合成层调用绘制操作,每个合成层的图形上下文都不一样。这点不像软件渲染过程。
RenderLayer::paintLayer() {
if (isComposited()) {
if(context->updatingControlTints() || (paintingInfo.paintBehavior & PaintBehaviorFlattenCompositingLayers)) {
paintFlagsl= PaintLayerTemporaryClipRects;
}
else if (!backing()->paintsIntoWindow() &&
!backing()->paintsIntoCompositedAncestor()&&
!shouldDoSoftwarePaint(this,paintFlags & PaintLayerPaintingReflection)
) {
// If this RenderLayer should paint into its backing,thatwill be done via RenderLayerBacking::paintIntoLayer().
return;
}
else if (viewportConstrainedNotCompositedReason() == NotCompositedForBoundsOutofView) {
return;
}
}
}
- 第二种情形是使用CSS 3D变形的合成层。在CSS 3D变形中介绍
- 第三种情形是使用WebGL技术的Canvas元素所在的合成层。在2D图形的硬件加速机制中介绍。
- 第四种情形是类似使用了硬件加速的视频元素所在的合成层。该层内容其实是由视频解码器来绘制,而后通过定时器或其他通知机制告知webkit内容已经发生改变。在多媒体章节中介绍。
最后,渲染引擎将所有绘制玩的合成层合并起来。这个是有webkit的移植来完成的。在后面内容详细介绍。
3D图形上下文
webkit中的3D图形上下文主要是提供一组抽象接口(提供类似OpenGLES的功能),主要目的是使用OpenGL绘制3D图形的能力。在webkit中,3D图形上下文的主要用途是webGL。
图8-9给出了webkit的GraphicsContext3D类,该类是一个抽象类,它包含的接口所处理的对象就是OpenGL中所提供的能力,如,针对纹理、着色器、纹理贴图、顶点等操作。
图中的GraphicsContext3DPrivate是一个跟各个移植相关的类。在各个移植中都使用该名称,但定义不同,主要针对移植的不同来实现。
PlatformGraphicsContext3D类是webCore用于创建surface等对象的参数,因此,名字一致,但每个移植定义实际上不一致。
GraphicsContext3D中的接口有三种类型:
- 所有移植共享实现的接口。如,texlmage2DResourceSafe;
- 一些移植能够共享实现的接口。如texlmage2D;
- 跟每个移植具体相关的接口。如,PlatformGraphicsContext3D。
这些跟移植相关的类是需要每个移植去实现的,否则这一机制不能工作。下面就是介绍chromium移植如何实现这些部分并包含哪些不同之处。
chromium的硬件加速机制
GraphicsLayer的支持
GraphicsLayer对象是对渲染后端存储中某一层的抽象,需要具体的实现类来支持该类所要提供的功能。为了完成这一功能,chromium提供了更为复杂的设施类。本节主要介绍从GraphicsLayer类到合成器这一过程中所涉及的众多内部结构。
图8-10描述了从webCore的同移植无关的GraphicsLayer,到webkit的chromium移植,再到chromium浏览器所设计的chromium合成器的layerImpl类这一过程。
从上到下介绍这些类:
- GraphicsLayerChromium:GraphicsLayer的子类,实现了GraphicsLayer的功能,并增加了chromium需要的信息。
- WebLayer:webkit的chromium移植的抽象接口类,被GraphicsLayerChromium等调用。主要目的是把chromium的实际后端存储类抽象出来,以便webCore使用。
- WebLayerImpl:WebLayer的实现类,具体作用是把合成器的层能力暴露出来,与Layer类一一对应。
- Layer:合成器的层表示类,是chromium合成器的接口类,用于表示合成器的合成层,形成一颗合成树。
- LayerImpl:与Layer对象一一对应,是实际的实现类,包含后端存储。可能跟Layer树不在同一线程。
由上面的介绍可以看出,这个过程基本就是各种类的映射,从GraphicsLayer类到LayerImpl类,目的是将webkit的合成层映射到chromium合成器中的合成层,合成器最终合成这些层。
(在新的blink中,图中webkit的chromium移植部分被移除。原因blink不需要多层次的接口。)
框架
在chromium中,所有使用GPU硬件加速的操作都是由GPU进程负责完成的,包括使用GPU硬件进行绘制和合成。
chromium是多进程架构,每个网页的Renderer进程都是将3D绘图和合成操作,通过IPC传递给GPU进程,由它统一调度执行。
图8-11描述chromium多进程架构中,GPU进程和其他进程之间的联系。
实际上,每个Rrenderer进程都依赖GPU进程来渲染网页。Browser进程也会跟GPU进程进行通信,作用是创建该进程,并提供渲染(过程最后绘制)的目标存储。
(GPU进程也被使用在其他用途中,如后面介绍的pepper插件,因为chromium提供了绘制3D图形能力的接口给pepper插件。)
在介绍完GPU进程后,下面主要介绍webkit渲染引擎是如何使用GPU进程来渲染网页的。图8-12描述了具体的调用栈。
webkit定义了两种类型的图形上下文,都可以使用GPU加速。3D和2D图形上下文在chromium中,分别对应3D图形上下文实现和Skia画布。
它们在调用GL操作后,被转换成IPC消息传递给GPU进程。该进程的解释器对消息进行解释后,调用GL函数指针表中的函数(函数指针是从GL库中获取的)。
对于Windows,3D图形库是D3D而不是OpenGL接口,chromium的做法是通过开源项目ANGLE,将D3D封装成OpenGL方式的接口。这样,chromium统一了3D图形上下文的接口调用。
下面以chromium的3D图形上下文为例,详细说明它的调用经过哪些chromium具体类,最后调用操作系统的3D图形库。图8-13描述了中间使用到的各种主要类。
同前面的调用栈一样,图中也是分为Renderer进程和GPU进程。首先来了解Renderer进程的主要类。
- WebGraphicsContext3DCommandBufferlmpl:
- 主要转接自webkit的调用到chromium的具体实现。同时将3D图形操作转换成GL命令。包括一个RendererGLContext对象。
- RendererGLContext:
- Renderer进程对GLContext的封装,包括所有跟GPU交互的类,有一个GLES2Implementation对象、一个CommandBufferProxy对象和一个GPUChannelHost对象。
- GLES2Implementation:
- 该类是模拟OpenGL ES2的编程接口。将调用转换成特定格式的命令存入CommandBuffer中。
- CommandBufferHelper:
- 辅助类。包括一个CommandBuffer代理类和一个共享内存。
- CommandBufferProxy:
- CommandBuffer的代理类,实现CommandBuffer的接口,用于和CommandBufferStub之间的通信。
- GPUChannelHost:
- 用于传递GL命令的IPC消息辅助类。
下面介绍GPU进程的主要类:
- GPUChannel:
- 辅助类,用于接收GL命令并发送回复。
- GPUCommandBufferStub:
- CommandBuffer的桩,接收来自CommandBufferProxy的消息,将请求交给CommandBufferService处理。
- CommandBufferService:
- CommandBuffer的具体处理类。
- 不解析处理命令。当有新的命令到达时,调用注册的回调函数处理。
- GPUScheduler:
- 负责调度执行CommandBuffer的命令,它会检查CommandBuffer是否应该被执行,并适时将命令交给CommandParser处理。
- CommandParser:
- 仅检查CommandBuffer中的命令头部,其余部分交给具体的命令解释器处理。
- 同GL命令的理解是独立的。
- GLES2Decoderlmpl:
- 针对GLES命令的命令解释器。
- 会解析每条命令并执行调用相应的GL函数。
- GL Implementation Wrapper:
- 一组GL相关的函数指针。通过设定的3D图形库来读取库中相应函数的地址。
- GL Libraries:
- 具体的函数库。在chromium中,可以设置为OpenGL、OpenGL ES、 Mesa GL、Mock、ANGLE等。
通过以上介绍,我们大概了解chromium跨进程的硬件加速机制的工作过程。那么,GPU进程和Renderer进程是如何同步这些命令的呢?
GPU处理一些命令后,会向Renderer进程报告自己当前的状态,而Renderer进程检查状态信息和期望结果来确认是否满足自己的条件。
GPU进程将页面内容直接绘制在浏览器的标签窗口内;软件渲染则是通过共享内存传递给Browser进程。
命令缓冲区
命令缓冲区(CommandBuffer)是用于GPU进程和GPU的调用进程传递GL操作命令。
现有的实现是基于共享内存完成的。命令基于GLES编码成特定格式存储在共享内存中。共享内存采用欢迎缓冲区的方式管理,内存可以循环使用。
一条命令可以被分为两个部分:
- 命令头:是命令的原始数据信息。包括两部分:
-
- 命令的长度。
- 命令的标识。
- 命令体:包含命令所需的其他信息,如命令的立即操作数。
命令的长度可以固定,也可以变化。具体结构如图8-14。
命令缓冲区本身没有定义具体的命令格式,故GLES实现可以根据自己的需要定义。GLES实现使用的命令一般分为两类:
- 基本命令:主要用来操作桶,跳转、调用和返回等指令。
- GLES2函数相关的命令:主要用于操作GLES2的函数。
命令本身保存在共享内存中,且每条命令长度不能超过221-1。
对于需要传输较大数据的命令,chromium使用独立的共享内存来实现,如TextImage2D(传递大量数据到GPU内存)。
另外,chromium还提供一种新的机制,桶(Bucket)机制来解决。原理是:通过共享内存来分块传输,把分块的数据保存在本地的桶内,从而避免申请大块的共享内存。当数据传输完成后,对数据进行操作的命令就可以执行了。
(桶机制还可以用来传输字符串类型的边长数据:接收端首先获取桶内字符串的长度,通过共享内存分块传输,最后合并在接收端的桶内。)
chromium合成器
架构
合成层的作用就是讲多个合成层合成并输出一个最终的结果。
-
- 输入:多个合成层。
- 输出:一个后端存储。
chromium合成器是一个独立且复杂的模块,既可以合成网页,也可以合成用户界面,或多个标签页。
在架构设计上,合成器采用的是表示和实现分离的原则,即合成器layer层和具体合成器所要合成的操作分离的原则。图8-18描述了这一思想。
webkit对合成层的各种设置,最终都是用Layer树表示,每个Layer节点包含3D变形、剪裁等属性,但chromium将这些属性应用到后端存储并合成,这一过程并不是在Layer树进行,而是将这些功能委托给LayerImpl树完成。两者之间通过代理同步。代理的作用是协调和同步两者之间的操作。
Layer树所有的信息都会拷贝到LayImpl树中。
-
- Layer树工作在主线程。这里的“主线程”指的是渲染引擎工作的线程,不一定是Renderer进程的主线程。
- LayerImpl树在“实现部分”的线程。
-
-
- 该线程可以是主线程,
-
-
-
-
- 线程内合成
-
-
-
-
- 也可以是一个单独的线程。
-
-
-
-
- 线程化合成
-
-
基础设施
为了支持chromium合成器的线程化合成和线程内合成等机制,chromium引入了一些类来支持,下面结合合成器的架构来分析它们。
首先来看合成器的主要组成,大致分为以下几个部分:
- 事件处理部分。
-
- 主要是webkit或其他的用户事件(网页滚动、放大缩小等),这些事件会请求合成器重新绘制每一个合成层,再合成这些层的绘制结果。
- 合成器的表示和实现。
-
- 主要定义各种类型的合成层,包括它们的位置、滚动位置、颜色等属性。
- 合成器组成两种类型的树,以及他们之间的同步机制。
- 合成调度器。
-
- 主要调度来自用户的请求,包括一个状态,用于调度当前队列中需要执行的请求。目的是协调合成层的绘制和合成、树的同步等操作。
- 合成器输出结果。
-
- 合成器合成结果可以是一个GPU Surface,也可以是一个CPU的存储空间
- 同时,也包括GL操作类,可以让合成器使用GL来合成这些合成层。
- 各种后端存储等资源。
-
- 合成器需要能够创建各种类型的GL缓冲区、纹理等,以为每个合成层都需要这些资源。
- 支持动画和3D变形这些功能所需要的基础设施。
首先看两种树,以及他们之间如何同步。图8-16描述了它们主要使用的类。
先看Layer所在的线程。每一层都是一个Layer对象,而Layer树由LayerTreeHost类维护。
LayerTreeHost的作用:一是根据调用者需求创建和更新Layer树;二是将这些变动通过代理拷贝给实际的实现者LayerTreeImpl。(这可能需要跨线程,拷贝的作用就是使合成器能够独立工作,不依赖于webkit渲染所在的线程)
代理也很重要。代理是一个抽象类,定义了Layer树和LayerImpl树之间完成合成所需要的转接工作。它有两个子类,分别是SingnleThreadProxy类和ThreadProxy类,分别用于线程内合成和线程化合成两种情况。
以ThreadProxy为例(因为它更复杂),代理的一些接口由主线程调用(也就是由LayerTreeHost调用),用来复制信息到实现类LayerImpl。另外就是使用调度器来调度合成的过程。
再看实现部分。实现的主要逻辑由LayerTreeHostImpl来负责,如调度、复制信息到LayerImpl树等,它至少包含一个LayerImpl树对象。(在线程化的绘图模式中,可能至少包含三个树对象)。
而LayerTreeImpl树只维护一个LayerImpl树,包括为树中的层创建后端存储、为整个树创建输出结果、合成该树各个节点的实际过程等。
类Layer和LayerImpl是两种基类,各自都有多个子类,它们和各自的子类是一一对应的。这里以Layer类和它的子类为例说明合成器中的合成层。图8-17描述了Layer类和它的子类。
每个类都有各自的应用场景。如VideoLayer类是表示视频播放的,SolidColorLayer类表示单一颜色的背景层,而TextureLayer类表示该合成层直接接收一个纹理,该纹理已经有其他部分处理,不需要合成器触发任何绘图操作(在chromium中,一些插件是能够使用硬件绘图并输出纹理结构的)。
图8-17中有两个类被标记为灰色:一是Layer类,是所有类的基类;另一个是TiledLayer,是一个中间类,被ContentLayer类和ImageLayer类继承。它的含义是,一个层的后端存储被分割成瓦片状,由多个小后端存储共同存储而成。
图8-18描述了一个合成层的后端存储被分割成多个大小相同的瓦片状的小存储空间。每个瓦片可以理解一个OpenGL的一个纹理对象,合成结果被分开存储在这些瓦片中。
什么样的合成层会被瓦片化呢?TiledLayer的子类告诉了我们,其一是ContentLayer,它表示合成层使用skia画布将内容绘制到结果中,对应到网页中就是html元素,如DOM树中的html、div等所在的层,都是使用skia图形库的skCanvas来绘图;其二是图片元素,如果合成层中只包含一个图片,那么该图片就会使用该技术。
为什么是用瓦片化的后端存储呢?概括来讲有以下几点:
- DOM树中的html元素所在的层可能会非常大,因为网页的高度很大,如果只用一个后端存储的话,那么需要一个很大的纹理对象,但实际上GPU硬件支持的纹理对象大小有限。
- 有些合成层比较大,当其中一部分发生改变,重新绘制整个纹理对象,这样必然产生额外的开销。使用瓦片化的后端存储,可以只绘制存在更新的瓦片
- 当层发生滚动时,一些瓦片可能不再需要,webkit又需要一些新的瓦片来绘制新的区域,这些大小相同的后端存储很容易做到重复利用。
在线程内合成模式下,chromium是不需要调度器的,仅在线程化合成下才需要用到。所以调度器是在合成线程中,因此不能访问主线程资源。
调度器需要考虑整个合成器系统的状态,需要考虑何时更新树、何时绘图、何时运行动画、何时上传内容到纹理对象等。
合成器中的调度器和状态机如图8-19所示。
- Scheduler类就是调度器类,任何合成相关的操作都要经过该调度器。
-
- 例如,ThreadProxy类会调用SetNeedsCommit函数来触发Commit操作,该操作含义是将Layer树的属性等改变同步到LayerImpl树。
- 任务的发起者只是告诉调度器希望执行该任务。Scheduler本身不直接处理这些状态设置,而是将它转给SchedulerStateMachine类处理,由状态机设置相应的状态位。一个任务一般不会立即执行,而是等到调度器调度到该任务时才会执行。
当调用Scheduler类的ProcessScheduledActions时,调度器会通过状态机来获取当前需要执行的任务,状态机根据之前设置的各种信息来决定下面的任务是什么。一旦确定了任务,调度器会通过SchedulerClient来执行实际的任务(ThreadProxy就是SchedulerClient的一个子类,可以桥接到Layer树、LayerImpl树或其他设施。 )。
调度器Scheduler的基本原则是一切请求都是设置状态机中的状态。调度任务的主要函数是ProcessScheduledActions,它的工作方式如图8-20所示。
首先调用状态机的NextAction函数,由状态机计算和决定下一个要执行的任务。状态计算出下一个任务,调度器获得任务类型并执行任务,然后再计算下一个要执行的任务,如此循环,直到空闲为止。
下面以同步Layer树到LayerImpl树(commit)为例说明任务的调度过程以及调度器在这一过程中起到的作用。图8-21描述了commit任务的调度过程。
首先,当Layer树有变动时,它需要调用ThreadProxy的SetNeedsCommit函数,这些任务是在渲染线程中的,随后会提交一个请求到合成线程。
其次,当合成线程处理到该请求时,会通过调度器的SetNeedsCommit函数设置状态机的状态。
再次,调度器的SetNeedsCommit会调用ProcessScheduledActions函数,检查后面需要执行的任务。
然后,如果没有其他任务或时间合适的话,状态机决定立刻执行该任务,它会调用ThreadProxy的ScheduledActionCommit函数,该函数实际执行commit任务需要的具体流程。
合成过程
合成工作主要有四个步骤,这些步骤都由调度器调度,需要个各类参与共同完成。
- 步骤一:创建输出结果的目标对象Surface,即合成结果的存储空间。
- 步骤二:开始一个新的帧,包括计算滚动和缩放大小、动画计算、重新计算网页布局、绘制每个合成层等。
- 步骤三:将Layer树中包含的这些变动同步到LayerImpl树中,即前面提到的commit任务的调度过程。
- 步骤四:合成Layer树中的各个层并交换前后帧缓冲区,完成一帧的绘制和显示。
图8-22是合成器工作的典型过程。结合8-22下面依次分析这四个步骤。
在步骤一中,合成线程搜狐县创建合成器需要的(输出结果的)后端存储。在调度器执行该任务时,该线程会把任务交给主线程完成。主线程会创建后端存储并把它传回给合成线程。
在步骤二中,合成线程告诉主线程,需要开始绘制新的一帧(同样是通过线程间通信来传递任务)。
主线程接收到任务后,需要做的事情非常多,主要是执行动画操作、重新计算布局,以及绘制需要更新的合成层等(参考图中4.1-4.6,实际还省略了一些次要操作)。在这之后,主线程会等待步骤三,当步骤三完成后会通知主线程的LayerHost等类。这是一位步骤三需要同步Layer树,需要阻塞主线程。
在步骤四中,主要就是合成工作了。
经过步骤三后,合成线程有了合成这些层需要的一切资源,不需要主线程的参与就能完成合成工作。图中过程5.1.1-5.1.6这些步骤就是合成各个层并交换前后帧缓冲区。
这样就能解释:渲染线程在做其他事时,网页滚动等操作并不会受到渲染线程的影响。因为这时候合成器的工作线程仍然能够正常进行,合成器线程继续合成当前的各个合成层,生成网页结果。虽然此时可能有些内容没有更新,但用户根本感觉不到网页被阻塞,浏览网页的用户体验更好。
chromium的最新设计为了合成网页(可以包含iframe等内嵌网页)和浏览器用户界面(典型的是在安卓系统上,桌面系统通常不需要),可能需要多个合成器。
每个网页可能需要一个合成器,网页内的iframe也需要一个合成器,整个网页同用户界面的结合也需要一个合成器,这些合成器构成一个层次画的合成器结构,如图8-23所示。
这里,合成器2和合成器3按理说是在Renderer进程中进行(因为是网页相关的合成),而根合成器是在Browser进程中的,这样做会增加内存带宽的使用。目前,chromium的设计使用mailbox机制将Renderer进程中的合成器结果同步到Browser进程,根合成器可以使用这些结果。
实践:减少重绘
网页加载后,当重新绘制新的一帧时,一般需要三个阶段,即计算布局、绘图、合成。
如果想减少每一帧的时间,提高性能,就要从这三个阶段下手。
其中,计算布局和绘图比较费时间,合成需要的时间相对少一些。而且,当布局的变化越多时,绘图需要的时间越多。
例如,当使用js的计时器来控制动画时,webkit可能需要修改布局和比较多的绘图操作,这会明显增加webkit绘制每帧的时间,有什么办法来避免这一情况呢?办法很多,这里介绍webkit两种典型的办法:一是使用合适的网页分层技术以减少重新计算的布局和绘图;二是使用CSS 3D变形和动画技术。
首先是网页分层问题。假设设计一款闯关游戏,游戏画面中有背景、各种障碍物、人物,使用Canvas 2D来实现。假如只用一个canvas元素实现,当人物运动时,webkit需要将元素内所有内容绘制一遍,包括背景、障碍物、人物等,导致开销太大。
比较好的做法是,使用多个canvas元素,按照前后顺序叠放,前面的元素背景透明。以前面的游戏为例,我们可以使用第一个canvas绘制背景,第二个canvas绘制障碍物,第三个canvas绘制金币等奖励,第四个canvas绘制人物。人物不动时,如果障碍物和金币在变化,只需绘制第二三个元素;如果人物走动,只需绘制第四个元素。第一个元素很少会被重新绘制,这样做有效减小开销。图8-24描述了这一概念。
这是一个基本思路,我们可以将这一思想应用在很多场景。详细情况可以参考 层次化canvas渲染优化技术:developer.ibm.com/tutorials/w…。
其次是使用CSS 3D变形技术,能够让浏览器仅使用合成器合成所有层就能实现动画效果,而不是通过重新设置其他的css属性来触发计算布局、重新绘制、重新合成所有层这一过程。采用这一技术,webkit不需要大量布局计算和重新绘制元素,只需要在合成时更改合成层的3D变形属性,可以极大节省时间。