在浅析Chrome的渲染流程(上)中我们介绍了渲染流水线中的 DOM生成、样式计算 和 布局 三个阶段。今天我们来讲下渲染流水线后面的阶段。
分层
经过生成布局之后生成的布局树,将每个元素的具体位置信息都计算出来了,那么接下来是不是开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果,比如一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等。为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerThree)。提到图层,大家最先应该想到的是PS软件。PS中图层是很常见的,比如将图片背景变成透明的,就需要先复制一个图层出来后,用滤棒清除掉图层中不需要的部分,然后应用到原始图像上,就可以生成一个具有透明背景的图片了。
要想直观地理解什么是图层,可以通过Chrome的 “开发者工具”,选择 “Layers” 标签,就可以可视化页面的分层情况,如下图:
如果找不到 “Layers” 这个标签,请参考下图:
从上面的第一张图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定的顺序折叠加在一起,就形成了最终的页面。
现在你知道了浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。那么这些图层和布局树节点的之间的关系是什么样子呢?请参考下图:
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的图层,那么这个节点就从属于父节点的图层。比如上图中的span标签就没有专属图层,那么它们就从属于它们的父节点图层。但不管怎样,最终每个节点都会直接或间接地从属一个层。
浏览器对于满足以下情况的节点会单独创建一个图层:
1.拥有层叠上下文属性的元素会被提升为单独的一个层。
页面是个二维平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上。
这张是层叠上下文的示意图。
层叠上下文一般具有以下几个属性:
- 文档根元素(html)。
- postion值为absolute(绝对定位-相对于父级元素)或relative(相对定位)且z-index不为auto的元素。
- position值为fixed(固定定位-相对于整个文档)或sticky(粘滞定位)的元素。
- flex(flexbox)容器的子元素,且z-index值不为auto。
- grid容器的子元素,且z-index值不为auto。
- opacity属性值小于1的元素。
- mix-blend-mode属性值不为normal的元素。
- transform/filter/perspective/clip-path/mask/mask-image/mask-border的属性值不为none的元素。
- isolation的属性值为isolate元素。
- -webkit-overflow-scrolling属性值为touch的元素。
- will-change值设定了任一属性而该属性在non-initial值时会创建层叠上下文的元素。
- contain属性值为layout、paint或包含它们其中之一的合成值(比如contain:strict/content)的元素。
2.需要剪裁(clip)的地方也会被创建为图层
不过你先需要了解什么是剪裁,结合下面的HTML代码:
<style>
div {
width: 200;
height: 200;
overflow:auto;
background: gray;
}
</style>
<body>
<div>
<p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
<p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
<p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p>
</div>
</body>
在这里我们把div大小限定为200 * 200像素,而div里面的文字内容比较多,文字所显示的区域肯定会超出200 * 200的面积,这时候就产生了剪彩,渲染引擎会把剪裁文字内容的一部分用于显示在div区域,最终的效果如下图:
所以说,元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升为单独一层。
我们也可以通过Layers查看我们写的页面对应的图层树,尽量减少图层树的深度,进一步优化我们的网页代码。
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么它是怎么实现绘制的呢?
渲染引擎会把每一个图层的绘制拆分为很多小的绘制指令,然后把这些指令按照顺序组成一个待绘制列表,如下图:
从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩阵或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
同样我们可以在 “Layers” 里通过点击相应的图层来查看它的绘制指令。
栅格化操作
绘制列表只是用了记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
上图中说明了渲染主线程和合成线程之间的关系。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
我们先讲下视口的概念:
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫作视口。
用户通过视口只能看到页面的一部分,在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是256 * 256或者512 * 512。
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将位图转换为图块。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图:
通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫作快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。
GPU操作是运行在GPU进程中的,如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。
从图中可以看出,渲染进程把生成图块的指令发送给GPU,然后在GPU中执行生成图块的位图,并保存在GPU的内存中。
合成和显示
一旦所有图块被栅格化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发送过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过一些列的阶段,编写好的HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面了。
渲染流水线总结
经过两篇文章的介绍,大家对浏览器的渲染过程有个大致的印象了吧。我来总结一下:从HTML到DOM、样式计算、布局、图层树、绘制、光栅化、合成和显示。如下图所示:
- 渲染进程将HTML内容转化为浏览器能够读懂的DOM树结构。
- 渲染引擎将CSS样式表转化为浏览器能够理解的styleSheets,计算出DOM节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交给合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转化为位图。
- 合成线程发送绘制指令DrawQuad给浏览器进程。
- 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。
来源:CSDN
作者:三月踏雪
链接:https://blog.csdn.net/weixin_42071117/article/details/104884732