MOO 音乐是 TME 旗下的新锐音乐服务,其团队是公司内最早实践 Flutter 的先行者之一。本系列文章将提炼 MOO APP 开发中遇到的情况,就 Flutter 内存占用治理方面,分享日常开发的一些基本认知、注意要点、排查方法和优化方案。内存治理篇文章共分上、中、下三篇,本篇为下篇。
五、内存优化策略
1. 图片内存优化
各种导致内存增长的资源中,图片引起的问题是尤为明显和常见的,一张高清图动辄几百K,MOO 音乐很多列表都使用 GIF 动图,大小可以达几MB乃至十几MB,图片所占内存跟图片大小有关,影响更大的是图片缓存尺寸导致的内存增长,一旦出问题就很容易导致 OOM。
要实现图片内存优化,我们从图片加载流程入手,分析可以从哪些处理节点作为优化的切入点,图一是 NetworkImage 的加载过程。
图一
i. 图片缓存尺寸(即解码尺寸)优化
从源码可以看到,Image.network、Image.asset、Image.file、Image.memory 都有执行设置缓存尺寸的 resize,如果没有设置 cacheWidth 和 cacheHeight,默认使用的是图片自身的像素尺寸。
根据框架源码,具体图片所占内存的计算方法为:图片占用内存大小 = 解码宽度像素 * 解码高度像素 * 4,也就是图片解码数据内存占用跟解码面积成正比。
下面做个简单的实验来验证设置缓存尺寸对内存大小的影响程度。
直接加载一张 @3x 尺寸为 2058x1800 的图片。
不设置缓存尺寸引擎会以原图的尺寸作为解码尺寸,也就是 2058x1800,解码内存达到了 18.8MB,如图二所示。
图二
这里的缓存尺寸只设置了 cacheWidth,cacheHeight 会自动根据图片原比例计算得出。
设置了缓存尺寸,图片解码内存占用只有 5.2MB,如图三所示。
图三
那么,缓存尺寸该如何取值呢?
相对屏幕物理尺寸取值,图片尺寸 和 显示逻辑尺寸 * dpr(设备像素比) 取较小者即可。
屏幕逻辑像素和物理像素,以 iPhone 为参考如下:
设计切图尺寸若基于 750 作为 @1x 尺寸基准,如果不设置缓存尺寸,内存将会是设置了缓存尺寸的 3 倍 到 4 倍。
同样,BoxDecoration 的 Image 属性也需要设置缓存尺寸,为了提高开发效率,可考虑封装 Image 组件和 BoxDecoration 组件,实现自动按照布局尺寸去设置缓存尺寸。
ii. 图片资源裁剪
另外,network 图片在产生解码内存之前,会先将图片数据请求下来,获得一份二进制源图数据,即使图片解码完成,这份数据仍然留存在内存里,如图四所示。
图四
可以根据显示尺寸,利用图片服务的裁剪能力对图片尺寸进行裁剪,可以减少这部分的内存占用,也有利于提升加载效率和解码效率。
iii. 将图片缓存到本地
使用 cached_network_image 组件,可以将网络下载下来的图片缓存到本地,大幅度提升二次加载的效率。
iv. 针对 asset 图片做压缩处理
设计师切图一般给到的是 24 位 png 格式高清图片,可以使用 tinypng 工具进行手动压缩也可以使用 tinypng 提供的压缩服务,将 24 位压缩成 8 位以及删除一些不必要的元数据,压缩效果可达 50% 以上,虽是有损压缩,但是视觉上并无明显差异,是被设计师认可的压缩方式。
图五
减少图片数据内存增长,也有利于提升解码效率,还可以减少安装包大小。
v. 调整图片缓存阈值
了解下 ImageCache 对象(PaintingBinding.instance.imageCache):
缓存存储分为三种情况:请求处理中、使用中以及暂未使用图片缓存。
针对 _cache 的部分,内部实现了 LRU 机制,默认 100MB 或 1000 张图 满⾜其⼀,就标记最先缓存的对象给释放其引用。
可针对设备配置,适当降低缓存阈值,有助于降低 OOM 的概率,配合图片本地缓存,浏览体验不会有明显的影响。
vi. 样式图片和内容图片缓存隔离
我们可以将图片分为两大类,样式图片和内容图片:
样式图片:作为 APP 的 UI 风格的构成部分,通常被访问到的频率较高,作为样式的构成,我们一般不希望这种图片的加载存在用户能感知到的延迟,甚至有时候我们会选择提前预加载在缓存中。
内容图片:通常从接口获取,作为内容呈现给用户,用户习惯上可以容忍一定的加载延时。
对样式图片我们需要尽可能将高频访问的图片保留在内存中,而针对内容图片,我们可以选择更实时的方式去清理,然而框架自带的缓存机制对图片缓存的管理是无差别的。
解决方案是改造 ImageCache 类,加一个存储类型_assetsCache,存储 asset 类型图片缓存 ,需要的话也可以支持 LRU,指定缓存大小阈值。
在 _assetsCache 的基础上,我们可以高频的执行 _cache.clear() 来清理不再访问的缓存。
选择触发清理缓存的时机:
- 可以选择页面退出时触发,以及类弹框功能退出时触发。
- 长列表无限加载场景,也可考虑滑窗实时创建和清理,同时在距离滑窗一定范围内对图片进行预加载,提高图片渲染效率。
2. 页面栈维度内存优化
用户长时间的浏览操作,在不同的页面之间穿梭,少不了持续不断的 push 页面到页面栈,随着页面不断地增加,内存也在持续增长。我们不得不考虑在页面栈的维度去做内存优化。
在原来的页面栈基础上,我们只需要保留顶层两个页面,第三层及以下的页面全部都被销毁回收内存。这种模式下,用户不断的打开新页面,内存也不会有明显的增长。
- 当新打开一个页面,原来第二层的页面被执行销毁,回收该页面的所有内存。
图六
- 当页面栈执行了 pop 操作,倒数第三层的页面变成第二层,开始执行页面重建,包括数据请求、Widget 树构建以及图片加载。
图七
- 动态创建销毁页面的的方式,可能会丢失用户交互过程所产生的状态变更,影响用户体验。针对这种情况可以增加支持设置页面是否 KeepAlive,选择性地保留一些不好还原浏览状态的页面。
图八
当然,针对 KeepAlive 的页面,我们仍然可以执行对该页面图片缓存的强制清理。
六、总结
内存排查在引用链上寻找编码问题线索会有一定难度,需要多操作熟悉引用链的一些常见对象和展示规律,也可以尝试引入开源工具或自研工具来提升排查效率。
内存治理无法一蹴而就,需要提升对内存问题的警觉性,在编码细节上多留意强引用的释放时机,业务功能开发完后在转测前后去检查相关引用释放情况,确保避免内存随着浏览时间不断堆积。
MOO音乐的Flutter实战总结之内存治理(上)
MOO音乐的Flutter实战总结之内存治理(中)
QQ音乐招聘Android/ios客户端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com