1

按照渲染的先后顺序会经历下面几个阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
前文我们介绍了构建 DOM 树、样式计算、布局阶段,本文我们接着后面讲,说下分层、绘制、分块、光栅化和合成。
我们先简单回顾下前三个阶段的主要内容:在 HTML 页面内容被提交给渲染引擎之后,渲染引擎首先将 HTML 解析为浏览器可以理解的 DOM;然后根据 CSS 样式表,计算出 DOM 树所有节点的样式;接着又计算每个元素的几何坐标位置,并将这些信息保存在布局树中。

分层

现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?答案依然是否定的。因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。就类似PS中的图层的概念。正是这些图层叠加在一起构成了最终的页面图像。要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况。

下面这张图是很经典的从DOM Tree 到 Layer Tree的转化
image.png
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。
满足下面条件中的任何一个的元素就可以被提升为单独的一个图层:

  • NormalPaintLayer

    • 根元素(HTML)
    • 有明确的定位属性(relative、fixed、sticky、absolute)
    • 透明的(opacity 小于 1)
    • 有 CSS 滤镜(fliter)
    • 有 CSS mask 属性
    • 有 CSS mix-blend-mode 属性(不为 normal)
    • 有 CSS transform 属性(不为 none)
    • backface-visibility 属性为 hidden
    • 有 CSS reflection 属性
    • 有 CSS column-count 属性(不为 auto)或者 有 CSS column-width 属性(不为 auto)
    • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
  • OverflowClipPaintLayer

    • overflow 不为 visible

图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?通常,你会把你的绘制操作分解为三步:

  1. 绘制蓝色背景;
  2. 在中间绘制一个红色的圆;
  3. 再在圆上绘制绿色三角形。

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

你也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示:
image.png
在该图中,右侧下方左侧部分 就是 document 的绘制列表,拖动右侧下方由侧部分 中的进度条可以重现列表的绘制过程。

栅格化(Raster)

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:

image.png

如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?

通常情况下,一个页面可能很长,不会只有一屏。我们把用户可以看到的这个部分叫做视口(viewport)。通过视口,用户只能看到页面的其中一部分,所以在这种情况下,要绘制出全部图层内容的话,就会产生太大的开销,而且也没有必要。基于这个原因,合成线程首先会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:
image.png
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

image.png

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:

image.png
从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

渲染流程总结

好了,我们现在已经分析完了整个渲染流程,从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程:
结合上图,一个完整的渲染流程大致可总结为如下:
image.png

  1. 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  3. 创建布局树,并计算元素的布局信息。
  4. 对布局树进行分层,并生成分层树。
  5. 为每个图层生成绘制列表,并将其提交到合成线程。
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

好了深入浅出渲染流程第二部分也已经讲完了,你们以为这就结束了吗,嘿嘿,当然没有啦,细心的同学可能已经注意到了,第一张图中最后还有一个Graphic Layer是什么鬼?没错,下篇文章我们就重点说说这个,另外还有关于渲染流程的优化,大家敬请期待哦~


wens
272 声望4 粉丝