1、Flutter列表中多图加载的问题
在社交或者资讯类App中,经常会使用到Feeds流页面,Feeds流页面的列表一般结构繁杂,单行资讯内就可能有多张图片;
Flutter 通过 Image.asset,Image.file,Image.network 等方法创建一个 Image Widget 来加载显示本地或者网络图片。
而使用Image控件加载多图,会出现一些问题
- 内存过载:列表中图片过多时,内存占用很轻松的飙升到了七八百MB,如果手机的配置不够,很可能就会导致页面空白甚至是Crash;
- 内存峰值高:图片加载的过程中,加载前期阶段会内存峰值很高;
- 没有磁盘缓存:Flutter原生的图片缓存机制,缓存到的是内存中,没有磁盘缓存,每次重新打开APP或者缓存被清理都会再次进行网络请求。
2、Flutter图片加载过程
Flutter中图片主要有4个类:
- Image :显示图⽚的Widget,通过ImageState管理ImageProvider的⽣命周期;
- ImageProvider:图⽚的抽象概念(如NetworkImage、FileImage等),约定图⽚唯⼀性(key)、获取图⽚字节数据(load),创建ImageStream⽤于监听结果;
- ImageStream:图⽚的加载对象,通过 ImageStreamCompleter 最后会返回⼀个 ImageInfo,⽽ImageInfo 中的ui.Image是RenderObject的⽬标绘制对象;
- ImageCache:缓存单例PaintingBinding.instance.imageCache,只用于内存缓存;
加载流程:
- Image 通过 ImageProvider 得到 ImageStream 对象
- _ImageState 利用 ImageStream 添加监听,等待图片数据
- .ImageProvider 通过 load 方法去加载并返回 ImageStreamCompleter 对象
- ImageStream 会关联 ImageStreamCompleter
- ImageStreamCompleter 会通过 http 下载图片,再经过 PaintingBinding 编码转化后,得到 ui.Codec 可绘制对象,并封装成 ImageInfo 返回
- ImageInfo 回调到 ImageStream 的监听,设置给 _ImageState build 的 RawImage 对象。
- RawImage 的 RenderImage 通过 paint 绘制 ImageInfo 中的 ui.Codec
3、内存大的原因
图片要显示在移动终端一般会经历加载、解码和渲染三个步骤,其中解码阶段是内存消耗最多的过程,解码是一个计算量较大的任务,主要需要CPU来执行;并且解码出来的图片所占内存与图片的宽高正相关,而与图片原来的大小无关。
我们以Image Widget为例,Image Widget 要显示在屏幕上的时候,需要以Image作为数据源,Image持有的数据DataBuffer是未解码的压缩数据,能节省较多的内存和加快存储。
当DataBuffer数据被赋值给Image Widget时,图像数据会被解码为Image Buffer,变成代表RGB的颜色数据。
解压图片需要的内存算法为:
图⽚所占内存⼤⼩ = 图⽚⻓度(像素)* 图⽚宽度(像素)* ⼀个像素所占内存空间4字节(RGBA)。
当我们业务场景中需要加载一张游戏图片(3000*4000像素大小),解码内存大小占用为 45.77MB。
4、优化措施
根据以上的内存原因,我们可以总结一些在Flutter原生上的优化措施
优化下载项:利用云端压缩缩略图功能,在云端压缩切割图片
降低采样率:设置合适的采样大小,减小解码内存大小。追踪源码,我们发现,cacheWidth和cacheHeight能够影响到ImageDescriptor,可以降低内存
磁盘缓存:通过Chanel桥接,将下载好的图片进行磁盘缓存,Flutter侧进行图片加载的时候,如果内存没有命中,就去磁盘缓存中进行二次搜索。如果都没有命中才会走网络请求。
优化措施后有以下效果
- 减小图片下载量,加快加载速度
- 内存平均水平显著降低
- 磁盘缓存,网络图片无需重复下载
5、依然存在的问题
- 峰值内存高依然存在:原因:先直接decode 原始的image,此时消耗的内存就与图片原始尺寸成正比, 而后再做rasterize时这个时候的内存消耗大小才与设置大小成正比,设置cacheWidth和cacheHeight没有解决峰值问题。
- cacheWidth和cacheHeight设置问题,与原图宽高⽐例不⼀致易出现图⽚模糊、变形(可以等图片下载之后再去设置,这需要hook,去改动framework);
- 图片解码无法复用:dispose之后Flutter 会立刻回收解码后的内存,即 Flutter 仅对图片的原始压缩数据进行存储,并不缓存 pixel buffer,用空间换时间;
- 磁盘缓存的效率问题:由于我们的磁盘缓存文件是通过Channel来通信,而Flutter定义的channel机制,从本质上说是提供了一个消息传送机制,用于图像等数据的传输必然引起内存和CPU的巨大消耗。无论是用ffi还是普通channel传输,都会导致FPS下降;
在Flutter 端做的优化目前看来并没有能支撑我们解决图片引起的OOM问题,对于内存峰值和内存及时释放,Flutter 端都无法给出完美的方案。