作者:易旭昕 原文链接:https://zhuanlan.zhihu.com/p/354631257 本文由作者授权发布。
写作费时,敬请点赞,关注,收藏三连。
在渲染流水线中的光栅化文章中,我介绍了不同渲染引擎使用的不同光栅化的策略。在 Flutter 的渲染引擎中,使用的是所谓的同步光栅化或者也称为即时光栅化(On Demand),在这种光栅化策略中:
使用间接光栅化的主要目的是通过避免对内容没有发生变化的图层的重复光栅化,来减少每一帧的光栅化耗时。
但是使用间接光栅化也会引起其它的一些副作用:
Flutter 渲染引擎在 RasterCache 中实现了图层的间接光栅化,并且采取了一系列措施来规避和减轻间接光栅化带来的一些副作用,这篇文章的目的就是通过讲解 RasterCache 的实现和 Flutter 渲染引擎对它的使用来帮助读者进一步了解 Flutter 渲染引擎的内部实现细节。
img
Flutter Gallery Demo 显示哪些图层使用了 RasterCache
RasterStatus CompositorContext::ScopedFrame::Raster(
flutter::LayerTree& layer_tree,
bool ignore_raster_cache) {
...
bool root_needs_readback = layer_tree.Preroll(*this, ignore_raster_cache);
...
layer_tree.Paint(*this, ignore_raster_cache);
...
return RasterStatus::kSuccess;
}
上面的代码是 Flutter 光栅化输出一帧代码的简化版本,其实就是图层树的 Preroll 和 Paint。
void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) {
...
SkPicture* sk_picture = picture();
...
if (auto* cache = context->raster_cache) {
SkMatrix ctm = matrix;
ctm.postTranslate(offset_.x(), offset_.y());
cache->Prepare(context->gr_context, sk_picture, ctm,
context->dst_color_space, is_complex_, will_change_);
}
在 PictureLayer 被 Preroll 时就会调用 RasterCache::Prepare。
bool RasterCache::Prepare(GrContext* context,
SkPicture* picture,
const SkMatrix& transformation_matrix,
SkColorSpace* dst_color_space,
bool is_complex,
bool will_change) {
...
if (picture_cached_this_frame_ >= picture_cache_limit_per_frame_) {
return false;
}
if (!IsPictureWorthRasterizing(picture, will_change, is_complex)) {
// We only deal with pictures that are worthy of rasterization.
return false;
}
...
PictureRasterCacheKey cache_key(picture->uniqueID(), transformation_matrix);
// Creates an entry, if not present prior.
Entry& entry = picture_cache_[cache_key];
if (entry.access_count < access_threshold_) {
// Frame threshold has not yet been reached.
return false;
}
if (!entry.image) {
entry.image = RasterizePicture(picture, context, transformation_matrix,
dst_color_space, checkerboard_images_);
picture_cached_this_frame_++;
}
return true;
}
RasterCache::Prepare 做的事情就是检查该图层是否满足使用间接光栅化的条件,如果满足则为该图层分配一个像素缓冲区,并把该图层的 DisplayList 预先绘制到这个像素缓冲区上,供后面使用。
为了规避或者减轻间接光栅化带来的一些副作用,RasterCache 设置了一系列条件来检查图层是否满足间接光栅化的条件,包括:
图层间接光栅化后的像素缓冲区被一个 Map 持有,以 PictureRasterCacheKey 作为 Key,从代码中我们可以知道 PictureRasterCacheKey 由 SkPicture 的 UniqueID 和图层的最终变换矩阵组成(图层自身变换矩阵和祖先图层变换矩阵的叠加),不过这个变换矩阵在生成最终 Key 值时会将平移分量置空。这意味这如果图层的内容发生变化(SkPicture 的 UniqueID 发生变化),或者图层的最终变换矩阵的非平移分量(比如旋转或者缩放)发生变化,图层之前生成的像素缓冲区都会失效,需要重新光栅化,如果只是平移则缓存一直有效。
bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const {
PictureRasterCacheKey cache_key(picture.uniqueID(), canvas.getTotalMatrix());
auto it = picture_cache_.find(cache_key);
if (it == picture_cache_.end()) {
return false;
}
Entry& entry = it->second;
entry.access_count++;
entry.used_this_frame = true;
if (entry.image) {
entry.image->draw(canvas);
return true;
}
return false;
}
如果绘制该图层时,在 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 内存的占用,愿意以部分复杂场景的性能可能略微下降为代价,可以修改的地方包括:
你可能还喜欢