外接纹理显示图片1:问题篇

外接纹理显示图片1:内存问题

Posted by Ted on June 9, 2022

1、Flutter列表中多图加载的问题

在社交或者资讯类App中,经常会使用到Feeds流页面,Feeds流页面的列表一般结构繁杂,单行资讯内就可能有多张图片;

Image

Flutter 通过 Image.asset,Image.file,Image.network 等方法创建一个 Image Widget 来加载显示本地或者网络图片。

而使用Image控件加载多图,会出现一些问题

  • 内存过载:列表中图片过多时,内存占用很轻松的飙升到了七八百MB,如果手机的配置不够,很可能就会导致页面空白甚至是Crash;
  • 内存峰值高:图片加载的过程中,加载前期阶段会内存峰值很高;
  • 没有磁盘缓存:Flutter原生的图片缓存机制,缓存到的是内存中,没有磁盘缓存,每次重新打开APP或者缓存被清理都会再次进行网络请求。

img

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,只用于内存缓存;

加载流程:

  1. Image 通过 ImageProvider 得到 ImageStream 对象
  2. _ImageState 利用 ImageStream 添加监听,等待图片数据
  3. .ImageProvider 通过 load 方法去加载并返回 ImageStreamCompleter 对象
  4. ImageStream 会关联 ImageStreamCompleter
  5. ImageStreamCompleter 会通过 http 下载图片,再经过 PaintingBinding 编码转化后,得到 ui.Codec 可绘制对象,并封装成 ImageInfo 返回
  6. ImageInfo 回调到 ImageStream 的监听,设置给 _ImageState build 的 RawImage 对象。
  7. RawImage 的 RenderImage 通过 paint 绘制 ImageInfo 中的 ui.Codec

img

3、内存大的原因

图片要显示在移动终端一般会经历加载、解码和渲染三个步骤,其中解码阶段是内存消耗最多的过程,解码是一个计算量较大的任务,主要需要CPU来执行;并且解码出来的图片所占内存与图片的宽高正相关,而与图片原来的大小无关。

img

我们以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侧进行图片加载的时候,如果内存没有命中,就去磁盘缓存中进行二次搜索。如果都没有命中才会走网络请求。

img

优化措施后有以下效果

  • 减小图片下载量,加快加载速度
  • 内存平均水平显著降低
  • 磁盘缓存,网络图片无需重复下载

5、依然存在的问题

  • 峰值内存高依然存在:原因:先直接decode 原始的image,此时消耗的内存就与图片原始尺寸成正比, 而后再做rasterize时这个时候的内存消耗大小才与设置大小成正比,设置cacheWidth和cacheHeight没有解决峰值问题。
  • cacheWidthcacheHeight设置问题,与原图宽高⽐例不⼀致易出现图⽚模糊、变形(可以等图片下载之后再去设置,这需要hook,去改动framework);
  • 图片解码无法复用:dispose之后Flutter 会立刻回收解码后的内存,即 Flutter 仅对图片的原始压缩数据进行存储,并不缓存 pixel buffer,用空间换时间;
  • 磁盘缓存的效率问题:由于我们的磁盘缓存文件是通过Channel来通信,而Flutter定义的channel机制,从本质上说是提供了一个消息传送机制,用于图像等数据的传输必然引起内存和CPU的巨大消耗。无论是用ffi还是普通channel传输,都会导致FPS下降;

在Flutter 端做的优化目前看来并没有能支撑我们解决图片引起的OOM问题,对于内存峰值和内存及时释放,Flutter 端都无法给出完美的方案。