目录:
- listview性能优化
2. 图片优化实战
3. 图片优化方案
4. 图片外接纹理方案
5. 页面栈维度内存优化
1. listview性能优化
1.1 现象: listview+大量图片真的是崩溃了
原因: 在图片加载解码完成之前,你无法知道到底将要消耗多少内存,并且大量的图片加载,会导致的解码任务需要产生大量的IO。
解决方案: 官方针对这种情况提供了场景化的处理方式: ScrollAwareImageProvider
void _resolveImage() {
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
context: _scrollAwareContext,
imageProvider: widget.image,
);
final ImageStream newStream =
provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
));
assert(newStream != null);
_updateSourceStream(newStream);
}
@override
void resolveStreamForKey(
ImageConfiguration configuration,
ImageStream stream,
T key,
ImageErrorListener handleError,
) {
if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
return;
}
if (context.context == null) {
return;
}
if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
SchedulerBinding.instance.scheduleFrameCallback((_) {
scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
});
return;
}
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
}
原理: 判断快速滑动, 下载和解码会停止
问题可以使用外界纹理用原生成熟的图片加载库
这点我们同事在实际的业务场景中遇到过,对于列表加载多图,即使划出屏幕的图片组件element被回收,但图片缓存任然累积在内存中,当时引起了大量的OOM,最后通过外界纹理的方案解决了这个问题
1.2 针对 ListView item 中有 image 的情况来优化内存
1.3。listview 源码分析:
首先ListView继承于BoxScrollView继承于ScrollView继承于StatelessWidget
整个Widger的主要嵌套结构就是 ScollView(ListView) -> Scrollable -> Viewport -> SliverList
对于组合类的Widget-ScrollView和Scrollable
我们很清楚,他的mount()过程核心在于updateChild(_child, built, slot)
方法,在第一次构建的时候这个方法会调用子节点的inflateWidget(newWidget, newSlot)
生成对应的Element对象并插入到树中。
渲染类的Widget-ViewportElement
这里执行的是父类RenderObjectElement的mount()
ListView的item的挂载:
ListView的每一个item一定会在某个阶段并入到Element和RenderObject树中
listview复用原理:
在布局过程过程中,不停的布局子节点,直到当前窗口范围被布满或者没有子节点
2. 图片优化实战操作
2.1 检测消耗多余内存的图片
Flutter Inspector:点击 “Highlight Oversizeded Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。
通过下面两张图可以清晰的看出使用“Highlight Oversizeded Images”的检测效果
2.2 看内存大小占用的分析数据图
3. 图片优化方案
3.1 图片加载原理
以NetworkImage为例,我们看一下Flutter中图片的加载过程,首先通过ImageProvider的resolve获取相应的图片资源,得到ImageStream,通过底层进行解码,并生成纹理。ImageState接收到纹理对象绘制图片,上层获取图片纹理后会调用ImageState的SetState方法将纹理对象传给底层Render object,排版完成后图片就会绘制到屏幕。当上层Image Widget被销毁,Image Cache清空时,触发底层纹理的释放
具体步骤如下:
Flutter 的图片加载原理与原生客户端中的图片框架加载原理相似,具体可点击下方大图查看,加载步骤如下:
1、 区分数据来源生成缓存列表中数据映射的唯一key;
2、 通过key读取缓存列表中的图片数据;
3、 缓存存在,返回已存在的图片数据;
4、 缓存不存在,按来源加载图片数据,解码后同步到缓存中并返回;
5、 设置回调监听图片数据加载状态,数据加载完成后重新渲染控件显示图片;
3.1 缓存配置
经过以上源码探索我们发现Flutter本身提供了定制化的内存缓存能力,但内存缓存上限默认是100MB,这样在配置比较低的机器上内存(Flutter+原生)会超出上限产生OOM,所以使用中我们需要获取机器的实际物理内存去重新调整Flutter端的内存缓存限制大小,通过PaintingBinding.instance.imageCache调用的maximumSize和maximumSizeBytes动态设置合理的图片缓存限制避免因图片内存占用过多导致OOM
3.1.1 磁盘缓存: 针对网络图片我们可能还需要使用一层额外的磁盘缓存。需要注意的是,官方提供的NetworkImage是没实现磁盘缓存的。
3.2 图片预加载
数据预加载:如果使用的图片资源是一些异步获取的数据,可以考虑是不是可以提前获取相关的数据,在要使用的时候,再拿过来使用。利用空闲资源,提前获取加载所需关键数据。
图片预加载机制:precacheImage,在合适的时机提前使用precacheImage对需要展示的图片数据进行预加载到内存中,这样在真正展示的时候,图片已经被加载到内存了,就可以在内容加载时达到“直出”的效果。
延时加载:在很多场景中,如酒店列表,酒店详情头部轮播图,第一次只需要加载首屏内的数据,就可以对非首屏的数据进行延迟加载,避免加载瞬时资源竞争,优先保证重要资源的加载,实现良好的加载体验
3.3 图片资源优化, 压缩
图片资源处理,图片压缩,图片格式建议优先使用webp格式,Flutter中原生支持webp图片格式。
CDN优化是另一个非常重要的方面,主要是在资源层面,最小化传输图片大小,最快响应图片请求,最优化图片选择,支持网络图片大小裁剪,根据实际的需要,加载对应的图片,比如大的头图和小的缩略图,根据具体的场景,加载裁剪之后的不同的图片资源。
3.4 图片内存优化
外接纹理: 共享内存:打通Native内存数据,保证同样的数据在内存中只保留一份,避免重复加载造成的内存开销。使用磁盘缓存,这样既可以增大缓存的数据量,同时通过磁盘,Native和Flutter又可以共享一份数据,极大的减少了内存占用,保证了内存平稳运行。
4. 图片外接纹理方案
图片方案是自研的外接纹理方案
虽然实现了一个App内一个内存缓存,并且将纹理和Flutter图片都存进去了,节省了内存空间,提高了内存使用率,但还是侵入了ImageCache源码,后续flutter engine的升级和代码维护,需要有额外的工作。
为什么flutter图片加载要采用外接纹理方案?
其实Flutter本身已具备加载图片的能力,Image组件就满足网络图片、本地图片、文件图片的加载。那为什么我们还需要实现其他图片加载方案呢?其实是因为Flutter图片组件功能上存在一些缺陷:
- 图片缓存没有持久化能力,无网环境下不支持显示图片。
- 文件图片与原生环境不共用,导致图片资源文件重复。
我们不仅实现图片方案的本地能力复用,而且还能实现视频能力的纹理外接
内存性能不足
从整个APP的视角来说,采用原生图片方案的情况下,其实我们维护了两个大的缓存池:一个是Native的图片缓存,一个是Flutter侧的图片缓存。两个缓存无法互通,这无疑是一个巨大的浪费。特别是对内存的峰值内存性能产生了非常大的压力。
调研梳理所有优化方向,确定图片压缩、更换图片组件、降低页面刷新次数三个方向
5. 页面栈维度内存优化
用户长时间的浏览操作,在不同的页面之间穿梭,少不了持续不断的 push 页面到页面栈,随着页面不断地增加,内存也在持续增长。我们不得不考虑在页面栈的维度去做内存优化。
在原来的页面栈基础上,我们只需要保留顶层两个页面,第三层及以下的页面全部都被销毁回收内存。这种模式下,用户不断的打开新页面,内存也不会有明显的增长。
当新打开一个页面,原来第二层的页面被执行销毁,回收该页面的所有内存
当然,针对 KeepAlive 的页面,我们仍然可以执行对该页面图片缓存的强制清理。