我在蚂蚁 Codeday#4 广州站的演讲上介绍了为 U4 4.0 新设计的渲染流水线, 我们称为直接合成(Direct Compositing),区别于原生 WebView 同步合成(Synchronous Compositing)的渲染流水线。
新渲染流水线除了更好的系统兼容性和支持独立 GPU 进程外,也希望通过简化合成器架构和支持直接光栅化来提升渲染性能,减少 GPU 内存占用。降低 GPU 内存占用的意义除了减少应用 OOM 崩溃的概率外,也可以降低出现 GL OOM 虽然没有崩溃但是会出现黑屏/黑块的现象。
在渲染流水线中的光栅化这篇文章里,我介绍了各种渲染流水线使用的不同光栅化方式,和它们各自的优劣。在新渲染流水线的实际性能测试中,也基本印证了文中的观点。
直接光栅化对比分块异步光栅化,在动态内容的渲染性能和 GPU 内存占用上会有较为明显的优势,在首屏性能上也有略微的领先,但是对于图层动画特别是惯性滚动动画,也存在一定程度的劣势。在实际的测试中,使用直接光栅化对比使用异步光栅化,测试较为复杂的网页(比如天猫超市),在低端机上的惯性滚动帧率存在一定程度下降。
混合光栅化
为了继续优化低端机的惯性滚动动画性能,我们实验了新的优化方向 —— 混合光栅化。混合光栅化顾名思义就是合成器同时使用直接光栅化和分块异步光栅化,光栅化的决策是以图层为单位,也就是说合成器需要支持不同的图层使用不同的光栅化方式。
通过对部分图层使用异步光栅化的方式,这些图层可以避免在合成输出的线程进行光栅化,合成输出的耗时对比完全直接光栅化会有所减少,同时因为不是所有图层都使用异步光栅化,整体的 GPU 内存占用又优于完全使用异步光栅化的情况。特别是对于存在大量图层的页面,分块缓存占用了较多的 GPU 内存,但实际上大部分图层又比较简单,直接光栅化的开销对比绘制图层的分块来说并不高,对于这种页面,混合光栅化可以在内存占用和性能之间获得一个更好的平衡。
绘制复杂度
为了决定哪些图层使用直接光栅化,哪些图层使用异步光栅化。我们设计了绘制复杂度(Paint Complexity)的概念,所谓绘制复杂度,就是通过对图层 DisplayList 中的绘图指令进行静态分析,计算出一个代表这个 DisplayList 绘制的复杂度值,这个值跟光栅化引擎实际绘制这个 DisplayList 的 CPU 耗时成正比。
通过对 Skia GPU 光栅化绘制耗时的分析,绘制复杂度的算法大致如下:
- 绘图指令的基础绘制复杂度为 2,包括 DrawRect,DrawRRect,DrawTextBlob,DrawPath,DrawImage 等
- 如果包含图片绘制 +1
- 如果包含 Slow Path +1,Slow Path 是指路径包含凹多边形
- 如果受 ClipPath 影响,对上述计算出来的指令绘制复杂度进行倍乘,支持嵌套 ClipPath
- 如果受 SaveLayer 影响,对上述计算出来的指令绘制复杂度进行倍乘,支持嵌套 SaveLayer
图层光栅化决策
有了图层的绘制复杂度,我们就可以再结合图层的其它属性,比如宽高大小来决定这个图层采用何种光栅化的方式。大致的计算方式如下:
- 计算图层的最大可见面积,然后根据最大可见面积和 Viewport 面积的比例,将图层归类到不同层级,比如 <= 1/16,<= 1/8;
- 计算图层最大可见面积和总面积的比值,推导出图层的平均绘制复杂度;
- 为不同层级的图层设定不同的绘制复杂度阈值,如果图层的平均绘制复杂度大于所属层级的阈值,则选择异步光栅化,反之则选择直接光栅化;
这样的策略就是为了让绘图指令比较复杂,光栅化耗时较长的图层在 Worker 线程去做光栅化,避免阻塞合成输出的线程。
说明:
最大可见面积为图层宽高和 Viewport 宽高的最小值的乘积,代表在当前的 Viewport 下,图层最大可见的区域面积;
使用上述策略对天猫超市进行实际计算的结果如上图:
- 在首屏有三个图层使用异步光栅化,第一个被顶栏遮盖,第二个是中部的分类选择区域,第三个是“今日疯抢”;
- 在中部的商品显示也有三个图层使用异步光栅化,第一个是顶部输入框的右侧,第二个是顶部的分类标签栏,第三个是超大的商品显示图层;
- 红色遮罩表示图层的平均绘制复杂度超过所在层级阈值的两倍,光栅化的耗时会比较长;
性能测试与分析
光栅化性能
下面的表格显示了在 Google Pixel 上,天猫超市首屏和中部内容区域合成输出一帧的平均耗时(单位毫秒)。包含三种不同光栅化策略的结果,分别是完全直接光栅化(DIRECT),混合光栅化(HYBIRD),完全分块异步光栅化(ASYNC)。
* | Draw (DIRECT) | Flush (DIRECT) | Total (DIRECT) | Draw (HYBIRD) | Flush (HYBIRD) | Total (HYBIRD) | Draw (ASYNC) | Flush (ASYNC) | Total (ASYNC) | ||
---|---|---|---|---|---|---|---|---|---|---|---|
首屏 | 5.6 | 4.6 | 10.2 | 3.5 | 3.0 | 6.5 | 2.1 | 2.7 | 4.8 | ||
中部 | 5.8 | 4 | 9.8 | 2.3 | 2.7 | 5 | 1.2 | 2.9 | 4.1 |
Draw 代表 Display Compositor 绘制一帧 CompositorFrame 的耗时,Skia 执行 2D 绘图指令,生成相应的 GL 指令,Encode 到 Commad Buffer 里面。Flush 代表 GPU Service decode Command Buffer,输出真正的 GL 指令,包括调用 eglSwapBuffers 。Draw 和 Flush 两者分别在不同的线程,可以异步运行,在运行中可能存在部分交叠,所以实际一帧的合成耗时并不等于两者简单的叠加,Total 栏位显示两者之和只是为了方便理解。
从数据我们可以看出,天猫超市页面的绘制复杂度的确很高,如果完全使用直接光栅化,在设备性能不足的情况下,因为一帧的合成输出(包括光栅化)耗时较长,惯性滚动下比较容易出现掉帧。而使用混合光栅化时,合成输出的耗时比较接近分块异步光栅化,对低端机更为友好。
GPU 内存占用
* | DIRECT | HYBIRD | ASYNC |
---|---|---|---|
首屏静止状态 | 110 | 130 | 155 |
快速上下滑动后,中部静止状态 | 120 | 140 | 155 |
快速上下滑动过程峰值 | 120 | 185 | 190 |
说明:
内核运行在单进程模式下,使用 meminfo 查看进程的内存占用,GPU 内存占用可以认为是 Gfx dev 和 GL mtrack 两项之和,因为测试 UI 非常简单,Android UI 本身的 GPU 占用很低(不超过 2m),所以基本可以认为进程的 GPU 内存占用都是内核分配的
分块缓存池的大小跟 WebView 的大小相关,Pixel 的屏幕分辨率是 1920 x 1080,WebView 运行在全屏模式下,大小为 1920 x 1080,根据这个 WebView 大小,我们设定分块缓存池的 Soft Limit 为 60m,如果分块缓存累积超过 Soft Limit,则会马上释放不可见的分块,另外也会周期主动释放超过一段时间不使用的分块。因为每个 WebView 的合成器有自己独立的分块缓存池,如果存在多个可见 WebView,总的分块缓存内存会是所有 WebView 的累加。
在首屏静止状态,我们可以看到直接光栅化的 GPU 占用大概在 110m,从 GPU Profiler 工具可以看到这些内存基本上是由两部分组成:
- 图片解码后上传纹理生成的纹理缓存;
- Skia 内部生成的缓存,Skia 生成的缓存主要是 Atlas 缓存,用于拼接字形和小图标;
混合光栅化在上述的内存分配外,再加上 20m 左右的分块纹理缓存,而异步光栅化则大概分配了 45m 左右的分块纹理缓存。
上下滑动后,在中部静止状态下的内存占用和首屏差不多,混合光栅化仍然保留了 20m 左右的分块纹理缓存,而异步光栅化需要的分块纹理缓存较首屏要少一些,大概 35m 左右。
从上下滑动过程的峰值我们可以看出,混合光栅化的 GPU 内存占用基本跟异步光栅化持平,因为两者的分块纹理缓存分配都达到或者略微超过了分块缓存池的 Soft Limit,主要的原因是天猫超市中部的超大内容图层在混合光栅化下使用了异步光栅化,所以它在滚动时同样需要不断分配新的分块缓存。不过实际的观察发现,混合光栅化达到峰值的频率和停留在峰值的时间会比异步光栅化要少。
从上述的分析我们可以看到混合光栅化对比完全分块异步光栅化在 GPU内存占用上的一些优势:
- 静止状态一般减少 15 ~ 25m 的 GPU 内存占用,这个值取决于页面的图层结构和图层的绘制复杂度,另外 WebView 越大(屏幕分辨率超过 1080p),可见 WebView 越多,这个值就越大(倍乘增长);
- 滚动过程的峰值,虽然在部分情况下,混合光栅化跟异步光栅化差别不大,不过达到峰值的频率和停留在峰值的时间会比异步光栅化要少;
当然,如果采用完全的直接光栅化,GPU 内存优化的结果是非常明显的,特别是在页面惯性滚动过程的峰值状态上。
进阶优化
不可滚动页面
有很多页面是不可滚动的,它们通常用于展现互动内容。混合光栅化主要是为了提升惯性滚动的性能,所以对于不可滚动的页面我们可以设定不使用混合光栅化,全部使用直接光栅化。对于不可滚动的页面,使用直接光栅化有助于节省 GPU 内存供 WebGL,Canvas 这样的 GPU 内存占用大户使用,另外对动态变化的内容,直接光栅化对性能提升也有一定帮助。
图层树简单的页面
如果页面的图层树比较简单,也就是说图层数量比较少并且绝大部分图层的绘制复杂度都比较低。这意味着即使某个图层可能绘制耗时较长,但是总耗时还是比较低的。所以,我们可以采用类似下面针对高性能设备的优化策略来进一步减少 GPU 内存的占用。测试验证对于百度搜索首页和部分新闻/电商二级内容页面的 GPU 内存占用会有比较明显的优化效果,接近完全直接光栅化的水平。
高性能设备
对于高性能的移动设备来说,绘制同样复杂度的图层,绘制的耗时更低。所以我们可以采用以下的一些策略来减少使用异步光栅化,进一步降低 GPU 内存占用,使得绝大部分情况下 GPU 内存占用都接近或者和实现与完全直接光栅化基本一致的结果。
- 提高每个层级的绘制复杂度阈值,比如直接乘以两倍;
- 只对面积较小的图层使用异步光栅化,超过一定面积的图层强制使用直接光栅化,比如限制最大可见面积和 Viewport 面积的比例小于 1/2 的图层才允许选择异步光栅化;