本文基于1.12.13+hotfix.8版本源码分析。

1、Image

点击进入源码,可以看到Image继承自StatefulWidget,那么重点自然在State里面。跟着生命周期走,可以发现在didUpdateWidget中调用了这个方法:

  void _resolveImage() {
// 在这里获取到一个流对象
final ImageStream newStream =
widget.image.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
));
assert(newStream != null);
_updateSourceStream(newStream);
} void _updateSourceStream(ImageStream newStream) {
// ... 省略部分源码
if (_isListeningToStream)
_imageStream.addListener(_getListener());
} ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}

在这里调用了image(ImageProvider)的resolve方法获取到一个ImageStream,并给这个流设置了监听器。从名字上,不难猜出这是个图片数据流,在listener拿到数据后会调用setState(() {})方法进行rebuild,这里不再贴代码。

2、ImageProvider

在上面我们看到了Image是需要接收图片数据进行绘制的,那么,这个数据是在哪里解码的?又是哪里发送过来的?

带着疑问,我们先进到ImageProvider的源码,可以发现其实这个类非常简单,代码量也不多,可以看看resolve方法的核心部分:

  Future<T> key;
try {
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
onError: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);

可以看到,这里会异步获取到一个key,然后从管理在PaintingBinding中的缓存池查找图片流。继续看关键的obtainKey和load方法,去到定义的地方,可以发现这两个都是子类实现的。从注释中可以看到,obtainKey的功能就是根据传入的ImageConfiguration生成一个独一无二的key(废话),而load方法则是将key转换成为一个ImageStreamCompleter对象并开始加载图片。

那么,我们从最简单的MemoryImage入手,首先看看obtainKey:

  @override
Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MemoryImage>(this);
}

可以看到,就只是把自己包了一层再返回,并没有什么特殊。接着看load:

  @override
ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
);
} Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
assert(key == this);
return decode(bytes);
}

同样非常简单,就是创建了一个ImageStreamCompleter的子类对象,同时传入了一个包装了解码器的Future(这个解码器是PaintingBinding.instance.instantiateImageCodec,内部调用native方法进行图片解码)。

看到这里,相信基本有猜想了,数据和解码器都提供了,看来ImageStreamCompleter就是我们要看的数据源提供者。

3、图片数据加载ImageStream、ImageStreamCompleter

废话不多说,直接看MultiFrameImageStreamCompleter,可以看到直接在构造函数中获取codec对象,在获取到以后就会去获取解码数据,下面是简化的代码片段:

  // 构造函数中获取codec
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {// 略}); void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null); if (hasListeners) {
// 拿到codec之后解码数据
_decodeNextFrameAndSchedule();
}
} Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
// 略
return;
}
if (_codec.frameCount == 1) {
// 发送数据
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
_scheduleAppFrame();
}

看到这里,终于找到了发送数据的地方,_emitFrame里面会调用setImage,而后在setImage中会找到listener并将数据发送,而listener就是widgets.Image注册的监听器。

4、缓存池PaintingBinding#imageCache

看完了加载流程,我们看看缓存池的缓存逻辑,回到ImageProvider的resolve方法,这里有个关键点,传给PaintingBinding的是个创建方法,而非实体。进入其源码可以看到是先检测cache中是否存在该对象,存在则直接返回,不存在才会调用load方法进行创建:

final _CachedImage image = _cache.remove(key);
if (image != null) {
// 有缓存就直接返回
_cache[key] = image;
return image.completer;
}
try {
// 没找到缓存就调外面传入的loader()进行创建
result = loader();
} // catch部分省略

并且,在刚创建时缓存中的对象是个PendingImage,这东西可以理解为类似一个占位符的作用,当图片数据加载完毕后才替换成实际数据对象CacheImage:

  void listener(ImageInfo info, bool syncCall) {
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
} // 数据加载完以后替换为实际数据对象
_cache[key] = image;
_checkCacheSize();
} // 这里创建了一个PendingImage插入缓存
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// 监听加载状态,result就是ImageStreamCompleter
result.addListener(streamListener);
}

5、网络图片加载

看完最基本的图片数据加载,接下来看看NetworkImage如何加载网络图片。看核心的load方法:

  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
// 关键点1,加载、解析数据
codec: _loadAsync(key, chunkEvents, decode),
// 关键点2,分块下载事件流传给completer用
chunkEvents: chunkEvents.stream,
scale: key.scale,
);
}

直接进入关键方法,看NetworkImage的_loadAsync方法:

  Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
) async {
try {
assert(key == this); 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 image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved); // 接收数据
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
// 这里能拿到下载进度
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
// 下载数据为空也会抛异常
throw Exception('NetworkImage is an empty file: $resolved'); // 解码数据
return decode(bytes);
} finally {
chunkEvents.close();
}
}

这里有2个点:

(1)通过HttpClient进行图片下载,下载失败或者数据为空都会抛异常,这里要做好异常处理。另外,从上面的图片缓存逻辑可以看到,flutter默认是只有内存缓存的,磁盘缓存需要自己处理,可以在这里入手处理;

(2)通过consolidateHttpClientResponseBytes接收数据,并将下载进度转成ImageChunkEvent发送出去。可以看看MultiFrameImageStreamCompleter对ImageChunkEvent的处理:

if (chunkEvents != null) {
chunkEvents.listen(
(ImageChunkEvent event) {
if (hasListeners) {
// 把这个事件传递给ImageStreamListener的onChunk方法
final List<ImageChunkListener> localListeners = _listeners
.map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)
.where((ImageChunkListener chunkListener) => chunkListener != null)
.toList();
for (ImageChunkListener listener in localListeners) {
listener(event);
}
}
}
);
}

顺着_listeners的来源,一路往上找,最后可以看到onChunk方法是这里传进来的:

  ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}

widget.loadingBuilder即自定义loading状态的方法。

flutter源码学习笔记-图片加载流程的更多相关文章

  1. Mybatis源码学习之资源加载(六)

    类加载器简介 Java虚拟机中的类加载器(ClassLoader)负责加载来自文件系统.网络或其他来源的类文件.Java虚拟机中的类加载器默认使用的是双亲委派模式,如图所示,其中有三种默认使用的类加载 ...

  2. 【Spring源码分析】Bean加载流程概览

    代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. 很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事 ...

  3. 【Spring源码分析】Bean加载流程概览(转)

    转载自:https://www.cnblogs.com/xrq730/p/6285358.html 代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. ...

  4. Spring源码分析:Bean加载流程概览及配置文件读取

    很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事的都是Java Web的工作,对于程序员来说,一个Web项目用到Spring,只是配置一下配置文件而已 ...

  5. mybatis(五):源码分析 - mapper文件加载流程

  6. mybatis(五):源码分析 - config文件加载流程

    详细的可以参考https://blog.csdn.net/weixin_33850890/article/details/88112849

  7. Hadoop源码学习笔记(1) ——第二季开始——找到Main函数及读一读Configure类

    Hadoop源码学习笔记(1) ——找到Main函数及读一读Configure类 前面在第一季中,我们简单地研究了下Hadoop是什么,怎么用.在这开源的大牛作品的诱惑下,接下来我们要研究一下它是如何 ...

  8. JDK源码学习笔记——LinkedHashMap

    HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序. LinkedHashMap保证了元素迭代的顺序.该迭代顺序可以是插入顺序或者是访问顺序.通过维护一个 ...

  9. Qt Creator 源码学习笔记03,大型项目如何管理工程

    阅读本文大概需要 6 分钟 一个项目随着功能开发越来越多,项目必然越来越大,工程管理成本也越来越高,后期维护成本更高.如何更好的组织管理工程,是非常重要的 今天我们来学习下 Qt Creator 是如 ...

随机推荐

  1. html/css系列-图片上下居中

    本文详情:http://www.zymseo.com/276.html图片上下居中的问题常用的几种方法:图片已知大小和未知大小,自行理解 .main{ width: 400px;height: 400 ...

  2. javascript闭包的用处

    谈及javascript的闭包,可能想到的就是内存泄露,慎用闭包,但是实际上闭包还有更多好的作用: 1,可以将for循环的变量封闭在闭包环境中,下面这种情况,无论点击1-5div,最终打印的都是5,因 ...

  3. 痞子衡嵌入式:恩智浦SDK驱动代码风格、模板、检查工具

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家讲的是恩智浦 SDK 驱动的代码风格. 上周痞子衡受领导指示,给 SE 同事做了一个关于 SDK 代码风格的分享.随着组内新人的增多,这样的培训 ...

  4. Aircrack-ng无线审计工具破解无线密码

    Aircrack-ng工具 Aircrack-ng是一个与802.11标准的无线网络分析的安全软件,主要功能有网络探测.数据包嗅探捕获.WEP和WPA/WPA2-PSK破解.Aircrack可以工作在 ...

  5. JDk下载和环境变量Path的配置

    JDK下载与安装 下载地址 打开该网址会显示如下图,点击DOWMLOAD即可: 出现该页面时,点击接受: 选择对应的安装包下载即可(本人用的是Windows64位): 注:如果您无法确定您的windo ...

  6. SQL Server 存储过程 函数 和sql语句 区别

    存储过程与sql语句 存储过程的优点: 1.具有更好的性能   存储过程是预编译的,只在创建时进行编译,以后每次执行存储过程都不需再重新编译,   而一般 SQL 语句每执行一次就编译一次,因此使用存 ...

  7. MAC下安装Fiddler抓包工具

    需求 我们都知道在Mac电脑下面有一个非常好的抓包工具:Charles.但是这个只能抓代理的数据包.但是有时候想要调试本地网卡的数据库 Charles 就没办法了.就想到了在windows下面的一个F ...

  8. tkinter学习1

    GUI 用户交互界面 tkinter 介绍 tkinter是 python自带的gui库,对图像处理库tk的封装 #导入tkinter库 import tkinter #创建主窗口对象 root = ...

  9. 如何使用域名访问自己的Windows服务器(Java web 项目)

    如何使用域名访问自己的Windows服务器(Java web 项目) 写在前面 前段时间在阿里云弄了个学生服务器,就想着自己搭建一个网站试一试,在网上查阅相关资料时发现大部分都是基于服务器是Linux ...

  10. 初始Django—Hello world

    1. 准备环境 > python -V Python > pip -V pip from c:\python3\lib\site-packages\pip (python 3.7) > ...