浏览器的"光栅化"与"合成帧"到底是什么?

2,115 阅读6分钟

引言

本文将会介绍光栅化与合成帧的具体工作方式和简单的工作原理, 为了照顾绝大部分不同基础的小伙伴, 首先会简单介绍一下浏览器的渲染过程.

渲染流程

浏览器的基本渲染会分为几个简单的步骤:

  1. 解析HTML, 生成DOM树;
  2. 解析CSS样式为CSSOM, 通过选择器挂载到不同的DOM节点上;
  3. 通过每个DOM树节点上不同的CSS样式, 生成相应的布局树;
  4. 通过光栅线程, 将每个图层光栅化;
  5. 光栅化完成后, 合成线程会收集被称为draw quads的图块信息用于创建合成帧;
  6. 合成帧被发送给 GPU 进程, 这一帧结束.

先简单解释一下上面的步骤, 我们的渲染进程会先解析HTML, 将HTML文档的每个标签和属性解析成我们的DOM节点, 然后通过解析我们的CSS样式为CSS对象模型, 通过对象模型中的选择器, 匹配到我们的DOM节点上, 将样式信息应用到我们的节点上. 现在虽然知道了我们的样式节点, 但是仍然无法绘制我们的页面.

所以我们的主线程会遍历我们的DOM节点并计算样式, 创建布局树, 其实这个布局树说简单了也就是将我们DOM树上不展示的节点" 摇 "掉, 例如我们head标签内的内容或css属性为display:none visibility:hidden p::before{content:"Hi!"}等等的样式节点.

其实只知道只有我们的布局树还是不足与绘制页面, 由于HTML中的元素是按照顺序进行绘制的, 会导致显示几何位置错位的现象. 所以我们引入了涂层的概念, 也就是三位平面的Z轴. 例如我们css中的z-index属性, 会将我们每一个模块形成成不同的层级, 在此绘制步骤中,主线程将遍历布局树以创建绘制记录。绘画记录是绘画过程的笔记,如"先背景,然后是文字,然后是矩形"。如果您使用js绘制了元素,那么您可能熟悉此过程。

接下来就到了我们今天的主角----光栅化&合成帧.

在了解他们之前我先来简单介绍一下几个概念.

什么是光栅化

光栅化是图形学上的一个术语, 其实说白了就是先将我们页面上每个图层分为约为 256x256像素的正方形网格.如下如所示:

image.png

(打开devTools, 点击更多工具, 选择绘制, 然后勾选边框层就可以看见)

分成网格后, 我们将此信息转化为屏幕上的像素生成位图, 最后将此位图信息提交到GPU缓存中. 这里面有一个细节就是栅格化分为 CPU栅格化和GPU栅格化.

CPU光栅化

其实Chromium内核是使用Skia库进行光栅化的, 这个库通过扫描线算法创建位图(这是一个非常线性的方法). 要将结果发送到CUP以在屏幕上绘制, 正常我们可以通过glTexImage2D()这个API来进行提交数据的.

但是周所周知, 由于渲染进程实在沙盒化的, 所以无法直接访问GPU, 因此Chromium会通过GPU进程在GPU与渲染进程之间进行代理; 如图:

image.png

接受 OpenGL 命令并将其传递给图形驱动程序. 因此, 我们必须将光栅化结果放入共享内存中, 并向Chromium GPU进程发送消息以调用glTexImage2D().

可能有些细心的朋友会发现, 这里面有一个明显的缺点, 就是由于我们CPU是线性工作的, 如果我们改变了元素的几何属性, 导致重排的话, 提交到GPU的数据就会大量激增, 如果我们大量的使用动画, CPU的工作负荷会大大的增加, 性能强大额PC还好, 如果是在移动设备上, 由于屏幕较小, 通常会隐藏一些元素, 直到用户请求就会产生大量的过度效果.

零拷贝光栅化

其实这种方式是CPU光栅化的一种优化, 说白了也就是说, 光栅化的方式与之前相同, 只不过将光栅化后生成的位图信息保存在我们的主内从当中, 为了最大程度减少传输到CPU过程当中的压力, 这次采用的方式不是手动通过glTexImage2D()API去上传, 而是告诉GPU映射的位图在主内存中的位置, 让GPU自己去读取.

GPU光栅化

由于GPU非线性运行的, 比较善于并发处理数据, GPU光栅化最直接的改变就是将CPU的负载转移到了GPU上. 所有多边形都必须使用 OpenGL 基元(三角形和线)进行渲染, 这也是由Skia通过名为Skia Ganesh的GPU后端执行的, 结果永远不会保存在主内存当中, 而是保存在CPU的显存(我们买显卡说的6GB显存, 8GB现存就是干这个用的)当中, 因此不用复制到任何地方, 也节省了主内存的空间.

其实GPU渲染也不是完美的, 由于GPU渲染最大的瓶颈就是现实字体或者小而复杂的形状. OpenGL没有任何原生文本渲染单元, 如果使用三角形来表述字符, 那么以汉语为例, 那需要多少个三角形才能组成一篇文章呢, 想想都是个灾难性的工程.

于是我们会创建一个预先计算的字符集, 并且存储到显存中, 其实也就是一个字体图集; 如图:

image.png

这个可以理解为我们前端开发当中的雪碧图. 虽然效果不错, 但是仍然无法满足我们世界上这么多语言, 而且, 我没每放大一个字体, 就需要重新渲染一次, 找到匹配字号的文字位置, 这着实不是最优解.

Chromium渲染方案

虽然利用GPU渲染效果会很好, 但是不能满足我们所有场景, 所以Chromium内核采用的是CPU零拷贝光栅化的方案, 即解决了文字渲染问题, 有解决了传输效率过低的问题;

合成帧

合成帧简单说就是最终呈现到我们显示器的过程, 就是通过我们的GPU线程, 通知GPU需要展示的映射在之内存当中的位图信息, 所需要渲染的所有的位图信息通过合成线程合成一帧提交到GPU当中, 在我们的显示器里面显示出来, 合成线程为什么单独存在呢, 因为也是为了防止和主线程发生阻塞, 当主线程进行复杂的计算时, 合成线程可以正常工作.

总结

这篇文章, 介绍了简单的浏览器渲染流程, 也介绍了什么是光栅化, 就是将我们页面信息生成位图的过程. 同时我们还介绍了CPU光栅化和GPU光栅化的区别和优势劣势. 最后我们总结了Chromium的渲染方案, 以及合成线程的作用和存在的意义.

文章如有不足, 欢迎评论区指出, 我会及时修正, 谢谢各位!