笔记 - 像素的一生
内容来自于 2018 年谷歌的一个视频。原视频不长但内容很多,非常值得一看。Chrome 的渲染机制是在不断变化的。
笔记原存于 Logseq,如果有类似于,“见CSS/图层”一类的语句,为笔记内超链接。相关概念需自行了解。
📚 资料
渲染流程:web content → magic(rendering) → pixels
1. Web content
最常见的 HTML + CSS + Javascript API
还有图片、视频、音频、web assembly、WebGL、Canvas、PDF等等
2. 渲染
渲染是在一个 sandbox 进行的。渲染引擎 Blink 是渲染代码的一个子集。
操作系统渲染 API:OpenGL,DirectX(Windows), vulkan。包含 textures shaders 等等。
2.1 渲染目标
-
把 web content 渲染为 openGL 的调用
-
为更新渲染建立起对应的数据结构
2.2 基本渲染流程
渲染流程太复杂,会被分为几个阶段进行数据结构的转换
不是每一次更新渲染,都要走全部的流程。相关看 浏览器/渲染原理 中的回流与重绘。
HTML to DOM
解析 HTML 为 DOM 树。DOM树的作用有两个,一个表示文档结构,二是暴露 API 给 Javascript,由 V8 JS引擎进行 API 绑定。
CSS to ComputedStyle
CSS 转换为 StyleRule 集,每个 StyleRule 包含 CSSSelector 和 CSSPropertyValue。流程如图
根据 css 样式规则,计算出每个 DOM 元素样式属性的最终值,存储于 ComputedStyle 对象模型中,是一个 style properties 与 values 的超大映射。这个过程被称为 Style Resolution。
ComputedStyle 对象已经暴露给了 JS。使用getComputedStyle(element)['padding-top']
即可获取。在 Dev tools 的 Elements 中的 Computed 选项卡的值就是依据此 对象来的。
Layout Stage
例子:
- Web 最基础的 Block Flow 布局,需要计算出 Block 的 x、y、width、height。
- 由于 Block 自适应高度,需要根据内容的尺寸,找到文字换行的地方,以计算每个 Block 的高度。
- 每一个 Block 的矩形有多种边界(在CSS/盒模型中说得很清楚)。内容 overflow 时,需要计算两个矩形,一个是实际内容区域,一个是能显示出的内容的区域。如果内容可滚动,还要计算滚动边界和滚动条边界。
Document 的根节点本身就是 overflow 且可滚动的。
Layout Objects 也树结构存在,几乎与 DOM 是一对一,但并非总是如此。比如伪元素、浮动元素。
在 style 计算后会构建一个没有填入任何数据的 Layout Tree。
更新布局的本质就是遍历 layout tree 并向里面填充所有数据。
Slides里说目前没有把 Layout Stage 的输入输出区分开,但没细说,只是说下个版本会改。放个图
Paint to display item lists
做类似于在指定坐标内画一个红色的矩形这样的动作。代码结构上 ,是 LayoutObject 有一个 Paint 方法。去调用更底层的 Paint API。
此阶段生成“作画步骤”,还没有真的画出像素。步骤是可以重放的。至于为什么这样做,之后再说(然而之后并没有说)。
Paint 是从 z-index 最大到最小进行的,而不是 DOM 的前后顺序。而两个并列的层叠上下文时,后覆盖前。在一个 CSS/层叠上下文 内,按层叠上下文的堆叠规则绘制(z轴的层叠规则),如图
Rasterization 栅格化
将 Display Item(位于 CC Layer 中,之后说)中记录的 Paint 操作转化为位图(bitmap)。
raster bitmap 通常保存于 GPU 内存中,被 OpenGL Texture Object 引用。GPU 不仅可以保存 bitmap,也可以生成 bitmap。叫 accelerated rasterization。
此时像素纹理已经生成到内存,但还是没有画到屏幕上。
栅格化通过 SKIA 库生成对 OpenGL 的调用。SKIA 提供了一系列对硬件的抽象。具体而言,PaintOps 会调用 SKIA 中的 SkCanvas 对象。
由于渲染过程是在 sandbox 进行的, 不能产生系统调用,OpenGL 调用是通过 command buffer 塞进另一个进程 GPU Process 执行的。GPU Process 接受到绘制命令后通过 GL API 去产生真正的 GL 调用。
使用 GPU process 隔离渲染进程除了渲染进程有沙盒机制外,OpenGL 可能也不太稳定或者是有漏洞。GPU Process 可以做一些保护措施。
GL API 来源于系统动态库 libGLESv2.so
。但是 windows 中来自于 Google 的 ANGLE 库 libglesv2.dll
,因为 Windows 上渲染不是用的 OpenGL,而是 DirectX API。ANGLE 库可以翻译 OpenGL 调用为 DirectX 调用。
3. 图层合成
3.1 帧与动画
每一帧是当前 Web 内容的完整呈现。
动画是连续的帧。针对第一部分提到的 style、layout、paint、raster,浏览器都做了跟踪失效的处理,只重绘改变的部分,其他部分复用。
3.2 合成线程
一个单独的渲染线程,减少 JS 主线程的其他操作和渲染之间 block。
图层给合成线程渲染。比如 video 和 gif 在单独的图层,还有 transform3D, will-change 等 css 属性也会新建图层进行处理。
合成线程也需要处理交互。能处理的先合成线程处理(比如滚动),处理不了的就进主线程。
Layer Tree(CC Layer Tree)
图层也是以树结构存在,前序遍历。
有的图层的存在不是绘制,而是图层效果,比如剪贴蒙版、滤镜。
图层的合成位于 CC (Chronium compositor) namspace, 所以代码里有很多 cc::layer
。
如果一个 Layout Object 没有指定单独的 layer(比如没有 will-change 属性),就会被绘制到父 layer 的图层。
Paint Layer 是要被分到 CC LayerTree 的。这就是CSS/层叠上下文与CSS/图层之间的关系。
3.3 compositioning update
在上一章的流程中,没有讲到合成这步,实际要加上。因为合成不是必须的,但合成步骤能优化渲染。
未来,创建图层的工作会放到 paint 之后(slimming paint)
commit
提交一次合成。这里合成线程与主线程都存在 layer tree,需要同步合成线程与主线程的状态。
3.4 Tilling
在 rastering 阶段,把 CC layer 分块成 tile。
tile 是 raster 的最小单位,在专门的 raster 线程 进行栅格化。
合成线程有一个 tile manager 安排 tile 优先级。
不同分辨率的 tile 策略是不同的。
3.5 Drawing
不同 tile 合成为 Quad。Quad 引用内存中的 raster output,封装在合成进程中,再提交到浏览器进程。浏览器动画帧的帧概念就是 Quad。
不同 commit 有先后顺序,需要从 pending 激活再绘制。
3.6 Display
浏览器进程将 Quad 展示到屏幕上的过程。位于 Viz 组件中,调用 OpenGL 绘制 GPU 进程中的 Quad 资源,和 rastering 的 GL call 一样。
大部分平台的显示合成输出是双倍缓冲(有过游戏画面撕裂经验的应该对这有概念),quad 是在后台缓冲器(GPU的还是Viz?)中绘制的,用 swap 命令让后台的 quad 到前台展示。
4. 总结
Blink 引擎严格执行了主线程的步骤。但由于要实现 Web 平台化,是有一些合成线程的权限的。
其他
渲染器和浏览器都用到了 GPU,都有向GPU进程的IPC通道。
如果想让滚动的交互产生的动画不在主线程而在合成线程执行,需要强制 will-transform 分层