elementui图片列表-深入理解Flutter图片加载原理

前言

随着Flutter稳定版本的逐步迭代更新,易迅APP内部的Flutter业务也与日俱增。 Flutter的开发为我们提供了高效的开发环境、优秀的跨平台适配、丰富的功能组件和动画以及接近原生的交互体验。 ,但是也突然带来了一些OOM问题。 通过结合网上的监控信息和Observatory工具,我们发现问题的原因是Flutter页面加载大量图片导致内存溢出,这也是原生开发中常见的问题之一。 首先,Flutter官方提供的Imagewidget实现图像的加载和显示。 只有了解了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方法请参考:

图片加载过程

Flutter的图片加载原理与原生客户端中的图片框架加载原理类似。 详情请点击下方大图查看。 加载步骤如下:

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

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

3、如果缓存存在,则返回已有的图像数据;

4、如果缓存不存在,则根据来源加载图像数据,解码并同步到缓存并返回;

5、设置反弹窃听图片的数据加载状态,数据加载完成后重新渲染控件显示图片;

您可能已经注意到,上一个流程图中的文件缓存部分是白色的。 目前官方不支持该功能。 接下来我们将通过源码逐步分析加载过程以及如何通过更改源码来完成文件缓存功能。

源码分析

下面将通过流程图结合UML类图来分析图片加载过程:

这个UML类图乍一看可能有点复杂,但是如果仔细看,你会发现图像数据加载过程被分成了几个模块。 下面将按照模块逐步分析。 下面以网络图片加载方式为例,讲解核心类和核心技能功能。

核心课程和技能简介

01

启动缓存相关类

PaintingBinding:图像缓存类和shader预加载,该类是基于框架的应用启动时绑定到Flutter引擎的带类,在启动入口main.dart的runApp方法中创建WidgetsFlutterBinding类时初始化,通过重写父类的 initInstances() 方法初始化内部着色器预加载(Skia sketch 在 GPU 上第一次需要编译对应的着色器,这个过程大约 20ms~200ms)和图像缓存等。图像缓存是外部使用单实例方法(PaintingBinding.instance.imageCache),这意味着图像缓存在APP中是全局的,并且该类还提供图像解码(instantiateImageCodec)、缓存消除(evict)等功能。

ImageCache:图像缓存类。 默认情况下,缓存对象的最大数量限制为 1000 个对象,最大容量限制为 100MB。 由于图片加载过程是一个异步操作,所以缓存的图片分为三种状态:已使用、已加载、未使用。 分别对应三个图片缓存列表,当图片列表超过限制时,图片缓存列表中最近最少使用的图片将被删除。 缓存列表有:已使用的图像缓存列表(_cache)、已加载的图像缓存列表(_pendingImages)、未使用的图像缓存列表(_liveImages),提供以下方法:获取缓存(putIfAbsent)、清除缓存(清除、clearLiveImages)、逐出单个图像(evict)、限制最大缓存数量(maximumSize)、限制最大缓存大小(maximumSizeBytes)等。

从源码中我们可以看到,缓存列表是Map类型的。 Flutter中Map创建的对象是LinkedHashMap,它是有序的,键盘值的插入顺序是迭代的。 Flutter使用LinkedHashMap来存储图像数据,实现类似LRU算法的缓存。 当缓存列表中的图片被使用后,图片数据将被重新插入到缓存列表的末尾,这样最近最少使用的图片仍然会被放置在列表的颈部。

当缓存列表减少图像数据时,会检查缓存列表是否超出最大缓存数量和最大缓存大小。 如果出现溢出,则使用Map的keys.first方法获取缓存列表。 返回最近最少使用的 Image 对象将被删除,直到达到缓存限制。

启动缓存总结:

Flutter启动后,在PaintingBinding中创建ImageCache缓存。 图片缓存是全局的,以单实例的形式提供外部使用。 默认最大缓存数量限制为 1000 个对象,最大容量限制为 100MB。 缓存中的Map列表以key/value的形式存储图像信息,并通过keys.first实现的类似LRU算法来管理图像缓存列表,并提供putIfAbsent()方法来获取缓存图像。 如果缓存中不存在,则会通过回弹图像加载类Image数据中的load()方法来加载,此外,图像缓存还提供了clear()和evict()方法来删除缓存。

02

图片数据加载相关类

ImageProvider:图像数据提供的具体类,定义了图像数据解析方法(resolve)、唯一密钥生成方法(obtainKey)、数据加载方法(load),obtainKey和load方法是通过泛型实现的,由obtainKey方法生成。 object用于显存缓存的key值。 load方法会根据不同的数据源加载图像数据。 常用的Provider泛型类型有:NetworkImage、AssetImage、FileImage、MemoryImage。 我们可以看到resolve方法返回图像加载对象类(ImageStream),load方法返回ImageStreamCompleter类来管理图像加载状态和图像数据(ImageInfo)。

ImageStreamCompleter:是一个具体类,用于在加载图像对象(ImageInfo)的加载过程中管理一些套接字。 Image控件就是通过它来监听图片加载状态的。

ImageStream:图像的加载对象,可以监听图像数据的加载状态,ImageStreamCompleter返回的一个ImageInfo对象,用于图像显示

NetworkImage:网络图像加载类,ImageProvider的实现类,通过URL加载网络图像elementui图片列表,并重写load()方法返回ImageStreamCompleter的实现类MultiFrameImageStreamCompleter。 创建该类,编解码器参数类型为Future,通过调用_loadAsync()下载网络图片数据并获取字节流后,调用PaintingBinding.instance.instantiateImageCodec方法对数据进行解码得到Future对象。 我们发现obtainKey方法的返回是SynchronousFuture(this)对象,也就是NetworkImage本身。 我们使用这个类的 == 方法可以看到,通过runtimeType、url、scale这三个参数来判断两个NetworkImage类是否相等,所以图片缓存中关键的相等判断就依赖于url、scale、图像的runtimeType参数。

MultiFrameImageStreamCompleter:是ImageStreamCompleter的实现类,是FlutterSDK的预设类。 要创建此类,需要编解码器参数类型。 Codec是处理图像编解码器的句柄,也是FlutterEngineAPI的包装类。 可以通过其内部frameCount变量获取图像。 图像帧率,分别处理单帧和多帧(动态图像)图像,内部getNextFrame()方法获取每一帧的图像数据并在Image控件中创建渲染所需的ImageInfo数据,并调用onImage方法将 ImageInfo 返回给 Image 控件。

图像数据加载总结:

里面分析了网络图片的加载过程。 首先通过ImageProvider的resolve()方法创建ImageStream对象,obtainKey()方法用于创建图片缓存列表中的唯一key(取决于图片url和scale),load()方法是用于在图像缓存列表中创建唯一键。 加载图像数据并返回MultiFrameImageStreamCompleter对象,并设置到ImageStream中的setCompleter()方法中添加监听图像加载完成状态,图像数据会单独由Codec处理帧率,最终创建一个ImageInfo对象并将其返回给 ImageStreamListener Image 控件的 onImage 方法。

03

图像渲染相关类

_ImageState:是Image控件创建的State类。 通过调用ImageProvider的resolve()方法来解析图像数据。 使用resolve()方法返回的ImageStream对象,通过addListener()减少对图像分析状态的窃听,通过ImageStreamListener(ImageInfo)加载完成状态的onImage弹起获取图像数据,onChunk弹起窃听数据加载进度,onError 监听图片加载错误状态,最后通过调用 setState 更新和概述数据。

细心的同学会发现,ImageProvider的实例对象(widget.image)被ScrollAwareImageProvider包装了,重新创建了一个provider。 ScrollAwareImageProvider内部,主要是重绘了resolveStreamForKey()方法。 FlutterSDK1.17版本中,减少了图像分析。 针对快速滚动进行优化,当判断当前屏幕处于快速滚动状态时,图像解析过程将推迟到下一帧末尾。

RawImage:RenderObjectWidget的泛型类型,重绘createRenderObject方法创建RenderObject的泛型类型。

RenderImage:渲染树中RenderObject的实现类,Flutter Widget、Element、RenderObject三棵树elementui图片列表,RenderObject负责草图和渲染。 RenderImage重绘performLayout()方法来衡量渲染规范和布局,重绘paint()方法获得画布Canvas,Canvas是一个用于记录图像操作的socket类。 通过参数处理图像镜像、裁剪、平铺等逻辑后,调用drawImageNine()和drawImageRect()将图像合成到画布上,最后调用Skia引擎API进行绘制。

图像渲染总结:

在Image控件中,通过调用ImageProvider的resolve()方法获取图像数据ImageInfo对象,并通过setState方法将数据更新到图像渲染控件(RenderImage)。 Canvas并调用Skia引擎API进行绘制。

总结

通过上述源码搜索,我们发现Flutter本身提供了多种显存缓存能力,但显存缓存的上限默认为100MB。 这样配置比较低的机器上显存(Flutter+native)就会超过上限,导致OOM,所以我们在使用时需要获取。 根据机器实际化学内存重新调整Flutter端显存缓存限制大小,通过PaintingBinding.instance.imageCache调用的maximumSize和maximumSizeBytes动态设置合理的图片缓存限制,防止图片过多导致OOM内存占用,所以我们阅读Image源代码 接下来我可以做什么? 请观看以下部分。

显卡未显示视频内存优化:

在使用过程中,我们发现一些离开屏幕的图片仍然保存在显存缓存中。 结合Image控件生命周期中的deactive()、dispose()等方法,导致页面控件中的图片不显示在屏幕上或者控件被关闭。 销毁时,调用图片缓存中的evict()方法释放资源,减少内存消耗。

图片预缓存处理:

Image控件提供了precacheImage()方法,可以将要显示的图像预加载到ImageCache的缓存列表中。 通过缓存列表中的键值可以识别同一张图像。 页面打开后可以直接从显存缓存中获取,快速显示图像。

图像文件C盘缓存:

通过查看网络图像加载类NetworkImage的源码可以发现,图像数据的下载和解码过程是通过_loadAsync()方法完成的,因此我们可以通过修改下载、读取的流程来减小图像文件大小,并在该方法中将图像文件保存到本地存储、访问原图库的缓存、图像下载DNS处理等功能。

自定义占位图和误差图疗效:

Image控件中的frameBuilder和errorBuilder参数分别为我们提供了占位符图像和错误图像的自定义方法。 您还可以使用 FadeInImage 控件提供的占位符和错误图像 imageErrorBuilder 等参数。 FadeInImage的内部实现也是一个Image控件。 ,有兴趣的朋友可以查看其源码实现。

大图下载进度自定义显示:

显示功效:

图像可拉伸区域设置(.9图像):

在RenderImage的paint方法中,我们发现在调用CanvasAPI进行绘制之前,会确定centerSlice参数,并分别调用drawImageNine()和drawImageRect()方法。 Image会通过centerSlice参数配置图像的可拉伸区域。 参考代码:centerSlice:Rect.fromLTWH(20 ,20,1,1),L:垂直可拉伸区域右侧的起点位置,T:水平可拉伸区域上方的起点位置,W:长度垂直可拉伸区域的长度,H:水平可拉伸区域的长度。

将来的计划

本文介绍了Flutter在易迅APP中遇到的问题、图片加载的原理以及使用过程中的一些方法。 随着FlutterSDK版本的迭代更新,我们会不断优化图片加载框架,原生开发中很多优秀的图片框架已经经过大量用户的测试,这仍然是我们希望复用的能力Flutter,所以我们也在积极探索原生和Flutter中的图像内存共享解决方案。 我们希望这种改进能力是非侵入式的,我们也在尝试外部纹理等解决方案。 这个技术细节的进展我们会在后续的文章中继续和大家讲解。

参考

1、

2、

3.

结尾

MySQLVSPostgreSQL谁是天下第一?

这里有最新的开源信息、软件更新、技术干货等。

点击这里↓↓↓记得关注✔star⭐