Flutter中⽹络图⽚加载和缓存的实现
前⾔
应⽤开发中经常会碰到⽹络图⽚的加载,通常我们会对图⽚进⾏缓存,以便下次加载同⼀张图⽚时不⽤再重新下载,在包含有⼤量图⽚的应⽤中,会⼤幅提⾼图⽚展现速度、提升⽤户体验且为⽤户节省流量。Flutter本⾝提供的Image Widget已经实现了加载⽹络图⽚的功能,且具备内存缓存的机制,接下来⼀起看⼀下Image的⽹络图⽚加载的实现。
重温⼩部件Image
常⽤⼩部件Image中实现了⼏种构造函数,已经⾜够我们⽇常开发中各种场景下创建Image对象使⽤了。
有参构造函数:
Image(Key key, @required this.image, ...)
开发者可根据⾃定义的ImageProvider来创建Image。中国的面积是多少
命名构造函数:
Imagework(String src, ...)
src即是根据⽹络获取的图⽚url地址。
Image.file(File file, ...)
file指本地⼀个图⽚⽂件对象,安卓中需要android.permission.READ_EXTERNAL_STORAGE权限。
Image.ast(String name, ...)
name指项⽬中添加的图⽚资源名,事先在pubspec.yaml⽂件中有声明。
<(Uint8List bytes, ...)
bytes指内存中的图⽚数据,将其转化为图⽚对象。
其中Imagework就是我们本篇分享的重点 -- 加载⽹络图⽚。
Imagework源码分析
下⾯通过源码我们来看下Imagework加载⽹络图⽚的具体实现。
Imagework(String src, {
Key key,
double scale = 1.0,
现代管理学之父.
.
}) : image = NetworkImage(src, scale: scale, headers: headers),
asrt(alignment != null),
asrt(repeat != null),
asrt(matchTextDirection != null),
super(key: key);
/// The image to display.
文化与翻译的关系final ImageProvider image;
⾸先,使⽤Imagework命名构造函数创建Image对象时,会同时初始化实例变量image,image是⼀个ImageProvider对象,该ImageProvider就是我们所需要的图⽚的提供者,它本⾝是⼀个抽象类,⼦类包括NetworkImage、FileImage、ExactAstImage、AstImage、MemoryImage等,⽹络加载图⽚使⽤的就是NetworkImage。
Image作为⼀个StatefulWidget其状态由_ImageState控制,_ImageState继承⾃State类,其⽣命周期⽅法包括initState()、didChangeDependencies()、build()、deactivate()、dispo()、didUpdateWidget()等。我们重点来_ImageState中函数的执⾏。
由于插⼊渲染树时会先调⽤initState()函数,然后调⽤didChangeDependencies()函数,_ImageState中并没有重写initState()函数,所以didChangeDependencies()函数会执⾏,看下didChangeDependencies()⾥的内容
@override
void didChangeDependencies() {
_invertColors = MediaQuery.of(context, nullOk: true)?.invertColors
SemanticsBinding.instance.accessibilityFeatures.invertColors;
啤酒成分_resolveImage();
if (TickerMode.of(context))
_listenToStream();
el
_stopListeningToStream();
super.didChangeDependencies();
}
_resolveImage()会被调⽤,函数内容如下
void _resolveImage() {
final ImageStream newStream =
solve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null
));
asrt(newStream != null);
_updateSourceStream(newStream);
}
函数中先创建了⼀个ImageStream对象,该对象是⼀个图⽚资源的句柄,其持有着图⽚资源加载完毕后的监听回调和图⽚资源的管理者。⽽其中的ImageStreamCompleter对象就是图⽚资源的⼀个管理类,也就是说,_ImageState通过ImageStream和ImageStreamCompleter管理类建⽴了联系。
再回头看⼀下ImageStream对象是通过solve⽅法创建的,也就是对应NetworkImage的resolve⽅法,我们查看NetworkImage类的源码发现并没有resolve⽅法,于是查找其⽗类,在ImageProvider类中找到了。
ImageStream resolve(ImageConfiguration configuration) {
asrt(configuration != null);
final ImageStream stream = ImageStream();
T obtainedKey;
Future<void> handleError(dynamic exception, StackTrace stack) async {
.
.
}
obtainKey(configuration).then<void>((T key) {
obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbnt(key, () => load(key), onError: handleError);
if (completer != null) {
stream.tCompleter(completer);
}
}).catchError(handleError);
return stream;
}
ImageStream中的图⽚管理者ImageStreamCompleter通过PaintingBinding.instance.imageCache.putIfAbnt(key, () =>
load(key), onError: handleError);⽅法创建,imageCache是Flutter框架中实现的⽤于图⽚缓存的单例,查看其中的putIfAbnt ⽅法
ImageStreamCompleter putIfAbnt(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
asrt(key != null);
asrt(loader != null);
ImageStreamCompleter result = _pendingImages[key]?.completer;
// Nothing needs to be done becau the image hasn't loaded yet.
if (result != null)
return result;
// Remove the provider from the list so that we can move it to the
// recently ud position below.
final _CachedImage image = _ve(key);
if (image != null) {
_cache[key] = image;
pleter;
}
try {
result = loader();
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} el {
rethrow;
}
}
void listener(ImageInfo info, bool syncCall) {
// Images that fail to load don't contribute to cache size.
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
// If the image is bigger than the maximum cache size, and the cache size
// is not zero, then increa the cache size to the size of the image plus
// some change.
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
final _PendingImage pendingImage = _ve(key);
if (pendingImage != null) {
}
_cache[key] = image;
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, listener);
result.addListener(listener);
}
return result;
}
通过以上代码可以看到会通过key来查找缓存中是否存在,如果存在则返回,如果不存在则会通过执⾏loader()⽅法创建图⽚资源管理者,⽽后再将缓存图⽚资源的监听⽅法注册到新建的图⽚管理者中以便图⽚加载完毕后做缓存处理。
根据上⾯的代码调⽤PaintingBinding.instance.imageCache.putIfAbnt(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⽅法的返回值初始化,查看该⽅法内容
小学四年级奥数题100道及答案static final HttpClient _httpClient = HttpClient();
Future<ui.Codec> _loadAsync(NetworkImage key) async {
asrt(key == this);
final Uri resolved = solve(key.url);
final HttpClientRequest request = await _Url(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientRespon respon = await request.clo();
if (respon.statusCode != HttpStatus.ok)
throw Exception('HTTP request failed, statusCode: ${respon?.statusCode}, $resolved');
final Uint8List bytes = await consolidateHttpClientResponBytes(respon);
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
}) : asrt(codec != null),
_informationCollector = informationCollector,
_scale = scale,
_framesEmitted = 0,
_timer = null {
codec.then<void>(_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;
asrt(_codec != null);
_decodeNextFrameAndSchedule();
}
⽅法中会将codec对象保存起来,然后解码图⽚帧
Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _NextFrame();
} 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) {
tImage(imageInfo);
_framesEmitted += 1;
}
/// Calls all the registered listeners to notify them of a new image.
@protected
void tImage(ImageInfo image) {
_currentImage = image;
if (_listeners.isEmpty)
return;
final List<ImageListener> localListeners = _listeners.map<ImageListener>(
(_ImageListenerPair listenerPair) => listenerPair.listener
).toList();
for (ImageListener listener in localListeners) {
try {
listener(image, fal);
} 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) {
tState(() {
涡轮式搅拌器_imageInfo = imageInfo;
});
}
最终就是调⽤tState⽅法来通知界⾯刷新,将下载到的图⽚渲染到界⾯上来了。
实际问题
从以上源码分析,我们应该清楚了整个⽹络图⽚从加载到显⽰的过程,不过使⽤这种原⽣的⽅式我们发现⽹络图⽚只是进⾏了内存缓存,如果杀掉应⽤进程再重新打开后还是要重新下载图⽚,这对于⽤户⽽⾔,每次打开应⽤还是会消耗下载图⽚的流量,不过我们可以从中学习到⼀些思路来⾃⼰设计⽹
络图⽚加载框架,下⾯作者就简单的基于Imagework来进⾏⼀下改造,增加图⽚的磁盘缓存。
解决⽅案
我们通过源码分析可知,图⽚在缓存中未找到时,会通过⽹络直接下载获取,⽽下载的⽅法是在NetworkImage类中,于是我们可以参考NetworkImage来⾃定义⼀个ImageProvider。
代码实现
拷贝⼀份NetworkImage的代码到新建的network_image.dart⽂件中,在_loadAsync⽅法中我们加⼊磁盘缓存的代码。蝴蝶飞啊
static final CacheFileImage _cacheFileImage = CacheFileImage();
Future<ui.Codec> _loadAsync(NetworkImage key) async {
asrt(key == this);
/// 新增代码块start
/// 从缓存⽬录中查找图⽚是否存在
final Uint8List cacheBytes = await _FileBytes(key.url);
if(cacheBytes != null) {
return PaintingBinding.instance.instantiateImageCodec(cacheBytes);
}
/// 新增代码块end
final Uri resolved = solve(key.url);
final HttpClientRequest request = await _Url(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientRespon respon = await request.clo();
if (respon.statusCode != HttpStatus.ok)
throw Exception('HTTP request failed, statusCode: ${respon?.statusCode}, $resolved');