前言
随着 Flutter 稳固版本逐渐迭代更新,京东 APP 外部的 Flutter 业务也日益增多,Flutter 开发为咱们提供了高效的开发环境、优良的跨平台适配、丰盛的性能组件及动画、靠近原生的交互体验,但随之也带来了一些 OOM 问题,通过线上监控信息和 Observatory 工具联合剖析咱们发现问题的起因是因为 Flutter 页面中加载的大量图片导致的内存溢出,这也是在原生开发中常见的问题之一,Flutter 官网为咱们提供的 Image widget 实现图片加载及显示,只有理解 Flutter 中图片的加载原理及图片内存治理形式能力真正发现问题的实质,本文将重点介绍 Flutter 中图片的加载原理,应用过程中有哪些须要留神的中央及优化思路和伎俩,心愿能给大家带来一些启发和帮忙。
根本应用
上面是 Image 的根本应用办法,image 参数是 Image 控件中的必选参数,也是数据源类型能够是 Asset、网络、文件、内存,上面将以咱们罕用的网络图片加载形式为例子解说原理,根本应用如下:
Image(
image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
width: 100.0,
heitht: 100.0
)
Image 控件的应用办法这里就不在开展了,控件的参数及 API 详情请参阅:https://api.flutter.dev/flutter/widgets/Image-class.html
图片加载流程
Flutter 的图片加载原理与原生客户端中的图片框架加载原理类似,具体可点击下方大图查看,加载步骤如下:
1、辨别数据起源生成缓存列表中数据映射的惟一 key;
2、通过 key 读取缓存列表中的图片数据;
3、缓存存在,返回已存在的图片数据;
4、缓存不存在,按起源加载图片数据,解码后同步到缓存中并返回;
5、设置回调监听图片数据加载状态,数据加载实现后从新渲染控件显示图片;
大家可能留神到了下面流程图中的文件缓存局部是灰色的,目前官网还不反对此性能,上面咱们会通过源码逐渐剖析加载流程及如何通过批改源码补全文件缓存性能。
源码剖析
上面将通过流程图联合 UML 类图剖析图片加载流程:
这个 UML 类图看起来略微有点儿简单,但认真看会发现已将图片数据加载流程分成几大模块,上面将依照模块进行逐渐剖析,上面将以网络图片加载形式为例解说外围类和外围办法性能。
外围类及办法介绍
启动缓存相干类
PaintingBinding:图片缓存类和着色器预加载,该类是基于框架的应用程序启动时绑定到 Flutter 引擎的胶水类,在启动入口 main.dart 的 runApp 办法中创立 WidgetsFlutterBinding 类时被初始化的,通过笼罩父类的 initInstances() 办法初始化外部的着色器预加载(Skia 第一次在 GPU 上绘制须要编译相应的着色器,这个过程大略 20ms~200ms)及图片缓存等,图片缓存以单例的形式(PaintingBinding.instance.imageCache)对外提供办法应用,也就是说这个图片缓存在 APP 中是全局的,并在这个类中还提供了图像解码(instantiateImageCodec)、缓存革除(evict)等性能。
ImageCache: 图片缓存类,默认提供缓存最大个数限度 1000 个对象和最大容量限度 100MB,因为图片加载过程是一个异步操作,所以缓存的图片分为三种状态:已应用、已加载、未应用,别离对应三个图片缓存列表,当图片列表超限时会将图片缓存列表中最近起码应用图片进行删除,缓存列表别离是:沉闷中图片缓存列表(\_cache)、已加载图片缓存列表 (\_pendingImages)、未沉闷图片缓存列表 (_liveImages),并对外提供以下办法:获取缓存(putIfAbsent)、清空缓存(clear、clearLiveImages)、驱赶单个图片(evict)、最大缓存个数限度(maximumSize)、最大缓存大小限度(maximumSizeBytes)等办法。
从源码中咱们能够看到缓存列表是 Map 类型,Flutter 中的 Map 创立的对象是 LinkedHashMap 是有序的,按键值插入程序迭代,Flutter 应用 LinkedHashMap 存储图片数据并实现相似 LRU 算法的缓存,当缓存列表中的图片被应用后会将图片数据从新插入到缓存列表的开端,这样最近起码应用的图片始终会被放在列表的头部。
当缓存列表减少图片数据后,会通过最大缓存个数和最大缓存大小两个纬度进行查看缓存列表是否超限,若存在超限状况则通过 Map 的 keys.first 办法获取缓存列表头部最近起码应用的图片对象进行删除,直到满足缓存限度。
启动缓存小结:
Flutter 启动后在 PaintingBinding 中创立 ImageCache 缓存,图片缓存是全局的并以单例形式对外提供应用办法,缓存默认最大个数限度 1000 个对象、最大容量限度 100MB,缓存中的 Map 列表通过 key/value 形式存储图片信息,并通过 keys.first 办法实现的相似 LRU 算法治理图片缓存列表,对外提供 putIfAbsent() 办法获取已缓存图像,若缓存中不存在则通过回调图片加载类中的 load() 办法加载图片数据,另外图片缓存中还提供 clear() 和 evict() 办法用来删除缓存。
图片数据加载相干类
ImageProvider: 图片数据提供抽象类,该类定义了图像数据解析办法(resolve)、惟一 key 生成办法(obtainKey)、数据加载办法(load),obtainKey 和 load 办法均由子类实现,obtainKey 办法生成的对象用于内存缓存的 key 值应用,load 办法将依照不同数据源加载图像数据,罕用的 Provider 子类有:NetworkImage、AssetImage、FileImage、MemoryImage,咱们能够看到 resolve 办法返回的是图片加载对象类(ImageStream),load 办法返回的是 ImageStreamCompleter 类用来治理图像加载状态及图像数据(ImageInfo)。
ImageStreamCompleter: 是一个抽象类,用于治理加载图像对象(ImageInfo)加载过程的一些接口,Image 控件中正是通过它来监听图片加载状态的。
ImageStream: 图像的加载对象,可监听图像数据加载状态,由 ImageStreamCompleter 返回一个 ImageInfo 对象用于图像显示
NetworkImage: 网络图片加载类,ImageProvider 的实现类,通过 URL 加载网络图像,笼罩 load() 办法返回 ImageStreamCompleter 的实现类 MultiFrameImageStreamCompleter,构建该类须要一个 codec 参数类型是 Future<ui.Codec>,通过调用_loadAsync() 办法下载网络图片数据取得字节流后通过调用 PaintingBinding.instance.instantiateImageCodec 办法对数据进行解码后取得 Future<ui.Codec> 对象,obtainKey 办法咱们发现返回的是 SynchronousFuture<NetworkImage>(this) 对象,正是 NetworkImage 本人自身,咱们通过该类的 == 办法能够看到判断两个 NetworkImage 类是否相等通过 runtimeType、url、scale 这三个参数来判断,所以图片缓存中的 key 相等判断取决于图片的 url、scale、runtimeType 参数。
MultiFrameImageStreamCompleter: 是 ImageStreamCompleter 的子类是 Flutter SDK 的预置类,构建该类须要一个 codec 参数类型是 Future<ui.Codec>,Codec 是解决图像编解码器的句柄也是 Flutter Engine API 的包装类,可通过其外部的 frameCount 变量获取图像帧数,别离解决单帧和多帧(动态图)图像,外部的 getNextFrame() 办法获取每帧的图像数据并创立 Image 控件中渲染须要的 ImageInfo 数据,调用 onImage 办法将 ImageInfo 返回给 Image 控件。
图像数据加载小结:
下面以网络图像加载流程剖析,首先通过 ImageProvider 的 resolve() 办法创立 ImageStream 对象,obtainKey() 办法创立图像缓存列表中的惟一 key(取决于图像 url 和 scale),通过 load() 办法加载图像数据并返回 MultiFrameImageStreamCompleter 对象,并将其设置给 ImageStream 中的 setCompleter() 办法增加监听图像加载实现状态,图像数据通过 Codec 解决帧数别离解决最终创立 ImageInfo 对象通过 ImageStreamListener 的 onImage 办法返回给 Image 控件。
图片渲染相干类
_ImageState: 是 Image 控件创立的 State 类,通过调用 ImageProvider 的 resolve() 办法解析图片数据,resolve() 办法返回的 ImageStream 对象,通过 addListener() 减少图片解析状态监听,通过 ImageStreamListener 的 onImage 回调中获取图片数据(ImageInfo)加载实现状态,onChunk 回调监听数据加载进度,onError 监听图片加载谬误状态,最终通过调用 setState 进行数据更新绘制。
仔细的同学会发现 ImageProvider 的实例对象(widget.image)被 ScrollAwareImageProvider 包装了一下又从新创立了一个 provider,在 ScrollAwareImageProvider 外部次要是重写了其中的 resolveStreamForKey() 办法,Flutter SDK 1.17 版本中对图片解析减少了疾速滚动优化,当判断以后屏幕处在疾速滚动状态时,则将图片解析过程提早下一帧帧尾进行。
RawImage:RenderObjectWidget 的子类,重写 createRenderObject 办法创立 RenderObject 子类。
RenderImage: 渲染树中 RenderObject 的实现类,Flutter 的三棵树 Widget、Element、RenderObject,而 RenderObject 这是负责绘制渲染的,RenderImage 重写 performLayout() 办法度量渲染尺寸并布局,重写 paint() 办法获取画布 Canvas,Canvas 是记录图片操作的接口类,通过参数解决图片镜像、裁剪、平铺等逻辑后调用的 drawImageNine() 和 drawImageRect() 办法将图片合成到画布上最终调用 Skia 引擎 API 进行绘制。
图片渲染小结:
Image 控件中通过调用 ImageProvider 的 resolve() 办法获取图片数据 ImageInfo 对象,通过 setState 办法将数据更新给图片渲染控件(RenderImage),RenderImage 中重写 paint() 办法依据传入参数对图片数据处理后绘制到 Canvas 画布上并调用 Skia 引擎 API 进行绘制。
总结
以上是 Image 图片加载原理及源码剖析,那么咱们在翻阅了 Image 源码后能做些什么呢?应用过程中有哪些能够优化的局部呢?让咱们持续往下看。
图片缓存池大小限度优化
Flutter 自身提供了定制化 Cache 的能力,所以优化 ImageCache 的第一步就是要依据机型的理论物理内存去做缓存大小的适配,通过 PaintingBinding.instance.imageCache 调用的 maximumSize 和 maximumSizeBytes 动静设置正当的图片缓存大小限度防止因图片过多导致 OOM。
未显示图像内存优化
可联合 StatefulWidget 控件生命周期中的 deactive()、dispose() 等办法,在页面控件中的图片未显示在屏幕上或控件已销毁时调用图片缓存中的 evict() 办法进行资源开释。
图片预缓存解决
Image 控件中提供了 precacheImage() 办法能够将须要显示的图片事后加载到 ImageCache 的缓存列表中,缓存列表中通过 key 值辨别雷同图片,在页面关上后间接从内存缓存获取,可疾速显示图片。
图片文件缓存
通过查看网络图片加载类 NetworkImage 源码能够发现,图片数据下载和解码过程都是通过_loadAsync() 办法实现的,所以咱们能够通过革新这个办法中图片文件下载、读取、保留过程去减少图片文件本地存储、获取原生图片库缓存、图片下载 DNS 解决等性能。
自定义占位图、谬误图成果
Image 控件中的 frameBuilder 和 errorBuilder 参数别离为咱们提供了占位图和谬误图的自定义形式,也可应用 FadeInImage 控件提供的占位图(placeholder)、谬误图 imageErrorBuilder 等参数,FadeInImage 外部实现也是 Image 控件,感兴趣的同学能够查看其源码实现。
大图下载进度自定义显示
显示成果:https://flutter.github.io/assets-for-api-docs/assets/widgets/loading\_progress\_image.mp4
图片可拉伸区域设置(.9 图片)
RenderImage 的 paint 办法中咱们发现在调用 Canvas API 绘制前会判断 centerSlice 参数别离调用 drawImageNine() 和 drawImageRect() 办法,Image 正式通过 centerSlice 参数配置图片的可拉伸区域,参考代码:centerSlice: Rect.fromLTWH(20, 20, 1, 1),L:横向可拉伸区域右边起始点地位,T:纵向可拉伸区域上边起始点地位,W:横向可拉伸区域宽度,H:纵向可拉伸区域宽度。
将来布局
本文介绍了京东 APP 中 Flutter 摸索遇到的问题以及图片的加载原理和应用过程中的一些技巧,随着 Flutter SDK 版本迭代更新,咱们将持续对图片加载框架进行优化,原生开发中的多个优良图片框架曾经经验了大量用户的考验这也始终是咱们渴望在 Flutter 上复用的能力,所以咱们也在积极探索原生和 Flutter 中图片内存共享计划,咱们心愿这个加强能力是非侵入式的,咱们也在尝试外接纹理等计划,这块技术细节停顿将在后续文章中持续和大家一起探讨。
参考资料
1、http://storage.360buyimg.com/pub-image/Image-source.jpg
2、https://book.flutterchina.club/chapter14/image\_and\_cache.html
3、https://api.flutter-io.cn/flutter/painting/ImageCache-class.html
作者:京东批发 徐雄伟
起源:京东云开发者社区