绘制流程

LCUI 的绘制流程是由脏矩形驱动的,窗口尺寸变化和组件的样式变化都会产生脏矩形,这些脏矩形主要集中在样式计算阶段和布局阶段产生,当脏矩形记录不为空时就会进入绘制流程,绘制流程由以下步骤组成:

  • 开始绘制:根据当前所在窗口和脏矩形,创建一个绘制上下文,包含绘制区域和充当画布的图形对象,其中图形对象引用自帧缓冲,而帧缓冲与应用程序窗口绑定,对该图形对象写入像素数据会同步到窗口中。

  • 绘制组件树:以根组件为起点,递归向下遍历整个组件树中的组件,将所有出现在脏矩形内的组件都绘制到画布中。

  • 结束绘制:销毁绘制上下文,释放相关资源。

所有脏矩形都重绘完后,LCUI 会调用系统提供的窗口 API 将帧缓冲的内容写入到窗口内以让窗口呈现最新内容。接下来我们将深入了解与绘制流程相关的功能和工作原理。

脏矩形

相较于画面更新频繁且内容很多的图形游戏,普通应用的界面更新频率要低得很多,而更新的内容通常也不是很多,甚至只是一个小区域(例如:按钮),如果像游戏一样强制重绘的话会造成资源浪费,对用户而言最明显的感受就是风扇转速和电量消耗速度都很快。脏矩形技术正是利用只更新变化区域来达到提高绘制效率的目的。在脏矩形系统中,屏幕上更新的区域被称为“脏矩形”,绘制引擎仅对脏矩形部分重绘,而其他部分保持原样。

出于性能和内存开销上的考虑,LCUI 中的脏矩形系统采用如下设计:

  • 每个组件中都有一个脏矩形类型标识和记录,其中类型标识有五个值:nonecustompadding-boxborder-boxcanvas-box,当值为 custom 时使用脏矩形记录中的准确数据。

  • 在样式计算阶段和布局阶段,组件自身的位置、尺寸、透明度、边框、背景等视觉样式发生变化时会更新脏矩形类型标识。如果组件有自己的绘制逻辑,则会通过调用相关函数来更新脏矩形记录。

  • 在脏矩形收集阶段,整个过程是从根组件开始向下遍历整个组件树,组件的脏矩形仅在不被父组件脏矩形包含且在可视区域内时才被收集;收集脏矩形时如果与已有的脏矩形相重叠,则会将它们合并成一个;收集完后重置组件的脏矩形记录。

这种设计的好处是在判断脏矩形是否存在和是否重叠时,只需要简单的比较标识值的大小即可,也就是将矩形间的 xywidthheight 的复杂比较简化成单个值的比较,极大的提升了脏矩形收集、去重和合并性能。

并行绘制

绘制本质上是将计算结果写入内存中,这些任务相互独立,可以分配给多个 CPU 核心同时执行以提升绘制效率。

LCUI 采用的策略如下:

  • 根据屏幕尺寸和预设的并行渲染线程数量,将屏幕区域从上到下划分成多个小区域。

  • 分别为这些小区域收集脏矩形,当已收集的脏矩形总面积超过区域面积的 80% 时,将整个区域作为脏矩形,不再逐个重绘脏矩形。

  • 当脏矩形总面积超过两个区域面积时,开启并行绘制。这样做是因为并行特性本身也有性能开销,如果计算量过少的话,很容易会导致性能降低。

如下图所示,整个屏幕区域按照预设的并行渲染线程数量分成了四个部分,其中第一部分和第三部分区域的脏矩形总面积较少,采用的是局部渲染方案,仅对脏矩形进行重绘;第二部分区域内的脏矩形总面积较大,采用的是全量渲染方案,整个区域都会被重绘;第四部分区域由于没有脏矩形,所以不渲染。

图层

图层是一个用于存放临时绘制内容的图形对象,LCUI 在绘制组件时会用到以下图层:

  • 根图层:该图层指向应用程序窗口的帧缓冲,绘制根组件时使用的就是该图层。

  • 上级图层:绘制父组件时所使用的图层,它引用自父组件的内容图层或上级图层。

  • 自身图层:用于存放组件自身的绘制结果。

  • 内容图层:用于存放用于存放所有在内容区域内可见的子组件的绘制结果。

  • 合成图层:用于存放组件自身图层和组件内容图层的混合结果。

这些图层的使用规则如下:

  • 组件无可绘制样式:不使用图层。

  • 组件是自定义组件或有背景色、背景图、边框、阴影样式:创建自身图层,然后基于该图层创建绘制上下文,让该组件的绘制结果都会输出到该图层中。

  • 组件有圆角边框:创建内容图层,在绘制子组件时基于该图层创建绘制上下文,让所有子组件的绘制结果输出到该图层中。在绘制完所有子组件后,根据圆角边框样式对内容图层进行裁剪,也就是将溢出圆角边框外的像素都清除掉。

  • 组件透明度小于 1:创建自身图层和内容图层,在两个图层都绘制完后,复制自身图层作为合成图层,然后将内容图层混合至该图层中,最后按照组件透明度将混合图层混合到上级图层中。

绘制上下文

绘制上下文在组件绘制前创建,它所包含的绘制缓冲和绘制区域会受到组件样式和父组件绘制上下文的影响,其中绘制缓冲只是个图形对象引用,它的引用对象有:根图层、父组件内容图层、上级图层,而绘制区域则是取自脏矩形与组件边界框重叠的区域。

绘制组件

我们从一个简单的例子来讲解组件的绘制流程。首先,屏幕上有以下三个组件:

  • Avatar:占用屏幕区域 (20, 20, 80, 80)

  • Text:占用屏幕区域 (120, 20, 250, 80)

  • Menu:占用屏幕区域 (60, 60, 140, 160)

然后我们将 Menu 组件的透明度改为 0.5,由于透明度影响整个组件的视觉效果,因此整个组件的区域都会标记为脏矩形,如下图中的蓝色区域所示:

我们可以看出脏矩形包含 Menu 且与 Avatar 和 Text 重叠,这意味着它们与脏矩形重叠的部分都需要重绘,且由于 Avatar 和 Text 都在 Menu 底下,Avatar 和 Text 的绘制顺序会先于 Menu。由此我们可以推测出大致的绘制流程:

  1. 绘制根组件:将组件内的区域 (60, 60, 140, 160) 绘制到屏幕区域 (60, 60, 140, 160)。

  2. 绘制 Avatar:将组件内的区域 (40, 40, 40, 40) 绘制到屏幕区域 (60, 60, 40, 40)。

  3. 绘制 Text:将组件内的区域 (0, 40, 60, 40) 绘制到屏幕区域 (60, 60, 40, 40)。

  4. 绘制 Menu:将组件内的区域 (0, 0, 140, 160) 绘制到屏幕区域 (60, 60, 140, 160)。

绘制过程中的图层使用情况如下:

  • 根组件:不使用图层,因为根组件没有任何需要绘制的内容。

  • Avatar:使用自身图层和内容图层,因为有圆角边界。

  • Text:使用自身图层,因为需要填充背景色。

  • Menu:使用自身图层、内容图层和合成图层,因为有阴影、透明度小于 1。

最后更新于