写作费时,敬请点赞,关注,收藏三连。
Flutter 渲染引擎详解系列文章
Flutter 渲染引擎详解 - iOS Metal 篇
Flutter 渲染引擎详解 - iOS GL 篇
Flutter 渲染引擎详解 - Android GL 篇
在渲染流水线中的光栅化文章中,我介绍了不同渲染引擎使用的不同光栅化的策略。在 Flutter 的渲染引擎中,使用的是所谓的同步光栅化或者也称为即时光栅化(On Demand),在这种光栅化策略中:
- 以直接光栅化为主,图层的 DisplayList 直接绘制到目标 Surface 上,光栅化生成的像素值直接写入目标 Surface 的像素缓冲区;
- 部分图层会触发间接光栅化,渲染引擎会为这些图层分配额外的像素缓冲区,先将该图层的 DisplayList 绘制到图层本身的像素缓冲区,然后在绘制该图层时,再将图层的像素缓冲区输出到目标 Surface 的像素缓冲区;
使用间接光栅化的主要目的是通过避免对内容没有发生变化的图层的重复光栅化,来减少每一帧的光栅化耗时。
- 虽然渲染引擎已经支持 GPU 光栅化,绘制一个比较复杂的 DisplayList,执行每一条绘图指令,将其转换成对应的 GPU 绘图指令,仍然需要一定的 CPU 耗时,而输出一个像素缓冲区则只需要一条绘图指令,自然后者 CPU 耗时更少;
- 如果 DisplayList 中绘图指令的绘制区域有较多的相互覆盖,采用间接光栅化也有助于减少 Overdraw,从而减少 GPU 的开销,特别是存在半透明混合的时候;
但是使用间接光栅化也会引起其它的一些副作用:
- 图层在当前帧触发间接光栅化,实际会增加当前帧的绘制开销,减少的是后续帧的开销(后续帧绘制该图层只需要输出一个像素缓冲区);
- 如果图层的内容频繁发生变化,采用间接光栅化反而会增加每一帧的绘制开销;
- 间接光栅化需要为图层分配额外的像素缓冲区,增加了 GPU 内存的占用;
Flutter 渲染引擎在 RasterCache 中实现了图层的间接光栅化,并且采取了一系列措施来规避和减轻间接光栅化带来的一些副作用,这篇文章的目的就是通过讲解 RasterCache 的实现和 Flutter 渲染引擎对它的使用来帮助读者进一步了解 Flutter 渲染引擎的内部实现细节。
Flutter Gallery Demo 显示哪些图层使用了 RasterCache
RasterCache
RasterStatus
上面的代码是 Flutter 光栅化输出一帧代码的简化版本,其实就是图层树的 Preroll 和 Paint。
void
在 PictureLayer 被 Preroll 时就会调用 RasterCache::Prepare。
bool
RasterCache::Prepare 做的事情就是检查该图层是否满足使用间接光栅化的条件,如果满足则为该图层分配一个像素缓冲区,并把该图层的 DisplayList 预先绘制到这个像素缓冲区上,供后面使用。
为了规避或者减轻间接光栅化带来的一些副作用,RasterCache 设置了一系列条件来检查图层是否满足间接光栅化的条件,包括:
- 每一帧最多只允许一定数量的图层完成间接光栅化(picture_cache_limit_per_frame_),默认是 3个,超过该数目后,该帧不再允许间接光栅化,从而避免对该帧的性能产生太大的影响;
- IsPictureWorthRasterizing 主要用于规避绘制指令比较简单的图层,内容会发生变化的图层,不可见的图层走间接光栅化,减少不必要的间接光栅化;
- access_threshold 进一步限制了只有图层的内容在连续多帧绘制中都没有发生变化,才允许图层间接光栅化,默认值为 3,进一步减少了不必要的间接光栅化;
图层间接光栅化后的像素缓冲区被一个 Map 持有,以 PictureRasterCacheKey 作为 Key,从代码中我们可以知道 PictureRasterCacheKey 由 SkPicture 的 UniqueID 和图层的最终变换矩阵组成(图层自身变换矩阵和祖先图层变换矩阵的叠加),不过这个变换矩阵在生成最终 Key 值时会将平移分量置空。这意味这如果图层的内容发生变化(SkPicture 的 UniqueID 发生变化),或者图层的最终变换矩阵的非平移分量(比如旋转或者缩放)发生变化,图层之前生成的像素缓冲区都会失效,需要重新光栅化,如果只是平移则缓存一直有效。
bool
如果绘制该图层时,在 RasterCache 有存在事先绘制的像素缓冲区,则直接输出该像素缓冲区(entry.image->draw(canvas)),如果没有,则直接光栅化图层的 DisplayList。
一些特定的图层比如 OpacityLayer 跟普通的 PictureLayer 不同,它不需要进行任何检查,直接走间接光栅化,而后续图层绘制的时候只需要设置不同的 alpha 值到输出的 Canvas,然后再绘制事先准备好的像素缓冲区即可。
即使规避了不必要的间接光栅化,但是只要使用间接光栅化就需要分配额外的光栅化缓存,所以尽快释放不再需要的缓存可以有效减少 Flutter 渲染引擎的 GPU 内存占用。Flutter 主要使用了如下策略来释放间接光栅化分配的像素缓存。
为每个缓存的 Entry 增加 used_this_frame 标记,用来表示该 Entry 有没有在该帧被使用,如果没有则在绘制完该帧后立即释放 Entry,也就是说一个分配了间接光栅化缓存的图层如果在当前帧没有参与绘制,那它的缓存就会马上被释放。
虽然 RasterCache 释放了 Entry 和它的 SkImage,但是 SkImage 真正的 Backing Store,GrGpuResource 并没有立即从 Skia 内部的 GrResourceCache 中释放,这也意味着分配的 GPU 内存并没有真正释放,这主要是为了让该 GPU 资源可以被重用,避免频繁重复分配和释放。所以 Flutter 在每绘制完一帧后,都会要求 GrResourceCache 释放超过 15 秒闲置的已经被回收的 GrGpuResource,也就是说如果一个缓存被 RasterCache 释放,并且超过 15 秒都没有被重用,那它分配的 GPU 内存就会真正被释放。
目前 Flutter 设置一个 Engine 对应的 GrResourceCache 上限为该 Engine 对应 FlutterView 的面积的 12 倍,如果一个 1920x1080 大小的 FlutterView,GrResourceCache 的上限差不多就是 95m。一般来说 RasterCache 大部分情况都不会触及这个上限,除非应用的 UI 复杂度非常高,在短时间触发了大量的间接光栅化缓存的分配。总的来说 Flutter RasterCache 的机制设计的还是比较完善的。
如果第三方需要在此基础上进一步减少 GPU 内存的占用,愿意以部分复杂场景的性能可能略微下降为代价,可以修改的地方包括:
- 提高图层间接光栅化的门槛,进一步减少间接光栅化的图层,比如要求 DisplayList 包含更多绘图指令的图层才走间接光栅化,默认是绘图指令大于 5,可以改成 8 或者 10,或者加上图层面积作为衡量因素,面积越大要求的绘图指令数越高;
- 加快释放 GrResourceCache,比如将 15 秒闲置改成 10 秒闲置或者 5 秒闲置;