一般来说,渲染引擎可以设计为两个线程:UI 线程和 GPU 线程
- UI 线程:构建 Tree、排版、绘制、事件
- GPU 线程:合成
UI 线程的工作会比较多,是比较重的一个线程。事件放在 UI 线程的好处是显示和交互是一体的,不会存在事件发生的时候 UI 还有很多未完成的操作。典型的例子是 WebView,WebView 的事件处理是单独的线程,所以一个长列表,你可以无限滚动,如果 UI 线程跟不上,就会导致白屏问题,本质上还是线程设计的问题。
2.2.2 构建 Tree
界面上的每一个元素叫做一个 Node,每个 Node 都是由一个树形结构 Tree 来管理的。Tree 的生成可以有不同的方式,但最原子的操作无非是 CRUD(增、删、查、改),这个部分没什么好说的,实现 CRUD 的 API 即可。
2.2.3 排版
排版过程主要是「Measure」和「Layout」,Measure 主要是计算元素大小,得到 Size,Layout 主要是给元素定位,得到元素相对位置 Offset,或者绝对位置 Position,「位置 + 大小」确定了一个元素在屏幕上的显示区域。
这个过程中最复杂的是选择什么样的排版算法,不同的方式对于节点计算差别很大(例如 CSS 盒子模型有 static、absolute、relative 等排版方式)。不过无论是怎么排,最终只是「父决定子」还是「子决定父」两种类型,以下面的 DOM 结构进行说明
说明:蓝色是叶节点,红色是父节点,max-w / max-h 是最大值,w / h 是期望值,假设窗口大小是 800 x 600
- 父决定子(fixed 固定值):节点 1 [800, 600],节点 2 [100, 50],节点 3 [50, 50],节点 4 [50, 50],节点 5 [5, 5],节点 6 [50, 50]
- 子决定父(auto 自适应):父控件需要等所有子控件确定大小,再回溯回来确定自己大小。节点 5 [w = 800 x 20% x 10%, h = 600 x 20% x 10] = [16, 12],节点 3 由节点 4 和节点 5 共同确定,大小为 [66, 62]
上面算法中,只是计算了宽和高的情况,实际计算中还要加上盒子模型中的 margin、border、padding 的值才是正确的,而且这些值也受 max-w 和 max-h 的影响。
子决定父(auto 自适应)的这种排版方式和 Android 的 WRAP_CONTENT 意思差不多,实现方式可能不尽相同。这种方式对排版性能比较大,如果子节点大小变化,会导致父节点也变化,最坏的情况是整棵树的大小都发生变化。所以修改节点大小时刻如何进行重排?
- 第一种:从 Root 节点开始全部重排,性能最差的方式
- 第二种:从第一个确定大小的祖先节点重排(fix 方式)
第一种方式比较暴力,性能会最差,第二种比较折中,但如果整棵树都是 auto 的,那和第一种没区别。
Measure:每一个 Node 计算出了最终的大小。例如盒子模型,则得出了 margin[left, top, right, bottom],border[left, top, right, bottom],padding[left, top, right, bottom],content[width, height] 的实际大小。
Layout:根据计算出来的大小,设定 Offset 或者 Position
- 相对于父节点的 Offset[x, y]
- 相对于屏幕坐标原点的 Position[x, y]
排版过程完成,元素的「位置 + 大小」就已经确定,在屏幕上的区域也就确定了。
排版算法最高效的是 absolute 排版,全部都是「父决定子」的方式,没有回溯的过程。Flexbox 排版算法也是「父决定子」的排版算法,性能也比较好。从 React Native 的使用经验上看,基本上 Flexbox 可以满足绝大多数的业务场景,而且也是比较标准化的排版算法,是个不错的选择。自定义排版算法不建议,因为会增加很多学习成本,Android 一堆的 Layout 控件,看着就很烦,而且很多控件排版性能也很差。
2.2.4 绘制、合成
绘制(Paint)的过程就是将每个 Node 变成「图层」的过程,图层的大小是自己的 Size。这个图层并不一定是指一张内存中的 Bitmap,它只是一个虚拟概念,它可以是一些绘制操作的集合,还不一定要真正绘制到 Canvas 上。
合成(Composite),也叫光栅化,它的过程是按照图层的 z-order 排序,依次将图层绘制到同一个画布 Canvas 上,把图层合成像素。Canvas 可以是