当前位置: 首页 > news >正文

Flutter完整开发实战详解(十、 深入图片加载流程)

作为系列文章的第十篇,本篇主要深入了解 Flutter 中图片加载的流程,剥析图片流程中有意思的片段,结尾再实现 Flutter 实现本地图片缓存的支持。

前文:

  • 一、Dart语言和Flutter基础
  • 二、 快速开发实战篇
  • 三、 打包与填坑篇
  • 四、 Redux、主题、国际化
  • 五、 深入探索
  • 六、 深入Widget原理
  • 七、 深入布局原理
  • 八、 实用技巧与填坑
  • 九、 深入绘制原理

在 Flutter 中,图片的加载主要是通过 Image 控件实现的,而 Image 控件本身是一个 StatefulWidget ,通过前文我们可以快速想到, Image 肯定对应有它的 RenderObject 负责 layoutpaint ,那么这个过程中,图片是如何变成画面显示出来的?

一、图片流程

Flutter 的图片加载流程其实“并不复杂”,具体可点击下方大图查看,以网络图片加载为例子,先简单总结,其中主要流程是:

  • 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、最后 RawImageRenderImage 通过 paint 绘制 ImageInfo 中的 ui.Codec

注意,这的 ui.Codec 和后面的 ui.Image等,只是因为 Flutter 中在导入对象时,为了和其他类型区分而加入的重命名:import 'dart:ui' as ui show Codec;

是不是感觉有点晕了?relax!后面我们将逐步理解这个流程。

在 Flutter 的图片的加载流程中,主要有三个角色:

  • Image :用于显示图片的 Widget,最后通过内部的 RenderImage 绘制
  • ImageProvider:提供加载图片的方式如 NetworkImageFileImageMemoryImageAssetImage 等,从而获取 ImageStream ,用于监听结果
  • ImageStream:图片的加载对象,通过 ImageStreamCompleter 最后会返回一个 ImageInfo ,而 ImageInfo 内包含有 RenderImage 最后的绘制对象 ui.Image

从上面的大图流程可知,网络图片是通过 NetworkImage 这个 Provider 去提供加载的,各类 Provider 的实现其实大同小异,其中主要需要实现的方法主要如下图所示:

1、obtainKey

该方法主要用于标示当前 Provider 的存在,比如在 NetworkImage 中,这个方法返回的是 SynchronousFuture<NetworkImage>(this),也就是 NetworkImage 自己本身,并且得到的这个 key 在 ImageProvider 中,是用于作为内存缓存的 key 值

NetworkImage 中主要是通过 runtimeTypeurlscale 这三个参数判断两个NetworkImage 是否相等,所以除了 url ,图片的 scale 同样会影响缓存的对象哦。

2、load(T key)

load 方法顾名思义就是加载了,而该方法中所使用的 key ,毫无疑问就是上面 obtainKey 方法所提供的。

load 方法返回的是 ImageStreamCompleter 抽象对象,它主要是用于管理和通知 ImageStream 中得到的 dart:ui.Image ,比如在 NetworkImage 中的是子类 MultiFrameImageStreamCompleter , 它可以处理多帧的动画,如果图片只有一针,那么将执行一次都结束。

3、resolve

ImageProvider 的关键在于 resolve 方法,从流程图我们可知,该方法在 Image 的生命周期回调方法 didChangeDependenciesdidUpdateWidgetreassemble 里会被调用,从下方源码可以看出,上面我们所实现的 obtainKeyload 都会在这里被调用

这个有个有意思的对象,就是 Zone

因为在 Flutter 中,同步异常可以通过try-catch捕获,而异步异常如 Future ,是无法被当前的 try-catch 直接捕获的。

所以在 Dart中 Zone 的概念,你可以给执行对象指定一个Zone,类似提供一个沙箱环境,而在这个沙箱内,你就可以全部可以捕获、拦截或修改一些代码行为,比如所有未被处理的异常。

resolve 方法内主要是用到了 PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)PaintingBinding 是一个胶水类,主要是通过 Mixins 粘在 WidgetsFlutterBinding 上使用,而以前的篇章我们说过, WidgetsFlutterBinding 就是我们的启动方法 runApp 的执行者。

所以图片缓存是在PaintingBinding.instance.imageCache内单例维护的。

如下图所示,putIfAbsent 方法内部,主要是通过 key 判断内存中是否已有缓存、或者正在缓存的对象,如果是就返回该 ImageStreamCompleter ,不然就调用 loader 去加载并返回。

值得注意的是,此时的的 cache 是有两个状态的,因为返回的 ImageStreamCompleter 并不代表着图片就加载完成,所以如果是首次加载,会先有 _PendingImage 用于标示该key的图片处于加载中的状态 ,并且添加一个 listener, 用于图片加载完成后,替换为缓存 _CacheImage

发现没有,这里和我们理解上的 Cache 概念稍微有点不同,以前我们缓存的一般是 key - bitmap 对象,也就是实际绘制数据,而在 Flutter 中,缓存的仅是ImageStreamCompleter 对象,而不是实际绘制对象 dart:ui.Image

3、ImageStreamCompleter

ImageStreamCompleter 是一个抽象对象,它主要是用于管理和通知 ImageStream ,处理图片数据后得到的包含有 dart:ui.Image 的对象 ImageInfo

接下来我们看 NetworkImage 中的 ImageStreamCompleter 实现类 MultiFrameImageStreamCompleter 。如下图代码所示,MultiFrameImageStreamCompleter 主要通过 codec 参数获得渲染数据,而这个数据来源通过 _loadAsync 方法得到,该方法主要通过 http 下载图片后,对图片数据通过 PaintingBinding 进行 ImageCodec 编码处理,将图片转化为引擎可绘制数据。

而在 MultiFrameImageStreamCompleter 内部, ui.Codec 会被 ui.Image ,通过 ImageInfo 封装起来,并逐步往回回调到 _ImageState 中,然后通过 setState 将数据传递到 RenderImage 内部去绘制。

怎么样,现在再回过头去看开头的流程图,有没有一切明了的感觉?

二、本地图片缓存

通过上方流程的了解,我们知道 Flutter 实现了图片的内存缓存,但是并没有实现图片的本地缓存,所以我们入手的点,应该从 ImageProvider 开始。

通过上面对 NetworkImage 的分析,我们知道图片是在 _loadAsync 方法通过 http 下载的,所以最简单的就是,我们从 NetworkImage cv 一份代码,修改 _loadAsync 支持 http 下载前读取本地缓存,下载后通过将数据保存在本地。

结合 flutter_cache_manager 插件,如下方代码所示,就可以快速简单实现图片的本地缓存:

 Future<ui.Codec> _loadAsync(NetworkImage key) async {
    assert(key == this);

    /// add this start
    /// flutter_cache_manager DefaultCacheManager
    final fileInfo = await DefaultCacheManager().getFileFromCache(key.url);
    if(fileInfo != null && fileInfo.file != null) {
      final Uint8List cacheBytes = await fileInfo.file.readAsBytes();
      if (cacheBytes != null) {
        return PaintingBinding.instance.instantiateImageCodec(cacheBytes);
      }
    }
    /// add this end

    final Uri resolved = Uri.base.resolve(key.url);
    final HttpClientRequest request = await _httpClient.getUrl(resolved);
    headers?.forEach((String name, String value) {
      request.headers.add(name, value);
    });
    final HttpClientResponse response = await request.close();
    if (response.statusCode != HttpStatus.ok)
      throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');

    final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
    if (bytes.lengthInBytes == 0)
      throw Exception('NetworkImage is an empty file: $resolved');
    
    /// add this start
    await DefaultCacheManager().putFile(key.url, bytes);
    /// add this edn

    return PaintingBinding.instance.instantiateImageCodec(bytes);
  }
复制代码

三、其他补充

1、缓存数量

在闲鱼关于 Flutter 线上应用的内存分析文章中,有过对图片加载对内存问题的详细分析,其中就有一个是 ImageCache 的问题。

上面的流程我们知道, ImageCache 缓存的是一个异步对象,缓存异步加载对象的一个问题是,在图片加载解码完成之前,你无法知道到底将要消耗多少内存,并且大量的图片加载,会导致的解码任务需要产生大量的IO。

而在 Flutter 中, ImageCache 默认的缓存大小是

const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 
复制代码

所以简单粗暴的做法是: PaintingBinding.instance.imageCache.maximumSize = 100; 同时在页面不可见时暂停图片的加载等。

2、.9图

在 Image中,可以通过 centerSlice 配置参数设置.9图效果哦。

自此,第十篇终于结束了!(///▽///)

资源推荐

  • Github : github.com/CarGuo
  • 本文代码 :github.com/CarGuo/GSYG…
完整开源项目推荐:
  • GSYGithubApp Flutter
  • GSYGithubApp React Native
  • GSYGithubAppWeex
文章

《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》

《Flutter完整开发实战详解(二、 快速开发实战篇)》

《Flutter完整开发实战详解(三、 打包与填坑篇)》

《Flutter完整开发实战详解(四、Redux、主题、国际化)》

《Flutter完整开发实战详解(五、 深入探索)》

《Flutter完整开发实战详解(六、 深入Widget原理)》

《Flutter完整开发实战详解(七、 深入布局原理)》

《Flutter完整开发实战详解(八、 实用技巧与填坑)》

《Flutter完整开发实战详解(九、 深入绘制原理)》

《Flutter完整开发实战详解(十、 深入图片加载流程)》

《Flutter完整开发实战详解(十一、全面深入理解Stream)》

《跨平台项目开源项目推荐》

《移动端跨平台开发的深度解析》

《React Native 的未来与React Hooks》

转载于:https://juejin.im/post/5cb1896ce51d456e63760449

相关文章:

  • 个人支付宝/微信/云闪付收款技术总览
  • [LeetCode] Wildcard Matching
  • Linux之编程进阶(函数、trap信号捕捉、数组、字符串处理...)
  • 子元素浮动后,父元素高度自动增加
  • 【高级数据类型2】- 8. 结构体和方法
  • 修改server 2008远程桌面端口
  • F. Multicolored Markers(数学思维)
  • 每周一练 之 数据结构与算法(Stack)
  • 逻辑回归(个人总结)-- 于俊杰
  • 工作问题总结-----付款
  • hiero_v2.0的下载安装和使用
  • LOJ#2882. 「JOISC 2014 Day4」两个人的星座(计算几何)
  • 软件实现
  • tcp的三次握手
  • 怎样提高个人素质与修养
  • [LeetCode] Wiggle Sort
  • 《用数据讲故事》作者Cole N. Knaflic:消除一切无效的图表
  • Create React App 使用
  • exif信息对照
  • Material Design
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • vue:响应原理
  • 安卓应用性能调试和优化经验分享
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 成为一名优秀的Developer的书单
  • 多线程事务回滚
  • 判断客户端类型,Android,iOS,PC
  • 手写一个CommonJS打包工具(一)
  • 突破自己的技术思维
  • 一份游戏开发学习路线
  •  一套莫尔斯电报听写、翻译系统
  • Java总结 - String - 这篇请使劲喷我
  • 阿里云API、SDK和CLI应用实践方案
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • ​iOS实时查看App运行日志
  • # MySQL server 层和存储引擎层是怎么交互数据的?
  • #define与typedef区别
  • #宝哥教你#查看jquery绑定的事件函数
  • #使用清华镜像源 安装/更新 指定版本tensorflow
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (二)PySpark3:SparkSQL编程
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (九)One-Wire总线-DS18B20
  • (十二)devops持续集成开发——jenkins的全局工具配置之sonar qube环境安装及配置
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (已解决)报错:Could not load the Qt platform plugin “xcb“
  • (转) 深度模型优化性能 调参
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • .mysql secret在哪_MySQL如何使用索引
  • .NET 将混合了多个不同平台(Windows Mac Linux)的文件 目录的路径格式化成同一个平台下的路径
  • .Net中的集合
  • @ 代码随想录算法训练营第8周(C语言)|Day53(动态规划)