0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Flutter图片是如何加载的?使用过程中有哪些需要注意的地方?

OSC开源社区 来源:京东零售技术 2023-08-18 10:06 次阅读

前言

随着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
)

c311ab2c-3cef-11ee-ac96-dac502259ad0.png

文章篇幅有限Image控件的使用方法这里就不在展开讲解了

图片加载流程

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

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

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

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

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

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

c346832e-3cef-11ee-ac96-dac502259ad0.png

大家可能注意到了上面流程图中的文件缓存部分是灰色的,目前官方还不支持此功能,下面我们会通过源码逐步分析加载流程及如何通过修改源码补全文件缓存功能。

源码分析

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

c35bfbe6-3cef-11ee-ac96-dac502259ad0.png

这个UML类图起初一看会感觉稍微有点儿复杂,但仔细看会发现已将图片数据加载流程分成几大模块,下面将按照模块进行逐步分析,下面将以网络图片加载方式为例讲解核心类和核心方法功能。

核心类及方法介绍

01 启动缓存相关类

PaintingBinding:图片缓存类和着色器预加载,该类是基于框架的应用程序启动时绑定到Flutter引擎的胶水类,在启动入口main.dart的runApp方法中创建WidgetsFlutterBinding类时被初始化的,通过覆盖父类的initInstances()方法初始化内部的着色器预加载(Skia第一次在GPU上绘制需要编译相应的着色器,这个过程大概20ms〜200ms)及图片缓存等,图片缓存以单例的方式(PaintingBinding.instance.imageCache)对外提供方法使用,也就是说这个图片缓存在APP中是全局的,并在这个类中还提供了图像解码(instantiateImageCodec)、缓存清除(evict)等功能。

c3c70350-3cef-11ee-ac96-dac502259ad0.png

c3e80262-3cef-11ee-ac96-dac502259ad0.png

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

c4465aec-3cef-11ee-ac96-dac502259ad0.png

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

c4870a60-3cef-11ee-ac96-dac502259ad0.png

当缓存列表增加图片数据后,会通过最大缓存个数和最大缓存大小两个纬度进行检查缓存列表是否超限,若存在超限情况则通过Map的keys.first方法获取缓存列表头部最近最少使用的图片对象进行删除,直到满足缓存限制。

c490f282-3cef-11ee-ac96-dac502259ad0.png

c4c8fe5c-3cef-11ee-ac96-dac502259ad0.png

启动缓存小结:

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

02 图片数据加载相关类

ImageProvider:图片数据提供抽象类,该类定义了图像数据解析方法(resolve)、唯一key生成方法(obtainKey)、数据加载方法(load),obtainKey 和load方法均由子类实现,obtainKey方法生成的对象用于内存缓存的key值使用,load方法将按照不同数据源加载图像数据,常用的Provider子类有:NetworkImage、AssetImage、FileImage、MemoryImage,我们可以看到resolve方法返回的是图片加载对象类(ImageStream),load方法返回的是ImageStreamCompleter类用来管理图像加载状态及图像数据(ImageInfo)。

c5011c24-3cef-11ee-ac96-dac502259ad0.png

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

c5386e72-3cef-11ee-ac96-dac502259ad0.png

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

c58506d8-3cef-11ee-ac96-dac502259ad0.png

NetworkImage:网络图片加载类,ImageProvider的实现类,通过URL加载网络图像,覆盖load()方法返回ImageStreamCompleter的实现类MultiFrameImageStreamCompleter,构建该类需要一个codec参数类型是Future,通过调用_loadAsync()方法下载网络图片数据获得字节流后通过调用PaintingBinding.instance.instantiateImageCodec方法对数据进行解码后获得Future对象,obtainKey方法我们发现返回的是SynchronousFuture(this)对象,正是NetworkImage 自己本身,我们通过该类的==方法可以看到判断两个NetworkImage类是否相等通过runtimeType 、url 、scale 这三个参数来判断,所以图片缓存中的key相等判断取决于图片的url、scale、runtimeType参数。

c59c3362-3cef-11ee-ac96-dac502259ad0.png

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

c5badc54-3cef-11ee-ac96-dac502259ad0.png

c5eb26e8-3cef-11ee-ac96-dac502259ad0.png

图像数据加载小结:

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

03 图片渲染相关类

_ImageState:是Image控件创建的State类,通过调用ImageProvider的resolve()方法解析图片数据,resolve()方法返回的ImageStream对象,通过addListener()增加图片解析状态监听,通过ImageStreamListener的onImage回调中获取图片数据(ImageInfo)加载完成状态,onChunk回调监听数据加载进度,onError监听图片加载错误状态,最终通过调用setState进行数据更新绘制。

c627c62a-3cef-11ee-ac96-dac502259ad0.png

细心的同学会发现ImageProvider的实例对象(widget.image)被ScrollAwareImageProvider包装了一下又重新创建了一个provider,在ScrollAwareImageProvider内部主要是重写了其中的resolveStreamForKey()方法,Flutter SDK 1.17版本中对图片解析增加了快速滚动优化,当判断当前屏幕处在快速滚动状态时,则将图片解析过程延迟下一帧帧尾进行。

c691e514-3cef-11ee-ac96-dac502259ad0.png

RawImage:RenderObjectWidget的子类,重写createRenderObject方法创建RenderObject子类。

RenderImage:渲染树中RenderObject的实现类,Flutter的三棵树Widget、Element、RenderObject ,而RenderObject是负责绘制渲染的,RenderImage重写performLayout()方法度量渲染尺寸并布局,重写paint()方法获取画布Canvas,Canvas是记录图片操作的接口类,通过参数处理图片镜像、裁剪、平铺等逻辑后调用的drawImageNine()和drawImageRect()方法将图片合成到画布上最终调用Skia引擎API进行绘制。

c6a323b0-3cef-11ee-ac96-dac502259ad0.png

c6d79cd0-3cef-11ee-ac96-dac502259ad0.png

图片渲染小结:

Image控件中通过调用ImageProvider的resolve()方法获取图片数据ImageInfo对象,通过setState方法将数据更新给图片渲染控件(RenderImage),RenderImage中重写paint()方法根据传入参数对图片数据处理后绘制到Canvas画布上并调用Skia引擎API进行绘制。

总结

经过以上源码探索我们发现Flutter本身提供了定制化的内存缓存能力,但内存缓存上限默认是100MB,这样在配置比较低的机器上内存(Flutter+原生)会超出上限产生OOM,所以使用中我们需要获取机器的实际物理内存去重新调整Flutter端的内存缓存限制大小,通过PaintingBinding.instance.imageCache调用的maximumSize和maximumSizeBytes动态设置合理的图片缓存限制避免因图片内存占用过多导致OOM,那么我们在翻阅了Image源码后还能做些什么呢?请继续往下看。

未显示图像内存优化:

在使用过程中我们发现部分离开屏幕的图片始终被保存在内存缓存中,结合Image控件生命周期中的deactive()、dispose()等方法,在页面控件中的图片未显示在屏幕上或控件已销毁时调用图片缓存中的evict()方法进行资源释放减少内存开销。

图片预缓存处理:

Image控件中提供了precacheImage()方法可以将需要显示的图片预先加载到ImageCache的缓存列表中,缓存列表中通过key值区分相同图片,在页面打开后直接从内存缓存获取,可快速显示图片。

图片文件磁盘缓存:

通过查看网络图片加载类NetworkImage源码可以发现,图片数据下载和解码过程都是通过_loadAsync()方法完成的,所以我们可以通过改造这个方法中图片文件下载、读取、保存过程去增加图片文件本地存储、打通原生图片库缓存、图片下载DNS处理等功能。

自定义占位图、错误图效果:

c70a751a-3cef-11ee-ac96-dac502259ad0.png

Image控件中的frameBuilder和errorBuilder参数分别为我们提供了占位图和错误图的自定义方式,也可使用FadeInImage控件提供的占位图(placeholder)、错误图imageErrorBuilder等参数,FadeInImage内部实现也是Image控件,感兴趣的同学可以查看其源码实现。

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

c73bf23e-3cef-11ee-ac96-dac502259ad0.png

显示效果: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

    文章

    140

    浏览量

    19612
  • URL
    URL
    +关注

    关注

    0

    文章

    139

    浏览量

    15328
  • 缓存器
    +关注

    关注

    0

    文章

    63

    浏览量

    11657
  • UML技术
    +关注

    关注

    0

    文章

    5

    浏览量

    1370
  • flutter
    +关注

    关注

    0

    文章

    13

    浏览量

    441

原文标题:深入理解Flutter图片加载原理

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    ADP5071在使用过程中有什么需要特别注意地方

    ,ADP5071 的两路输出电源都正常。 从这个情况来看,似乎好像ADP5071的输出带不动负载的样子,可是芯片手册以及Demo电路中,ADP5071的输出带200mA 的负载是可以的。 ①ADP5071在使用过程中有什么需要特别注意
    发表于 01-04 08:20

    在实际使用过程中需要大家注意的参数有哪些?

    电阻是大家学习电路过程中首先接触到的器件,可能很多人觉得电阻没什么神秘的。其实,电阻除了阻值之外,还有许多参数在实际使用过程中需要大家注意,下面我给大家一一道来。
    发表于 06-08 06:45

    有关FreeRTOS中信号量和计数信号量在使用过程中需要注意的细节

    本文介绍有关FreeRTOS中信号量和计数信号量在使用过程中需要注意的细节,以及自己在过程中的分享的一些有关遇到的问题和注意点。
    发表于 08-06 06:26

    UPS电源安装和使用过程中需要注意事项

    山特UPS电源安装和使用过程中需要注意事项山特UPS电源连接方法和注意事项山特UPS电源的输入插座要与市电连接,输出插座可经接线板与计算机和显示器连接。对于山特UPS电源,其输出电源分两部分:一部分
    发表于 11-15 06:46

    STM32使用过程中应该注意哪些事项?

    STM32使用过程中应该注意哪些事项?
    发表于 12-21 07:06

    TTL和CMOS电平在使用过程中有什么本质的区别?

    TTL和CMOS电平在使用过程中有什么本质的区别?为什么有的芯片是TTL而有的是CMOS,就一种不妥吗?
    发表于 04-25 09:25

    ST的LL库在使用的过程中需要注意些什么地方

    ST的LL库在使用的过程中需要注意些什么地方
    发表于 10-09 06:48

    PCB工程师需要注意地方

    PCB工程师需要注意地方,在PDF中有很多值得关注的地方
    发表于 11-20 16:21 0次下载

    软波导在使用过程中有哪些事项需要注意

    。如果指标要求不高的话一般用的是国产的。软波导的主要指标有:发射频率、接收频率、驻波比和回波损耗。下面我们就看下在使用过程中有哪些事项需要注意。 (1)在能够满足电性能使用要求的情况下,尽量选用选择截面尺寸
    发表于 06-29 13:48 722次阅读

    硅胶点胶代加工的使用过程中需要注意哪些问题

    代加工使用过程中要注意哪些问题? 1、硅胶点胶代加工时胶水的固化问题 硅胶点胶代加工在使用的过程当中是需要特别的重视胶水的固化问题,很多的厂家会给出一个温度曲线,在常温条件下有的硅胶胶
    发表于 10-13 10:34 1291次阅读

    工控主板使用过程中需要注意的事项有哪些

    工控主板是应用于工业场合的主板,被工业电脑所采用,根据需求可以适应宽温环境,可以适应恶劣环境,可以长时间高负荷工作等。那么工控主板使用过程中要注意哪些事项呢?下面一起来了解下。 1、请在断电条件下
    发表于 05-19 16:02 576次阅读

    LoRa模块的使用过程中要注意什么?

    LoRa无线模块作为近年来最火热的低功耗远距离的无线模块,在市场上是非常受欢迎的。合理规范安装使用是LoRa无线模块可以长期稳定工作的重要因素之一,同时也可以避免产生损坏,减少维护和项目运维成本。那么LoRa模块在使用安装过程中要注意什么,下面就来简单介绍一下。
    的头像 发表于 02-03 15:39 1231次阅读

    电池重物冲击试验机使用过程中需要注意的事项-电池安全检测设备

    重物冲击试验机在使用过程中需要注意的事项。   一、进行实验时,必须确保操作人员和调查人员远离安全警戒线,以避免实验对其生命安全的影响,从而确保其人身安全。   二、进行重物冲击试验时,卸下振动摆的步
    的头像 发表于 05-11 14:51 644次阅读
    电池重物冲击试验机<b class='flag-5'>使用过程中</b><b class='flag-5'>需要注意</b>的事项-电池安全检测设备

    安全光幕使用过程中必须注意的问题

    安全光幕使用过程中必须注意的问题: 1.用手遮挡安全光幕,并观察指示灯转换是否正常。 2.冲床光电保护装置在使用过程中,不得随意变动光电保护装置的位置。 3.在保护区间内,用手遮挡光幕,机器应能立即
    的头像 发表于 08-16 16:41 476次阅读

    焊接机器人使用过程中要注意哪些问题

    操作过程中需要注意以下几个方面的问题。 焊接机器人使用过程中要注意哪些问题? 1. 设备维护与保养 焊接机器人作为高精度设备,日常维护保养必不可少。定期检查各类传感器、机械臂的关节、导
    的头像 发表于 10-22 17:43 169次阅读
    焊接机器人<b class='flag-5'>使用过程中</b><b class='flag-5'>要注意</b>哪些问题