关于BeesAndroid项目
javascript
BeesAndroid项目提供了一系列的工具、理论分析与方法论,旨在下降Android系统源码的阅读门槛,让读者更好的理解Android系统的设计与实现。第一次阅览本系列文章,请参见导读,更多文章请参见文章目录。css
今天咱们来聊一聊Chromium的渲染机制,这也是渲染机制系列的第二篇,最近大半年的工做都和H5容器有关,于是花了点时间学习了下Chromium项目,这里着重去分析一下它的渲染机制。
从开发者的角度,当咱们去看一个H5容器的时候,和它一块儿工做的有如下角色:
html
以下所示:
前端
能够看到,页面在渲染以前还有须要工做须要处理,容器的启动也是个耗时的操做,为何会特意聊聊容器启动呢,由于这个也是H5页面体验的重要组成部分,由于是Native的关系,前端同窗可能会关注不到。并且容器导航阶段是重要的预加载时机,咱们能够在这里作不少事情,例如:java
- 接口预加载
- HTML文档预加载
- 资源预加载
- 导航的时候建立一个JS Engine,能够提早执行JS逻辑,把导航预加载这个能力开放给前端
言归正传,咱们接着来聊聊渲染机制。
node
浏览器的渲染过程就是把网页经过渲染管道渲染成一个个像素点,最终输出到屏幕上。这里面就涉及3个角色
android
什么是输入端(Content)?
git
咱们在Chromium这个项目里会频繁的看到Content这个概念,那么Content究竟是什么呢。Content是渲染网页内容的区域,在Java层对应AwContent,底层有WebContents表示,以下所示:github
content在代码由content::WebContents来描述,它在独立的Render进程由Blink建立。具体说来Content对应着前端开发中涉及的HTML、CSS、JS、image等,以下所示:web
什么是渲染管线(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。以下图所示:
**
咱们先来讲结构
从上到下,分层来讲:
这里面还提到了每一个层级向上输出的产物帧,帧(Frame)描述了渲染流水线下级模块向上级模块输出的绘制内容相关数据的封装。
整个渲染水流水线的调度基于请求和状态机响应,调度的中枢运行在Browser UI线程,它按照显示器的VSync信号向Layer Compositor发出输出下一帧的请求,而Layer Compositor根据自身的状态机的状态决定是否须要Blink输出下一帧。而Layer Compositor和Display Compositor是生成者和消费者的关系,Display Compositor持有一个Compositor Frame队列不断的进行取出和绘制,输出的频率取决于 Compositor Frame的输入帧率和自身GL Frame的绘制频率。
咱们再来讲流程
咱们来分别看具体的流程。
注:Rendering Pipeline里的图片来自于Chromium工程师的ppt Life of a Pixel的截图。
相关文档
相关源码
当咱们从服务器上下载了一份HTML文档,第一步就是解析,HTML解析器接收标签和文本流(HTML是纯文本格式)把HTML文档解析成DOM树。DOM(Document Object Model)即文档对象模型,DOM及时页面的内部表示,也为JavaScript暴露了API接口(V8 DOM API),可让JavaScript程序改变文档的结构、样式和内容。
它是一个树状结构,咱们在后续的渲染流程中还会看到不少树形结构(例如布局树、属性树等)由于它们都是基于DOM树的结构(HTML的结构)而来的。
注:HTML文档中可能包含多棵DOM树,由于HTML支持自定义元素,这种树一般被称为Shadow Tree。
解析HTML生成DOM树流程以下:
DOM树(DOM Tree)做为后续绘制流程的基础, 还会基于它生产各类类型的树,具体说来,主要会经历以下转换:
对象转换
- DOM Tree -> Render Tree -> Layer Tree
- DOM node -> RenderObject -> RenderLayer
DOM Tree(节点是DOM node)
当加载一个HTML时,会对他进行解析,生成一棵DOM树。DOM树上的每个节点都对应这网页里面的每个元素,网页能够经过JavaScript操做这棵DOM树。
Render Tree(节点是RenderObject)
可是DOM树自己并不能直接用于排版和渲染,所以内核会生成Render Tree,它是DOM Tree和CSS相结合的产物,二者的节点几乎是一一对应的。Render Tree是排版引擎和渲染引擎之间的桥梁。
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
- Corresponds to a
对上面的流程不了解也不要紧,咱们下面会一一解释。
当DOM树生成之后,就须要为每一个元素设置一个样式,有的样式只是会影响某个节点,有的样式会影响整个节点下面的整个DOM子树的渲染(例如,节点的旋转变换)。
相关源码
样式通常都是样式渲染器共同做用的结果,它有复杂的优先级语义和渲染过程,过程总体分为三步:
1 收集、划分和索引全部样式表中样式规则。
计算并应用了每一个DOM节点的样式之后,就须要决定每一个DOM节点的摆放位置。DOM节点都是基于盒模型摆放(一个矩形),布局就是计算这些盒子的坐标。
|-------------------------------------------------|
| |
| 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)。
在Layout操做完成之后,理论上就能够开始Paint操做了,可是咱们以前提过,若是直接开始Paint操做,绘制整个界面,代价是很是昂贵的。所以便引入了一个图层合成加速的概念。
什么是图层合成加速(Compositing Layer)?
图层合成加速基本思想是把整个页面按照必定规则分红多个图层(就像Photoshop的图层那样),在渲染时只须要操做必要的图层,其余图层只须要参与合成就好了,以此提升渲染效率。完成这个工做的线程叫Compositor Thread,值得一提的是Compositor Thread还具有处理输入事件的能力(例如滚动事件),可是若是在JavaScript注册了事件监听,它会把输入事件转发给主线程处理。
具体说来是为某些RenderLayer拥有本身独立的缓存,它们被称为合成图层(Compositing Layer),内核会被这些RenderLayer建立对应的GraphicsLayer。
- 拥有本身的GraphicsLayer的RenderLayer在绘制的时候就会绘制在本身的缓存里面。
- 没有本身的GraphicsLayer的RenderLayer会向上查找父节点的GraphicsLayer,直到RootRenderLayer(它老是会有本身的GraphicsLayer)为止,而后绘制在有GraphicsLayer的父节点的缓存里。
这样就造成了与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
- 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的过程,以下所示:
什么是属性树?
在描述属性的层次结构这一块,以前的方式是使用图层树的方式,若是父图层具备矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),须要递归的应用到子节点,时间复杂度是O(图层数),这在极端状况下会有性能问题。
所以引入了属性树的概念,合成器提供了变换树、裁剪树、特效树等。每一个图层都由若干节点id,分别对应不一样属性树的矩阵变换节点、裁剪节点和特效节点。这样的时间复杂度就是O(要变化的节点),以下所示:
Prepaint的过程就是构建属性树的过程,以下所示:
建立完属性树(Prepaint)之后,就开始进入Paint阶段了。
相关文档
相关源码
Paint操做会将布局树(Layout Tree)中的节点(Layout Object)转换成绘制指令(例如绘制矩形、绘制字体、绘制颜色,这有点像绘制API的调用)的过程。而后把这些操做封装在Dsipaly Item中,这些Dsipaly Item存放在PaintArtifact中。PaintArtifact就是是Paint阶段的输出。
到目前为止,咱们创建了能够重放的绘制操做列表,但没有执行真正的绘制操做。
注:重放(replay),如今图形系统大都采用recrod & replay机制,采集绘制指令与执行绘制指令相互分离,提升渲染效率
Paint操做最终会在Layout Tree的基础上生成一棵Paint Tree。
Paint阶段完成之后,进入Commit阶段。该阶段会更新图层和属性树的副本到合成器线程,以匹配提交的主线程状态。说的通俗点,就是把主线程里Paint阶段的数据(layers and properties)拷贝到合成器线程,供合成器线程使用。
可是合成器线程接收到数据后,并不会当即开始合成,而是进行图层分块,这里又涉及一个分块渲染的技术。
什么是分块渲染?
分块渲染(Tile Rendering)就是把网页的缓存分为一格一格的小块,一般为256x256或者512x512,而后分块进行渲染。
分块渲染主要基于两个方面的考虑:
- GPU合成一般是使用OpenGL ES贴图实现的,这时候的缓存实际就是纹理(GL Texture),不少GPU对纹理的大小是有限制的,好比长宽必须是2的幂次方,最大不能超过2048或者4096等。没法支持任意大小的缓存。
- 分块缓存,方便浏览器使用统一的缓冲池来管理缓存。缓冲池的小块缓存由全部WebView共用,打开网页的时候向缓冲池申请小块缓存,关闭网页是这些缓存被回收。
图块(tiling)是栅格化工做的基本单位。 栅格化会根据图块与可见视口的距离安排优先顺序进行栅格化。离得近的会被优先栅格化,离得远的会降级栅格化的优先级。这些图块拼接在一块儿,就造成了一个图层,以下所示:
图层分块完成之后,接着就会进行栅格化(Raster)。
什么是光栅化(栅格化)?
光栅化(Raterization),又称栅格化,它用于执行绘图指令生成像素的颜色值,光栅化策略分为两种:
- 同步光栅化:光栅化和合成在同一线程,或者经过线程同步的方式来保证光珊化和合成
- 直接光栅化:直接将全部可见图层的eDisplayList中的可见区域的绘图指令进行执行,在目标Surface的像素缓冲区上生成像素的颜色值。固然若是是彻底的直接光栅化,就不涉及图层合并了,也就不须要后面的合成了。
- 间接光栅化:容许为指定图层分配额外的缓冲区,该图层的光栅化会先写入自身的像素缓冲区,渲染引擎再将这些图层的像素缓冲区(Android里能够调用View.setLayerType容许应用为View分配像素缓冲区)经过合成输出大欧姆表Surface的像素缓冲区。Android和Flutter主要使用直接光栅化的测量,同时也支持间接光栅化。
- 异步分块光栅化
1 老版本的调用采用这种方式,Skia运行在Renderer Process,负责产生GL指令,GPU有单独的GPU Process,这种模式下Skia没法直接进行渲染系统调用,在初始化Skia的时候回给它一个函数指针表(指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理),函数指针表转换为真正的OpenGL API的过程称为命令缓冲区(GpuChannelMsg_FlushCommandBuffers),
单独的GPU进程有利于隔离GL操做,提高稳定性和安全性,这种模式也称为沙箱机制(不安全的操做运行在独立的进程里)。
在Commit以后,Draw以前有一个Activate操做。Raster和Draw都发生在合成器线程里的Layer Tree上,可是咱们知道Raster操做是异步的,有可能须要执行Draw操做的时候,Raster操做还没完成,这个时候就须要解决这个问题。
它将Layer树分为:
这个拷贝的过程就称为Activate,以下所示:
主线程的图层树由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的过程。
相关文档
Draw操做完成之后,就生成了Compositor Frame,它们会被输出到GPU Process。 它会从多个来源的Render Process接收Compositor Frame。
Viz是VIsual的缩写,它是Chromium总体架构转向服务化的一个重要组成部分,包含Compositing、GL、Hit Testing、Media、VR/AR等众多功能。
VIz也是双缓冲输出的,它会在后台缓冲区绘制draw quads,而后执行交换命令最终让它们显示在屏幕上。
什么是双缓冲机制?
在渲染的过程当中,若是只对一块缓冲区进行读写,这样会致使一方面屏幕要等到去读,而GPU要等待去写,这样要形成性能低下。一个很天然的想法是把读写分开,分为:
- 前台缓冲区(Front Buffer):屏幕负责从前台缓冲区读取帧数据进行输出显示。
- 后台缓冲区(Back Buffer):GPU负责向后台缓冲区写入帧数据。
这两个缓冲区并不会直接进行数据拷贝(性能问题),而是在后台缓冲区写入完成,前台缓冲区读出完成,直接进行指针交换,前台变后台,后台变前台,那么何时进行交换呢,若是后台缓存区已经准备好,屏幕尚未处理完前台缓冲区,这样就会有问题,显然这个时候须要等屏幕处理完成。屏幕处理完成之后(扫描完屏幕),设备须要从新回到第一行开始新的刷新,这期间有个间隔(Vertical Blank Interval),这个时机就是进行交互的时机。这个操做也被称为垂直同步(VSync)。
到这里,整个渲染流程就结束了,前端的代码变成了能够与用户交互的像素点。