2.遥遥领先. Flutter性能优化系列|一文教你完全掌握图片优化

目录:
  1. listview性能优化

2. 图片优化实战

3. 图片优化方案

4. 图片外接纹理方案

5. 页面栈维度内存优化


图片缓存策略.png

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 的情况来优化内存

代码.jpg
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 页面中找到它。

工具的使用1.jpg

通过下面两张图可以清晰的看出使用“Highlight Oversizeded Images”的检测效果

工具的使使用2.jpg

2.2 看内存大小占用的分析数据图

图片内存分析.jpg

3. 图片优化方案

3.1 图片加载原理

以NetworkImage为例,我们看一下Flutter中图片的加载过程,首先通过ImageProvider的resolve获取相应的图片资源,得到ImageStream,通过底层进行解码,并生成纹理。ImageState接收到纹理对象绘制图片,上层获取图片纹理后会调用ImageState的SetState方法将纹理对象传给底层Render object,排版完成后图片就会绘制到屏幕。当上层Image Widget被销毁,Image Cache清空时,触发底层纹理的释放

图片框架源码.jpg

具体步骤如下:

Flutter 的图片加载原理与原生客户端中的图片框架加载原理相似,具体可点击下方大图查看,加载步骤如下:

1、 区分数据来源生成缓存列表中数据映射的唯一key;

2、 通过key读取缓存列表中的图片数据;

3、 缓存存在,返回已存在的图片数据;

4、 缓存不存在,按来源加载图片数据,解码后同步到缓存中并返回;

5、 设置回调监听图片数据加载状态,数据加载完成后重新渲染控件显示图片;

图片缓存策略.png

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. 图片外接纹理方案

图片方案是自研的外接纹理方案


外接纹理.jpg

虽然实现了一个App内一个内存缓存,并且将纹理和Flutter图片都存进去了,节省了内存空间,提高了内存使用率,但还是侵入了ImageCache源码,后续flutter engine的升级和代码维护,需要有额外的工作。

为什么flutter图片加载要采用外接纹理方案?

其实Flutter本身已具备加载图片的能力,Image组件就满足网络图片、本地图片、文件图片的加载。那为什么我们还需要实现其他图片加载方案呢?其实是因为Flutter图片组件功能上存在一些缺陷:

  • 图片缓存没有持久化能力,无网环境下不支持显示图片。
  • 文件图片与原生环境不共用,导致图片资源文件重复。

我们不仅实现图片方案的本地能力复用,而且还能实现视频能力的纹理外接

内存性能不足

从整个APP的视角来说,采用原生图片方案的情况下,其实我们维护了两个大的缓存池:一个是Native的图片缓存,一个是Flutter侧的图片缓存。两个缓存无法互通,这无疑是一个巨大的浪费。特别是对内存的峰值内存性能产生了非常大的压力

调研梳理所有优化方向,确定图片压缩、更换图片组件、降低页面刷新次数三个方向

5. 页面栈维度内存优化

栈图片释放.png

用户长时间的浏览操作,在不同的页面之间穿梭,少不了持续不断的 push 页面到页面栈,随着页面不断地增加,内存也在持续增长。我们不得不考虑在页面栈的维度去做内存优化。

在原来的页面栈基础上,我们只需要保留顶层两个页面,第三层及以下的页面全部都被销毁回收内存。这种模式下,用户不断的打开新页面,内存也不会有明显的增长。

当新打开一个页面,原来第二层的页面被执行销毁,回收该页面的所有内存

当然,针对 KeepAlive 的页面,我们仍然可以执行对该页面图片缓存的强制清理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。