我们急需浏览器渲染引擎/Flutter 渲染引擎人才,欢迎大牛们加入我们。
前言Flutter 上的图片内存问题一直饱受诟病,主要原因有两点:
GC: dart 层的 image 对象引用了 native 的 SKImage,导致没有 GC 就不能真正释放 native 内存;
页面写法:可能会导致大量不可见的 image 被 resolve,导致过大的内存峰值;image 可能会被业务错误地引用,不能 GC,导致泄露。
针对问题1,Flutter 官方也在优化,可以检索官方文档“Downward Memory Pressure”。该方案的主要思路是让引擎更加容易触发 Idle GC。
针对问题2,需要由业务去排查优化,但是对于图片引用泄露的问题,目前官方没有很好用的工具,排查起来比较困难。
公众号在之前的文章中推送了一篇关于内存泄漏检测工具的文章,供读者参考。
U4 内核技术团队根据渲染引擎的多年技术积累,设计并在 Hummer 引擎上实现了一种新的图片内存优化方案。该方案无论从降低内存的角度还是从体现效果的角度,相比原 Flutter 引擎都会有大幅提升。本方案能很好地解决目前 Flutter 引擎中存在的图片内存问题。
本文首先对比了 Flutter 引擎与通用的 Web 引擎上的渲染架构的区别;提出了优化方案;最后以实际应用的视频证明了该方案在体验上的提升效果。
注意,由于本文使用了一些常用术语,因此建议读者对 Flutter 渲染引擎有一定的了解,并且了解 Flutter 相关代码。
对比Flutter 使用了直接光栅化的渲染架构,其图片排版,解码和光栅化,相对于通用 Web 渲染引擎的异步光栅化有很大区别。下面对比下网络加载的单帧图片渲染的流程。
下文描述的 Web 渲染引擎是多进程架构,且其线程模型经过简化;Paint 一词沿用了渲染流程中通用的词汇,表示记录绘制指令到 Picture 或 DisplayList。
Flutter 引擎
- Image widget 被 mount 到 element tree 时,就会触发 image resolve 流程,主要是虚线部分。如果 image 已经在 ImageCache 中,虚线部分流程直接跳过。这步主要是要得到解码和 upload 后的 SkImage;
- layout 和 paint 会用到上面的 SkImage,如果 SkImage 没有 ready,就没有排版大小,也不会记录到 Picture 中。详见 RenderImage::_sizeForConstraints 和 paint 方法;
- 光栅化时直接 playback Picture,SkImage 将通过 Skia 调用 GPU API 进行光栅化。
不同于 Flutter 需要下载完 image 才能解码,Web 渲染引擎支持渐进式解码,只要收到大于文件头的部分图片数据就能去解析文件头获取宽高去排版,也能将部分图片数据拿去解码。这里 Paint 时记录的是包括当前 image 原始数据的 SkImage;
Image 所在的 layer 需要光栅化到 Tile,如果 layer 所包含的 Image 不在 ImageDecodeCache 中,就需要先解码和 upload;
layer 光栅化完后,就会将 Tile 合成输出。
优化后的方案中,图片缓存变为两个:
ImageCache:原生 framework 层的 ImageCache 退化为图片原始数据缓存;
ImageDecodeCache:native 层的 MRU cache,缓存解码后的 SkImage,内部又分为 InUseCache 和 PersistentCache。
- 原生方案里加载/解码/upload 三个流程优化为加载/解析文件头两个流程,流程中使用的是 ImageCache;
- layout/paint 流程中用到的是包含原始数据的 SkImage;
- layer 光栅化前会安排 Picture 中可视区域内未被解码的图片去解码,这里会用到 ImageDecodeCache。光栅化时 SkImage 被替换为解码后的 SkImage。
![3137b7e2247f66989521928b2701eccc.png](https://i-blog.csdnimg.cn/blog_migrate/6404ddc81929f9a75e1519f9c56be56d.png)
- 业务无感知:优化方案的主要改动在引擎内部,不会影响业务的写法。对比业界常用的“外接纹理”方案,该方案主要改动是在 framework,业务需要使用特殊的 widget,有一定的适配工作量,而且在 Android 上存在额外的内存占用及兼容性问题;
- 只解码 viewport 区域内的 Image:避免不必要的解码内存;
- 内存优化明显:存在 native 端的 SkImage 的 ImageDecodeCache,可以主动释放,不受 GC 限制。另外,即使有泄露的 image,最终会在 ImageDecodeCache 里被淘汰(LRU),不会一直占用内存;
- 低端机优化:对于低端机型,设置更小的 ImageDecodeCache 上限,同时对 Image 降 size,减少内存占用;
- 更快的 layout/paint:只需要解码文件头,在首帧和快速滚动时效果明显。
我们修改了 Gallery 的中的 Backdrop,增加了更多的图片,并且设置了 Cache 的上限为 30M,以便能更快地看到内存回收的效果。 更快 layout/paint 以下视频是原生的效果。我们可以看到,屏幕滚动过程中会有明显的跳动 。这是由于 Image 被解码或者 Image 被淘汰,导致屏幕内的 Image 没能及时解码,而只显示了图片下面的 text,明显的跳动则是由排版变化导致的。 以下视频是优化后的效果。 我们可以看 到 , 屏幕滚动过程中 完全不会出现跳动 。 这是由于 在滚动过程中,由 于引擎 只解析 Ima g e 文件头,所以能很快参与排版,虽然解码还是有延迟,但是不会有上面跳动的感觉。
内存可控
内存测试是去获取 app 的 meminfo 中的“Gfx dev”的数值,该数据表示 GPU 的内存占用。 测试方法是点击 Backdrop,上下快速滑动,通过watch -n 0.2 "adb shell dumpsys meminfo io.flutter.demo.gallery >> mem.log"
间隔
0.2
秒
去获取
meminfo,然
后获取其中
Gfx d
e
v 的数值。
![e006b8c8217e51c9865a154bb0eba3e5.png](https://i-blog.csdnimg.cn/blog_migrate/6edd34b6138166ed27264aebae8d0911.png)
- 虽然 Cache 设置的 30M,由于 RasterCache,以及 mipmap 和其它 Skia 内部分配的 Buffer,这个值要更大一些。从曲线上看稳定的值应该在60M左右;
- 刚进 Backdrop 时,优化后的 GPU 内存是~8M,原生的 GPU 内存是~32M。这时由于 ListView 解码了多张图片,而优化后的方案只解码了 Viewport 内的两张图片;
- 滑动过程中,Image 内存超过 Cache 上限,两种方案都会去淘汰 Image,优化前的方案由于没有及时 GC,内存峰值超过 100M,后面 GC 后回到 70M 左右,后面有多次类似的波动,而且平均 GPU 内存占用要比优化后的方案大,这使得 OOM 的机率增大;优化后的方案滑动过程中 GPU 内存稳定在~60M,没有明显波动。
我们已经在集团内的客户端上线了这个优化,并且已经有实际的业务场景。后续我们会继续针对实际业务中存在多 FlutterView 这类场景进行深入优化。
UC 内核技术团队,专注渲染引擎&虚拟机技术。作为集团经济体共建 Flutter 的重要参与方,积极拥抱社区,力求给业务带来最大化的价值提升。Hummer 是我们深度定制优化的 Flutter 引擎,融合了团队在 Web 渲染引擎的多年沉淀。我们后续会介绍更多的优化实践,敬请关注。