// some change.
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();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, listener);
result.addListener(listener);
}
return result;
}
通过以上代码可以看到会通过key来查找缓存中是否存在,如果存在则返回,如果不存在则会通过执行**loader()**方法创建图片资源管理者,而后再将缓存图片资源的监听方法注册到新建的图片管理者中以便图片加载完毕后做缓存处理。
根据上面的代码调用PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);看出load()方法由ImageProvider对象实现,这里就是NetworkImage对象,看下其具体实现代码
@override
ImageStreamCompleter load(NetworkImage key) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
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()) {
return await file.readAsBytes();
}
return null;
}
/// 将下载的图片数据缓存到指定文件
Future saveBytesToFile(String url, Uint8List bytes) 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”);
if(!file.existsSync()) {
file.createSync();
await file.writeAsBytes(bytes);
}
}
}
这样就增加了文件缓存的功能,思路很简单,就是在获取网络图片之前先检查一下本地文件缓存目录中是否有缓存文件,如果有则不用再去下载,否则去下载图片,下载完成后立即将下载到的图片缓存到文件中供下次需要时使用。
工程的pubspec.yaml中需要增加以下依赖库
dependencies:
path_provider: ^0.4.1
crypto: ^2.0.6
自定义ImageProvider使用
在创建图片Widget时使用带参数的非命名构造函数,指定image参数为自定义ImageProvider对象即可,代码示例如下
import ‘imageloader/network_image.dart’ as network;
Widget getNetworkImage() {
return Container(
color: Colors.blue,
width: 200,
height: 200,
child: Image(image: network.NetworkImage(“https://flutter.dev/images/flutter-mono-81x100.png”)),
);
}
写在最后
以上对Flutter中自带的Image小部件的网络图片加载流程进行了源码分析,了解了源码的设计思路之后,吃货新增了简单的本地文件缓存功能,这使我们的网络图片加载同时具备了内存缓存和文件缓存两种能力,大大提升了用户体验。
有更多其他想法的朋友,欢迎[加vx:mm1591314250,一起交流技术。吃货还整理了一份Android高级工程师技术大纲以及一套系统全面而且非常深入的Android进阶资料,需要的可以进群,找管理领取。
结尾
- 腾讯T4级别Android架构技术脑图;查漏补缺,体系化深入学习提升
- 一线互联网Android面试题含详解(初级到高级专题)
这些题目是今年群友去腾讯、百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。并且大多数都整理了答案,熟悉这些知识点会大大增加通过前两轮技术面试的几率
有Android开发3-5年基础,希望突破瓶颈,成为架构师的小伙伴,可以关注我
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
id面试题含详解(初级到高级专题)**
这些题目是今年群友去腾讯、百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。并且大多数都整理了答案,熟悉这些知识点会大大增加通过前两轮技术面试的几率
[外链图片转存中…(img-4Yhg5J3c-1714963776901)]
有Android开发3-5年基础,希望突破瓶颈,成为架构师的小伙伴,可以关注我
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!