Flutter中网络图片加载和缓存源码分析(内存缓存+文件缓存

scale: key.scale,
informationCollector: (StringBuffer information) {
information.writeln(‘Image provider: $this’);
information.write(‘Image key: $key’);
}
);
}

代码中其就是创建一个MultiFrameImageStreamCompleter对象并返回,这是一个多帧图片管理器,表明Flutter是支持GIF图片的。创建对象时的codec变量由_loadAsync方法的返回值初始化,查看该方法内容

static final HttpClient _httpClient = HttpClient();

Future<ui.Codec> _loadAsync(NetworkImage key) async {
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 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’);

return PaintingBinding.instance.instantiateImageCodec(bytes);
}

这里才是关键,就是通过HttpClient对象对指定的url进行下载操作,下载完成后根据图片二进制数据实例化图像编解码器对象Codec,然后返回。

那么图片下载完成后是如何显示到界面上的呢,下面看下MultiFrameImageStreamCompleter的构造方法实现

MultiFrameImageStreamCompleter({
@required Future<ui.Codec> codec,
@required double scale,
InformationCollector informationCollector
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale,
_framesEmitted = 0,
_timer = null {
codec.then(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError(
context: ‘resolving an image codec’,
exception: error,
stack: stack,
informationCollector: informationCollector,
silent: true,
);
});
}

看,构造方法中的代码块,codec的异步方法执行完成后会调用_handleCodecReady函数,函数内容如下

void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);

_decodeNextFrameAndSchedule();
}

方法中会将codec对象保存起来,然后解码图片帧

Future _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
reportError(
context: ‘resolving an image frame’,
exception: exception,
stack: stack,
informationCollector: _informationCollector,
silent: true,
);
return;
}
if (_codec.frameCount == 1) {
// This is not an animated image, just return it and don’t schedule more
// frames.
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}

如果图片是png或jpg只有一帧,则执行_emitFrame函数,从帧数据中拿到图片帧对象根据缩放比例创建ImageInfo对象,然后设置显示的图片信息

void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}

/// Calls all the registered listeners to notify them of a new image.
@protected
void setImage(ImageInfo image) {
_currentImage = image;
if (_listeners.isEmpty)
return;
final List localListeners = _listeners.map(
(_ImageListenerPair listenerPair) => listenerPair.listener
).toList();
for (ImageListener listener in localListeners) {
try {
listener(image, false);
} catch (exception, stack) {
reportError(
context: ‘by an image listener’,
exception: exception,
stack: stack,
);
}
}
}

这时就会根据添加的监听器来通知一个新的图片需要渲染。那么这个监听器是什么时候添加的呢,我们回头看一下**_ImageState类中的didChangeDependencies()方法内容,执行完_resolveImage();后会执行_listenToStream();**方法

void _listenToStream() {
if (_isListeningToStream)
return;
_imageStream.addListener(_handleImageChanged);
_isListeningToStream = true;
}

该方法就向ImageStream对象中添加了监听器_handleImageChanged,监听方法如下

void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}

最终就是调用setState方法来通知界面刷新,将下载到的图片渲染到界面上来了。

实际问题

从以上源码分析,我们应该清楚了整个网络图片从加载到显示的过程,不过使用这种原生的方式我们发现网络图片只是进行了内存缓存,如果杀掉应用进程再重新打开后还是要重新下载图片,这对于用户而言,每次打开应用还是会消耗下载图片的流量,不过我们可以从中学习到一些思路来自己设计网络图片加载框架,下面作者就简单的基于Image.network来进行一下改造,增加图片的磁盘缓存。

解决方案

我们通过源码分析可知,图片在缓存中未找到时,会通过网络直接下载获取,而下载的方法是在NetworkImage类中,于是我们可以参考NetworkImage来自定义一个ImageProvider。

代码实现

拷贝一份NetworkImage的代码到新建的network_image.dart文件中,在_loadAsync方法中我们加入磁盘缓存的代码。

static final CacheFileImage _cacheFileImage = CacheFileImage();

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

/// 新增代码块start
/// 从缓存目录中查找图片是否存在
final Uint8List cacheBytes = await _cacheFileImage.getFileBytes(key.url);
if(cacheBytes != null) {
return PaintingBinding.instance.instantiateImageCodec(cacheBytes);
}
/// 新增代码块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’);

/// 新增代码块start
/// 将下载的图片数据保存到指定缓存文件中
await _cacheFileImage.saveBytesToFile(key.url, bytes);
/// 新增代码块end

return PaintingBinding.instance.instantiateImageCodec(bytes);
}

代码中注释已经表明了基于原有代码新增的代码块,CacheFileImage是自己定义的文件缓存类,完整代码如下

import ‘dart:convert’;
import ‘dart:io’;
import ‘dart:typed_data’;

import ‘package:crypto/crypto.dart’;
import ‘package:path_provider/path_provider.dart’;

class CacheFileImage {

/// 获取url字符串的MD5值
static String getUrlMd5(String url) {
var content = new Utf8Encoder().convert(url);
var digest = md5.convert(content);
return digest.toString();
}

/// 获取图片缓存路径
Future getCachePath() async {
Directory dir = await getApplicationDocumentsDirectory();
Directory cachePath = Directory(“${dir.path}/imagecache/”);
if(!cachePath.existsSync()) {
cachePath.createSync();
}
return cachePath.path;
}

/// 判断是否有对应图片缓存文件存在
Future getFileBytes(String url) async {
String cacheDirPath = await getCachePath();
String urlMd5 = getUrlMd5(url);
File file = File(“ c a c h e D i r P a t h / cacheDirPath/ cacheDirPath/urlMd5”);
print(“读取文件:${file.path}”);
if(file.existsSync()) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

需要资料的朋友可以点击我的GitHub免费领取

]

最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

需要资料的朋友可以点击我的GitHub免费领取
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值