阅读 451

【BeesAndroid系列】聊一聊Chromium的渲染机制

聊一聊Chromium的渲染机制

关于BeesAndroid项目

BeesAndroid项目提供了一系列的工具、理论分析与方法论,旨在降低Android系统源码的阅读门槛,让读者更好的理解Android系统的设计与实现。第一次阅览本系列文章,请参见导读,更多文章请参见文章目录


今天我们来聊一聊Chromium的渲染机制,这也是渲染机制系列的第二篇,最近大半年的工作都和H5容器有关,因而花了点时间学习了下Chromium项目,这里着重去分析一下它的渲染机制。

从开发者的角度,当我们去看一个H5容器的时候,和它一起工作的有以下角色:

  • 软件
    • Chromium:WebView、Content、Blink、V8、Net、Base等
    • Android OS:View/Window、Activity、WindowManager、ActivityManager、Surface/Texture、SurfaceFlinger。
    • Graphics:OpenGL ES、Skia、Vulkan
    • Binders
    • Linux Kernel
  • 硬件
    • 显示屏
    • CPU
    • GPU


如下所示:

the_life_of_pixel_webview_01.svg


当然实际的构造复杂的多,用户在点击屏幕打开H5页面时,一般会经历以下几个阶段。

  1. 触摸反馈:首先是触摸,这个触摸事件是怎么传递到Android里面的App的。
  2. 容器创建:当触摸事件传递到App中,Android是怎么启动WebView容器来加载URL的。这里会涉及Chromium内核的启动等相关知识。
  3. 页面加载:WebView启动以后,是怎样向服务端发送主文档请求的,又是怎么接收主文档响应的。
  4. 页面渲染:WebView接收到主文档之后,是怎么样将它解析成页面的,这个是最为关键也是最为复杂的一环。

可以看到,页面在渲染之前还有需要工作需要处理,容器的启动也是个耗时的操作,为什么会特地聊聊容器启动呢,因为这个也是H5页面体验的重要组成部分,因为是Native的关系,前端同学可能会关注不到。而且容器导航阶段是重要的预加载时机,我们可以在这里做很多事情,例如:

  1. 接口预加载
  2. HTML文档预加载
  3. 资源预加载
  4. 导航的时候创建一个JS Engine,可以提前执行JS逻辑,把导航预加载这个能力开放给前端


言归正传,我们接着来聊聊渲染机制。

Rendering Architecture


浏览器的渲染过程就是把网页通过渲染管道渲染成一个个像素点,最终输出到屏幕上。这里面就涉及3个角色

  • 输入端:网页,Chromium将其抽象成Content。
  • 渲染管线:主要是Blink负责DOM解析、样式布局、绘制等操作,将网页内容转换为绘制指令。
  • 输出端:主要负责把绘制指令转换为像素,显示在屏幕上。


什么是输入端(Content)?

我们在Chromium这个项目里会频繁的看到Content这个概念,那么Content到底是什么呢。Content是渲染网页内容的区域,在Java层对应AwContent,底层有WebContents表示,如下所示:

image.png

content在代码由content::WebContents来描述,它在独立的Render进程由Blink创建。具体说来Content对应着前端开发中涉及的HTML、CSS、JS、image等,如下所示:

image.png


什么是渲染管线(Rendering Pipeline)?

渲染管线可以理解为对渲染流程的拆解,向工厂流水线一样,上一个车间生成的半成品送到下一个车间继续装配。拆解渲染流程有助于把渲染流程简单化,提高渲染效率。

渲染时动态的,内容发生变化时,就会触发渲染,更新像素点,和Android的绘制系统一样,触发绘制也是由invalidate机制触发的,触发渲染后,执行整个渲染管线是非常昂贵的,因而Blink也在想法设法减少不必要的渲染动作,提高渲染效率。

  • 触发的条件如下所示:
    • scrolling
    • zooming
    • animations
    • incremental loading
    • javascript
  • 各个流程的触发方法如下:
    • Style:Node::SetNeedsStyleRecalc()
    • Layout:LayoutObject::SetNeedsLayout()
    • Paint:PaintInvalidator::InvalidatePaint()
    • RasterInvalidator::Generate()


渲染管道把网页转换为绘制指令后,它并不能直接把绘制指令变成像素点(光栅化)显示在屏幕上(Window),这个时候就需要借助操作系统自己的能力(底层的图形库),在图形界面这一块大部分平台都遵循OpenGL标准化的API。例如Windows上的DirectX,Android上的Vulcan。如下图所示:

image.png


通过上面的描述,我们了解了Conntent从哪里来,要到哪里去。总的来说就是把HTML、CSS、JS等转换为正确的OpenGL指令,然后渲染到屏幕上,与用户交互。

在了解了渲染的基本要素以后,我们来看看具体的渲染流程是怎样执行的,如下所示:

image.png

Structure

**
我们先来说结构

从上到下,分层来说:

  • Blink:运行在Render进程的Render线程,它是Chromium的Blink渲染引擎,主要负责HTML/CSS的解析、jS的解释执行(V8)、DOM操作、排版、图层树的构建更新等任务。
  • Layer Compositor:运行在Render进程的Compositor线程,它负责接收Blink生成的Main Frame,负责图层树的管理、图层的滚动、旋转等矩阵变化,图层的分块、光栅化、纹理上传等任务。
  • Display Compositor:运行在Browser进程的UI线程,它负责接收Layer Compositor生成的Compositor Frame,输出最终的OpenGL绘制指令,将网页内容通过GL贴图操作绘制到目标窗口上。


这里面还提到了每个层级向上输出的产物帧,帧(Frame)描述了渲染流水线下级模块向上级模块输出的绘制内容相关数据的封装。

  • Main Frame:包含了对网页内容的描述,主要以绘图指令的形式,或者理解为某个时间点对整个网页的一个矢量图快照。
  • Compositor Frame:Layer Compositor接收Blink生成的Main Frame,并转换成内部的合成器结构。它会被发往Browser,并最终到达Compositor Frame,它主要由两部分构成:
    • Resource:它是对Texture的封装,Layer Compositor为每个图层分块,然后为每个分块分配Resource,然后安排光栅化任务。
    • Draw Quad:它代表了绘制指令(矩形绘制指令,指定了坐标、大小、变换矩阵等属性),Layer Compositor接收到Browser的绘制请求时,它会为当前可见区域每个图层的每个分块生成一个Draw Quad绘制指令。
  • GL Frame:Display Compositor将Compositor Frame的每个Draw Quad绘制指令转换成一个GL多边形绘制指令,使用对应的Resource封装的Texture对目标窗口进行贴图。这个GL绘图指令的集合就构成了一个GL Frame,最终由GPU执行这些GL指令完成网页在窗口可见区域的绘制。


整个渲染水流水线的调度基于请求和状态机响应,调度的中枢运行在Browser UI线程,它按照显示器的VSync信号向Layer Compositor发出输出下一帧的请求,而Layer Compositor根据自身的状态机的状态决定是否需要Blink输出下一帧。而Layer Compositor和Display Compositor是生成者和消费者的关系,Display Compositor持有一个Compositor Frame队列不断的进行取出和绘制,输出的频率取决于 Compositor Frame的输入帧率和自身GL Frame的绘制频率。

Flow


我们再来说流程

  1. Parse/DOM:将Content解析成DOM树,它是后面各个渲染流程的基础。
  2. Style:解析并应用样式表。
  3. Layout:布局。
  4. Compositing update:将整个页面按照一定规则,分成独立的图层,便于隔离更新。
  5. prepaint:构建属性树,使得可以单独操作某个节点(变换、裁剪、特效、滚动),不至于影响它的子节点。
  6. paint:paint这个单词名词有油漆、颜料的含义。动词有用颜料画等含义。这里我觉得使用它的名词含义比较贴切,Paint操作会将布局树(Layout Tree)中的节点(Layout Object)转换成绘制指令(例如绘制矩形、绘制字体、绘制颜色,这有点像绘制API的调用)的过程。然后把这些操作封装在Dsipaly Item中,所以这些Display Item就像是油漆,它还没有真正的开始粉刷(绘制Draw)。
  7. Commit:commit会把paint阶段的数据拷贝的合成器线程。
  8. Tiling:raster接收到paint阶段的绘制指令之后,会先对图层进行分块。图块是栅格化(Raster)的基本工作单位。
  9. Raster:栅格化。
  10. Activate:栅格化是个异步的过程,因而图层树(Layer Tree)被分为了Pending Tree(负责接收Commit提交的Layer进行栅格化操作)和Activate Tree(从这里取出栅格化的Layer进行Draw操作),从Pending Tree拷贝Layer到Activate Tree的过程就叫做Activate。
  11. Draw:这里要和上面的Paint区分开来了,图块被栅格化以后,合成器线程会为每个图块生成draw quads(quads有四边形之意,它代表了在屏幕特定位置绘制图块的指令,包含属性树里面的变换、特效等信息),这些draw quads被封装到Compositor Frame中输出给GPU,Draw操作就是生成draw quads的过程。
  12. Display:生成了Compositor Frame以后,Viz会调用GL指令把draw quads最终输出到屏幕上。


我们来分别看具体的流程。

Rendering Pipeline

注:Rendering Pipeline里的图片来自于Chromium工程师的ppt Life of a Pixel的截图。

Blink

01 Parse


相关文档


相关源码


当我们从服务器上下载了一份HTML文档,第一步就是解析,HTML解析器接收标签和文本流(HTML是纯文本格式)把HTML文档解析成DOM树。DOM(Document Object Model)即文档对象模型,DOM及时页面的内部表示,也为JavaScript暴露了API接口(V8 DOM API),可以让JavaScript程序改变文档的结构、样式和内容。

它是一个树状结构,我们在后续的渲染流程中还会看到很多树形结构(例如布局树、属性树等)因为它们都是基于DOM树的结构(HTML的结构)而来的。

image.png

注:HTML文档中可能包含多棵DOM树,因为HTML支持自定义元素,这种树通常被称为Shadow Tree。


解析HTML生成DOM树流程如下:

  1. HTMLDocumentParser负责解析HTML中的token,生成对象模型。
  2. HTMLTreeBuilder负责生成一棵完整的DOM树,同一个HTML文档可以包含多个DOM树,Custom Element元素具有一棵shadow tree。在shadow tree slot中传入的节点会被FlatTreeTraversal向下遍历时找到。


DOM树(DOM Tree)作为后续绘制流程的基础, 还会基于它生产各种类型的树,具体说来,主要会经历如下转换:

对象转换

  • DOM Tree -> Render Tree -> Layer Tree
  • DOM node -> RenderObject -> RenderLayer

DOM Tree(节点是DOM node)

当加载一个HTML时,会对他进行解析,生成一棵DOM树。DOM树上的每一个节点都对应这网页里面的每一个元素,网页可以通过JavaScript操作这棵DOM树。

image.png

How Webkit Works

Render Tree(节点是RenderObject)

但是DOM树本身并不能直接用于排版和渲染,因此内核会生成Render Tree,它是DOM Tree和CSS相结合的产物,两者的节点几乎是一一对应的。Render Tree是排版引擎和渲染引擎之间的桥梁。

image.png

How Webkit Works

Layer Tree(节点是RenderLayer)

渲染引擎并不是直接使用Render Tree进行绘制的,为了更加方便的处理定位、裁剪、业内滚动等操作,渲染引擎会生成一棵Layer Tree。渲染引擎会为一些特定的RenderObject生成相应的RenderLayer,不过该RenderObject的子节点没有相应的RenderLayer,那么它就从属于父节点的RenderLayer。渲染引擎会遍历每一个RenderLayer,再遍历从属于这个RenderLayer的RenderObject,将每一个RenderObject绘制出来。

可以这么理解,Layer Tree决定了网页的绘制顺序,从属于RenderLayer的RenderObject决定了这个Layer的绘制内容。

什么样的RenderObject会成为RenderLayer呢。GPU Accelerated Compositing in Chrome是这样定义的:

  • It's the root object for the page
  • It has explicit CSS position properties (relative, absolute or a transform)
  • It is transparent
  • Has overflow, an alpha mask or reflection
  • Has a CSS filter
  • Corresponds to element that has a 3D (WebGL) context or an accelerated 2D context
  • Corresponds to a


对上面的流程不了解也没关系,我们下面会一一解释。

02 Style


当DOM树生成以后,就需要为每个元素设置一个样式,有的样式只是会影响某个节点,有的样式会影响整个节点下面的整个DOM子树的渲染(例如,节点的旋转变换)。

image.png


相关文档


相关源码


样式一般都是样式渲染器共同作用的结果,它有复杂的优先级语义和渲染过程,过程整体分为三步:

1 收集、划分和索引所有样式表中样式规则。

image.png


CSSParser首先CSS文件解析成对象模型StyleSheetContents,它里面包含各种样式规则(StyleRule),这些样式规则具有丰富的表现形式。包含选择器(CSSSelector)和属性值映射(CSSPropertyValue)在这些样式规则中,对象以各种方式建立索引,进行更有效的查找。

另外,样式属性以声明的方式进行定义,定义在Chromium里的css_properties,json5这个json文件里,这些定义会通过py脚本生成特定的C++类。

2 访问每个DOM元素并找到应用在该元素的所有规则。

样式引擎会遍历整个DOM树,计算每个节点的样式,计算样式(ComputeStyle)会完成property到rule的映射,例如字体样式、边距、背景色等。这些就是样式引擎的输出。

image.png


3 结合这些规则以及其他信息(样式引擎由部分默认的样式)生成最终的计算样式。

03 Layout


计算并应用了每个DOM节点的样式以后,就需要决定每个DOM节点的摆放位置。DOM节点都是基于盒模型摆放(一个矩形),布局就是计算这些盒子的坐标。

image.png


布局操作是建立在CSS盒模型基础之上的,如下所示:

|-------------------------------------------------|
    |                                                 |
    |                  margin-top                     |
    |                                                 |
    |    |---------------------------------------|    |
    |    |                                       |    |
    |    |             border-top                |    |
    |    |                                       |    |
    |    |    |--------------------------|--|    |    |
    |    |    |                          |  |    |    |
    |    |    |       padding-top        |##|    |    |
    |    |    |                          |##|    |    |
    |    |    |    |----------------|    |##|    |    |
    |    |    |    |                |    |  |    |    |
    | ML | BL | PL |  content box   | PR |SW| BR | MR |
    |    |    |    |                |    |  |    |    |
    |    |    |    |----------------|    |  |    |    |
    |    |    |                          |  |    |    |
    |    |    |      padding-bottom      |  |    |    |
    |    |    |                          |  |    |    |
    |    |    |--------------------------|--|    |    |
    |    |    |     scrollbar height ####|SC|    |    |
    |    |    |-----------------------------|    |    |
    |    |                                       |    |
    |    |           border-bottom               |    |
    |    |                                       |    |
    |    |---------------------------------------|    |
    |                                                 |
    |                margin-bottom                    |
    |                                                 |
    |-------------------------------------------------|
复制代码


相关文档


相关源码


基于DOM Tree会生成Layout Tee,生成每个节点的布局信息。布局的过程就是遍历整个Layout Tree进行布局操作。

DOM Tree和Layout Tree也不总是一一对应的,如果我们再标签里设置dispaly:none,它就不会创建一个布局对象(LayoutObject)。

image.png

04 Compositing Update


在Layout操作完成以后,理论上就可以开始Paint操作了,但是我们之前提过,如果直接开始Paint操作,绘制整个界面,代价是非常昂贵的。因此便引入了一个图层合成加速的概念。

什么是图层合成加速(Compositing Layer)?

图层合成加速基本思想是把整个页面按照一定规则分成多个图层(就像Photoshop的图层那样),在渲染时只需要操作必要的图层,其他图层只需要参与合成就行了,以此提高渲染效率。完成这个工作的线程叫Compositor Thread,值得一提的是Compositor Thread还具备处理输入事件的能力(例如滚动事件),但是如果在JavaScript注册了事件监听,它会把输入事件转发给主线程处理。

image.png

具体说来是为某些RenderLayer拥有自己独立的缓存,它们被称为合成图层(Compositing Layer),内核会被这些RenderLayer创建对应的GraphicsLayer。

  • 拥有自己的GraphicsLayer的RenderLayer在绘制的时候就会绘制在自己的缓存里面。
  • 没有自己的GraphicsLayer的RenderLayer会向上查找父节点的GraphicsLayer,直到RootRenderLayer(它总是会有自己的GraphicsLayer)为止,然后绘制在有GraphicsLayer的父节点的缓存里。

image.png

这样就形成了与RenderLayer Tree对应的GraphicsLayer Tree。当Layer的内容发生变化时,只需要更新所属的GraphicsLayer即可,而单一缓存架构下,就会更新整个图层,会比较耗时。这样就提高了渲染的效率。但是过多的GraphicsLayer也会带来内存的消耗,虽然减少了不必要的绘制,但也可能因为内存问题导致整体的渲染性能下贱。因而图层合成加速追求的是一个动态的平衡。


什么样的RenderLayer会被创建GraphicsLayer呢,GPU Accelerated Compositing in Chrome是这样定义的:

  • Layer has 3D or perspective transform CSS properties
  • Layer is used by
  • Layer is used by a element with a 3D context or accelerated 2D context
  • Layer is used for a composited plugin
  • Layer uses a CSS animation for its opacity or uses an animated webkit transform
  • Layer uses accelerated CSS filters
  • Layer has a descendant that is a compositing layer
  • Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer overlaps a composited layer and should be rendered on top of it)


图层化的决策是由Blink来负责(未来可能会转移到Layer Compositor决策),根据DOM树生成一个图层树,并以DisplayList记录每个图层的内容。

了解了图层合成加速的概念以后,我们再来看看发生在Layout操作之后的Compositing update(合成更新),合成更新就是为特定的RenderLayer(创建规则我们已经描述过了)创建GraphicsLayer的过程,如下所示:

image.png

05 Prepaint


什么是属性树?

在描述属性的层次结构这一块,之前的方式是使用图层树的方式,如果父图层具有矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),需要递归的应用到子节点,时间复杂度是O(图层数),这在极端情况下会有性能问题。

因此引入了属性树的概念,合成器提供了变换树、裁剪树、特效树等。每个图层都由若干节点id,分别对应不同属性树的矩阵变换节点、裁剪节点和特效节点。这样的时间复杂度就是O(要变化的节点),如下所示:

image.png


Prepaint的过程就是构建属性树的过程,如下所示:

image.png

06 Paint


创建完属性树(Prepaint)以后,就开始进入Paint阶段了。

相关文档


相关源码


Paint操作会将布局树(Layout Tree)中的节点(Layout Object)转换成绘制指令(例如绘制矩形、绘制字体、绘制颜色,这有点像绘制API的调用)的过程。然后把这些操作封装在Dsipaly Item中,这些Dsipaly Item存放在PaintArtifact中。PaintArtifact就是是Paint阶段的输出。

到目前为止,我们建立了可以重放的绘制操作列表,但没有执行真正的绘制操作。

注:重放(replay),现在图形系统大都采用recrod & replay机制,采集绘制指令与执行绘制指令相互分离,提高渲染效率


image.png


在绘制的过程中,会涉及一个绘制顺序的问题,它使用的是stacking order(z-index),而不是DOM order。z-index会决定绘制顺序,在没有z-order指定的情况下,Paint会按照以下顺序进行绘制。

image.png

  • 背景色
  • floats
  • 前景色
  • 轮廓


Paint操作最终会在Layout Tree的基础上生成一棵Paint Tree。

image.png


Layer Compositor

07 Commit


Paint阶段完成以后,进入Commit阶段。该阶段会更新图层和属性树的副本到合成器线程,以匹配提交的主线程状态。说的通俗点,就是把主线程里Paint阶段的数据(layers and properties)拷贝到合成器线程,供合成器线程使用。

image.png

08 Tiling


但是合成器线程接收到数据后,并不会立即开始合成,而是进行图层分块,这里又涉及一个分块渲染的技术。

什么是分块渲染?

分块渲染(Tile Rendering)就是把网页的缓存分为一格一格的小块,通常为256x256或者512x512,然后分块进行渲染。

分块渲染主要基于两个方面的考虑:

  • GPU合成通常是使用OpenGL ES贴图实现的,这时候的缓存实际就是纹理(GL Texture),很多GPU对纹理的大小是有限制的,比如长宽必须是2的幂次方,最大不能超过2048或者4096等。无法支持任意大小的缓存。
  • 分块缓存,方便浏览器使用统一的缓冲池来管理缓存。缓冲池的小块缓存由所有WebView共用,打开网页的时候向缓冲池申请小块缓存,关闭网页是这些缓存被回收。


图块(tiling)是栅格化工作的基本单位。 栅格化会根据图块与可见视口的距离安排优先顺序进行栅格化。离得近的会被优先栅格化,离得远的会降级栅格化的优先级。这些图块拼接在一起,就形成了一个图层,如下所示:

image.png

09 Raster


图层分块完成以后,接着就会进行栅格化(Raster)。

什么是光栅化(栅格化)?

光栅化(Raterization),又称栅格化,它用于执行绘图指令生成像素的颜色值,光栅化策略分为两种:

  • 同步光栅化:光栅化和合成在同一线程,或者通过线程同步的方式来保证光珊化和合成
    • 直接光栅化:直接将所有可见图层的eDisplayList中的可见区域的绘图指令进行执行,在目标Surface的像素缓冲区上生成像素的颜色值。当然如果是完全的直接光栅化,就不涉及图层合并了,也就不需要后面的合成了。
    • 间接光栅化:允许为指定图层分配额外的缓冲区,该图层的光栅化会先写入自身的像素缓冲区,渲染引擎再将这些图层的像素缓冲区(Android里可以调用View.setLayerType允许应用为View分配像素缓冲区)通过合成输出大欧姆表Surface的像素缓冲区。Android和Flutter主要使用直接光栅化的测量,同时也支持间接光栅化。
  • 异步分块光栅化


上面说到,在Paint阶段会生成DisplayItem列表,它们是对绘制指令的封装。光栅化(Raster)或者栅格化的过程就是把这些绘图指令变成位图(像素点,每个像素点都带有自己的颜色)。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1593585931104-95b9507b-bbdc-43d9-8eab-5a0c430bb6c2.png#align=left&display=inline&height=265&margin=%5Bobject%20Object%5D&name=image.png&originHeight=549&originWidth=1242&size=300772&status=done&style=none&width=600)

光栅化的过程还包括图片解码。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1594026056074-d76001e0-9120-4b19-ac5c-f11593d6514e.png#align=left&display=inline&height=271&margin=%5Bobject%20Object%5D&name=image.png&originHeight=576&originWidth=1276&size=358615&status=done&style=none&width=600)

过去GPU只是作为一个内存(GPU Memory),这些内存被GL纹理(OpenGL中的标识符)所引用。我们会将栅格化的像素点放到主内存中,然后上传到GPU,以减小内存压力。

现在GPU已经可以运行产生像素点的着色器,可以在GPU上进行栅格化,这种模式成为加速栅格(硬件加速)。不管是硬件栅格化还是软件栅格化,本质上都是生成了某种内存中像素的位图,这个时候还没有显示到屏幕上。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1593585957507-654a5009-fc3c-47eb-bb35-b46cf91d1c47.png#align=left&display=inline&height=291&margin=%5Bobject%20Object%5D&name=image.png&originHeight=549&originWidth=1132&size=256218&status=done&style=none&width=600)

GPU栅格化并不是直接调用GPU,而是通过Skia图形库(Google维护的2D图形库,在Android、Flutter、Chromium都有使用)发出的OpenGL调用。如下所示:

Skia提供了某种抽象层,屏蔽了底层硬件、路径、贝塞尔曲线等复杂的概念,当需要栅格化显示项(Display Item)时,会先去调用SkCanvas上面的方法,它是Skia的调用入口。SkCanvas提供了Skia内部更多的抽象,在硬件加速时,它会构建另一个绘图操作缓冲区,然后对其进行刷新,在栅格化任务结束时,通过flush操作,我们获得了真正的GL指令。GL指令运行在GPU Process。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1593586064954-8bed11e0-a743-415e-8e20-54fcb68ccc43.png#align=left&display=inline&height=268&margin=%5Bobject%20Object%5D&name=image.png&originHeight=566&originWidth=1267&size=330761&status=done&style=none&width=600)

Skia和GL指令可以运行在不同进程,也可以运行在同一个进程这就产生了两种调用方式。
  1. In Proess Raster
  2. Out of Proess Raster


1 老版本的调用采用这种方式,Skia运行在Renderer Process,负责产生GL指令,GPU有单独的GPU Process,这种模式下Skia无法直接进行渲染系统调用,在初始化Skia的时候回给它一个函数指针表(指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理),函数指针表转换为真正的OpenGL API的过程称为命令缓冲区(GpuChannelMsg_FlushCommandBuffers),

单独的GPU进程有利于隔离GL操作,提升稳定性和安全性,这种模式也称为沙箱机制(不安全的操作运行在独立的进程里)。

image.png


2 新版本把绘制操作放到了GPU Process,在GPU一侧运行Skia,这有助于提升性能。

image.png



接下来就是执行GL指令,GL指令一般是由底层so库提供,在Windows平台上OpenGL还会被转换为DirectX(Microsoft的图形API,用于图形加速)。

image.png

10 Activate


在Commit之后,Draw之前有一个Activate操作。Raster和Draw都发生在合成器线程里的Layer Tree上,但是我们知道Raster操作是异步的,有可能需要执行Draw操作的时候,Raster操作还没完成,这个时候就需要解决这个问题。

它将Layer树分为:

  • Pending Tree:负责接收commit,然后将Layer进行Raster操作
  • Active Tree:会从这里取出栅格化好的Layer进行draw操作。


这个拷贝的过程就称为Activate,如下所示:

image.png


事实上Layer Tree主要有四种:

  • 主线程图层树:cc::Layer,始终存在。
  • Pending树:cc::LayerImpl,合成器线程,用于光栅化阶段,可选。
  • Active树:cc::LayerImpl,合成器线程,用于绘制阶段,始终存在。
  • Recycle树:cc::LayerImpl,合成器线程,与Pending树不会同时存在。


主线程的图层树由LayerTreeHost拥有,每个图层以递归的方式拥有其子图层。Pending树、Active树、Recycle树都是LayerTreeHostImpl拥有的实例。这些树被定义在cc/trees目录下。之所以称之为树,是因为早期它们是基于树结构实现的,目前的实现方式是列表。

11 Draw

当每个图块都被光栅化以后,合成器线程会为每个图块生成draw quads(在屏幕指定位置绘制图块的指令,包含了属性树里面的变换、特效等操作),这些draw quads指令被封装在CompositorFrame对象中,CompositorFrame对象也是Render Process的输出产物。它会被提交到GPU Process中。我们平时提到的60fps输出帧率里面的帧指的就是Compositor Frame。

Draw操作就是栅格化的图块生成draw quads的过程。

image.png

Display Compositor

12 Display


相关文档


Draw操作完成以后,就生成了Compositor Frame,它们会被输出到GPU Process。 它会从多个来源的Render Process接收Compositor Frame。

  • Browser Process也有自己的Compositor来生成Compositor Frame,这些一般是用来绘制Browser UI(导航栏,窗口等)。
  • 每次创建tab或者使用iframe,会创建一个独立的Render Process。


image.png


Display Compositor运行在Viz Compositor thread,Viz会调用OpenGL指令来渲染Compositor Frame里面的draw quads,把像素点输出到屏幕上。

什么是VIz?

Viz是VIsual的缩写,它是Chromium整体架构转向服务化的一个重要组成部分,包含Compositing、GL、Hit Testing、Media、VR/AR等众多功能。


VIz也是双缓冲输出的,它会在后台缓冲区绘制draw quads,然后执行交换命令最终让它们显示在屏幕上。

什么是双缓冲机制?

在渲染的过程中,如果只对一块缓冲区进行读写,这样会导致一方面屏幕要等到去读,而GPU要等待去写,这样要造成性能低下。一个很自然的想法是把读写分开,分为:

  • 前台缓冲区(Front Buffer):屏幕负责从前台缓冲区读取帧数据进行输出显示。
  • 后台缓冲区(Back Buffer):GPU负责向后台缓冲区写入帧数据。

这两个缓冲区并不会直接进行数据拷贝(性能问题),而是在后台缓冲区写入完成,前台缓冲区读出完成,直接进行指针交换,前台变后台,后台变前台,那么什么时候进行交换呢,如果后台缓存区已经准备好,屏幕还没有处理完前台缓冲区,这样就会有问题,显然这个时候需要等屏幕处理完成。屏幕处理完成以后(扫描完屏幕),设备需要重新回到第一行开始新的刷新,这期间有个间隔(Vertical Blank Interval),这个时机就是进行交互的时机。这个操作也被称为垂直同步(VSync)。


到这里,整个渲染流程就结束了,前端的代码变成了可以与用户交互的像素点。