Flutter中网络图片加载和缓存的实现
前言
应用开发中经常会碰到网络图片的加载,通常我们会对图片进行缓存,以便下次加载同一张图片时不用再重新下载,在包含有大量图片的应用中,会大幅提高图片展现速度、提升用户体验且为用户节省流量。Flutter本身提供的ImageWidget已经实现了加载网络图片的功能,且具备内存缓存的机制,接下来一起看一下Image的网络图片加载的实现。
重温小部件Image
常用小部件Image中实现了几种构造函数,已经足够我们日常开发中各种场景下创建Image对象使用了。
有参构造函数:
Image(Keykey,@requiredthis.image,...)
开发者可根据自定义的ImageProvider来创建Image。
命名构造函数:
Image.network(Stringsrc,...)
src即是根据网络获取的图片url地址。
Image.file(Filefile,...)
file指本地一个图片文件对象,安卓中需要android.permission.READ_EXTERNAL_STORAGE权限。
Image.asset(Stringname,...)
name指项目中添加的图片资源名,事先在pubspec.yaml文件中有声明。
Image.memory(Uint8Listbytes,...)
bytes指内存中的图片数据,将其转化为图片对象。
其中Image.network就是我们本篇分享的重点--加载网络图片。
Image.network源码分析
下面通过源码我们来看下Image.network加载网络图片的具体实现。
Image.network(Stringsrc,{
Keykey,
doublescale=1.0,
.
.
}):image=NetworkImage(src,scale:scale,headers:headers),
assert(alignment!=null),
assert(repeat!=null),
assert(matchTextDirection!=null),
super(key:key);
///Theimagetodisplay.
finalImageProviderimage;
首先,使用Image.network命名构造函数创建Image对象时,会同时初始化实例变量image,image是一个ImageProvider对象,该ImageProvider就是我们所需要的图片的提供者,它本身是一个抽象类,子类包括NetworkImage、FileImage、ExactAssetImage、AssetImage、MemoryImage等,网络加载图片使用的就是NetworkImage。
Image作为一个StatefulWidget其状态由_ImageState控制,_ImageState继承自State类,其生命周期方法包括initState()、didChangeDependencies()、build()、deactivate()、dispose()、didUpdateWidget()等。我们重点来_ImageState中函数的执行。
由于插入渲染树时会先调用initState()函数,然后调用didChangeDependencies()函数,_ImageState中并没有重写initState()函数,所以didChangeDependencies()函数会执行,看下didChangeDependencies()里的内容
@override
voiddidChangeDependencies(){
_invertColors=MediaQuery.of(context,nullOk:true)?.invertColors
??SemanticsBinding.instance.accessibilityFeatures.invertColors;
_resolveImage();
if(TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream();
super.didChangeDependencies();
}
_resolveImage()会被调用,函数内容如下
void_resolveImage(){
finalImageStreamnewStream=
widget.image.resolve(createLocalImageConfiguration(
context,
size:widget.width!=null&&widget.height!=null?Size(widget.width,widget.height):null
));
assert(newStream!=null);
_updateSourceStream(newStream);
}
函数中先创建了一个ImageStream对象,该对象是一个图片资源的句柄,其持有着图片资源加载完毕后的监听回调和图片资源的管理者。而其中的ImageStreamCompleter对象就是图片资源的一个管理类,也就是说,_ImageState通过ImageStream和ImageStreamCompleter管理类建立了联系。
再回头看一下ImageStream对象是通过widget.image.resolve方法创建的,也就是对应NetworkImage的resolve方法,我们查看NetworkImage类的源码发现并没有resolve方法,于是查找其父类,在ImageProvider类中找到了。
ImageStreamresolve(ImageConfigurationconfiguration){
assert(configuration!=null);
finalImageStreamstream=ImageStream();
TobtainedKey;
FuturehandleError(dynamicexception,StackTracestack)async{
.
.
}
obtainKey(configuration).then((Tkey){
obtainedKey=key;
finalImageStreamCompletercompleter=PaintingBinding.instance.imageCache.putIfAbsent(key,()=>load(key),onError:handleError);
if(completer!=null){
stream.setCompleter(completer);
}
}).catchError(handleError);
returnstream;
}
ImageStream中的图片管理者ImageStreamCompleter通过PaintingBinding.instance.imageCache.putIfAbsent(key,()=>load(key),onError:handleError);方法创建,imageCache是Flutter框架中实现的用于图片缓存的单例,查看其中的putIfAbsent方法
ImageStreamCompleterputIfAbsent(Objectkey,ImageStreamCompleterloader(),{ImageErrorListeneronError}){
assert(key!=null);
assert(loader!=null);
ImageStreamCompleterresult=_pendingImages[key]?.completer;
//Nothingneedstobedonebecausetheimagehasn'tloadedyet.
if(result!=null)
returnresult;
//Removetheproviderfromthelistsothatwecanmoveittothe
//recentlyusedpositionbelow.
final_CachedImageimage=_cache.remove(key);
if(image!=null){
_cache[key]=image;
returnimage.completer;
}
try{
result=loader();
}catch(error,stackTrace){
if(onError!=null){
onError(error,stackTrace);
returnnull;
}else{
rethrow;
}
}
voidlistener(ImageInfoinfo,boolsyncCall){
//Imagesthatfailtoloaddon'tcontributetocachesize.
finalintimageSize=info?.image==null?0:info.image.height*info.image.width*4;
final_CachedImageimage=_CachedImage(result,imageSize);
//Iftheimageisbiggerthanthemaximumcachesize,andthecachesize
//isnotzero,thenincreasethecachesizetothesizeoftheimageplus
//somechange.
if(maximumSizeBytes>0&&imageSize>maximumSizeBytes){
_maximumSizeBytes=imageSize+1000;
}
_currentSizeBytes+=imageSize;
final_PendingImagependingImage=_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);
}
returnresult;
}
通过以上代码可以看到会通过key来查找缓存中是否存在,如果存在则返回,如果不存在则会通过执行loader()方法创建图片资源管理者,而后再将缓存图片资源的监听方法注册到新建的图片管理者中以便图片加载完毕后做缓存处理。
根据上面的代码调用PaintingBinding.instance.imageCache.putIfAbsent(key,()=>load(key),onError:handleError);看出load()方法由ImageProvider对象实现,这里就是NetworkImage对象,看下其具体实现代码
@override
ImageStreamCompleterload(NetworkImagekey){
returnMultiFrameImageStreamCompleter(
codec:_loadAsync(key),
scale:key.scale,
informationCollector:(StringBufferinformation){
information.writeln('Imageprovider:$this');
information.write('Imagekey:$key');
}
);
}
代码中其就是创建一个MultiFrameImageStreamCompleter对象并返回,这是一个多帧图片管理器,表明Flutter是支持GIF图片的。创建对象时的codec变量由_loadAsync方法的返回值初始化,查看该方法内容
staticfinalHttpClient_httpClient=HttpClient(); Future_loadAsync(NetworkImagekey)async{ assert(key==this); finalUriresolved=Uri.base.resolve(key.url); finalHttpClientRequestrequest=await_httpClient.getUrl(resolved); headers?.forEach((Stringname,Stringvalue){ request.headers.add(name,value); }); finalHttpClientResponseresponse=awaitrequest.close(); if(response.statusCode!=HttpStatus.ok) throwException('HTTPrequestfailed,statusCode:${response?.statusCode},$resolved'); finalUint8Listbytes=awaitconsolidateHttpClientResponseBytes(response); if(bytes.lengthInBytes==0) throwException('NetworkImageisanemptyfile:$resolved'); returnPaintingBinding.instance.instantiateImageCodec(bytes); }
这里才是关键,就是通过HttpClient对象对指定的url进行下载操作,下载完成后根据图片二进制数据实例化图像编解码器对象Codec,然后返回。
那么图片下载完成后是如何显示到界面上的呢,下面看下MultiFrameImageStreamCompleter的构造方法实现
MultiFrameImageStreamCompleter({
@requiredFuturecodec,
@requireddoublescale,
InformationCollectorinformationCollector
}):assert(codec!=null),
_informationCollector=informationCollector,
_scale=scale,
_framesEmitted=0,
_timer=null{
codec.then(_handleCodecReady,onError:(dynamicerror,StackTracestack){
reportError(
context:'resolvinganimagecodec',
exception:error,
stack:stack,
informationCollector:informationCollector,
silent:true,
);
});
}
看,构造方法中的代码块,codec的异步方法执行完成后会调用_handleCodecReady函数,函数内容如下
void_handleCodecReady(ui.Codeccodec){
_codec=codec;
assert(_codec!=null);
_decodeNextFrameAndSchedule();
}
方法中会将codec对象保存起来,然后解码图片帧
Future_decodeNextFrameAndSchedule()async{ try{ _nextFrame=await_codec.getNextFrame(); }catch(exception,stack){ reportError( context:'resolvinganimageframe', exception:exception, stack:stack, informationCollector:_informationCollector, silent:true, ); return; } if(_codec.frameCount==1){ //Thisisnotananimatedimage,justreturnitanddon'tschedulemore //frames. _emitFrame(ImageInfo(image:_nextFrame.image,scale:_scale)); return; } SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); }
如果图片是png或jpg只有一帧,则执行_emitFrame函数,从帧数据中拿到图片帧对象根据缩放比例创建ImageInfo对象,然后设置显示的图片信息
void_emitFrame(ImageInfoimageInfo){
setImage(imageInfo);
_framesEmitted+=1;
}
///Callsalltheregisteredlistenerstonotifythemofanewimage.
@protected
voidsetImage(ImageInfoimage){
_currentImage=image;
if(_listeners.isEmpty)
return;
finalListlocalListeners=_listeners.map(
(_ImageListenerPairlistenerPair)=>listenerPair.listener
).toList();
for(ImageListenerlistenerinlocalListeners){
try{
listener(image,false);
}catch(exception,stack){
reportError(
context:'byanimagelistener',
exception:exception,
stack:stack,
);
}
}
}
这时就会根据添加的监听器来通知一个新的图片需要渲染。那么这个监听器是什么时候添加的呢,我们回头看一下_ImageState类中的didChangeDependencies()方法内容,执行完_resolveImage();后会执行_listenToStream();方法
void_listenToStream(){
if(_isListeningToStream)
return;
_imageStream.addListener(_handleImageChanged);
_isListeningToStream=true;
}
该方法就向ImageStream对象中添加了监听器_handleImageChanged,监听方法如下
void_handleImageChanged(ImageInfoimageInfo,boolsynchronousCall){
setState((){
_imageInfo=imageInfo;
});
}
最终就是调用setState方法来通知界面刷新,将下载到的图片渲染到界面上来了。
实际问题
从以上源码分析,我们应该清楚了整个网络图片从加载到显示的过程,不过使用这种原生的方式我们发现网络图片只是进行了内存缓存,如果杀掉应用进程再重新打开后还是要重新下载图片,这对于用户而言,每次打开应用还是会消耗下载图片的流量,不过我们可以从中学习到一些思路来自己设计网络图片加载框架,下面作者就简单的基于Image.network来进行一下改造,增加图片的磁盘缓存。
解决方案
我们通过源码分析可知,图片在缓存中未找到时,会通过网络直接下载获取,而下载的方法是在NetworkImage类中,于是我们可以参考NetworkImage来自定义一个ImageProvider。
代码实现
拷贝一份NetworkImage的代码到新建的network_image.dart文件中,在_loadAsync方法中我们加入磁盘缓存的代码。
staticfinalCacheFileImage_cacheFileImage=CacheFileImage(); Future_loadAsync(NetworkImagekey)async{ assert(key==this); ///新增代码块start ///从缓存目录中查找图片是否存在 finalUint8ListcacheBytes=await_cacheFileImage.getFileBytes(key.url); if(cacheBytes!=null){ returnPaintingBinding.instance.instantiateImageCodec(cacheBytes); } ///新增代码块end finalUriresolved=Uri.base.resolve(key.url); finalHttpClientRequestrequest=await_httpClient.getUrl(resolved); headers?.forEach((Stringname,Stringvalue){ request.headers.add(name,value); }); finalHttpClientResponseresponse=awaitrequest.close(); if(response.statusCode!=HttpStatus.ok) throwException('HTTPrequestfailed,statusCode:${response?.statusCode},$resolved'); ///新增代码块start ///将下载的图片数据保存到指定缓存文件中 await_cacheFileImage.saveBytesToFile(key.url,bytes); ///新增代码块end returnPaintingBinding.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';
classCacheFileImage{
///获取url字符串的MD5值
staticStringgetUrlMd5(Stringurl){
varcontent=newUtf8Encoder().convert(url);
vardigest=md5.convert(content);
returndigest.toString();
}
///获取图片缓存路径
FuturegetCachePath()async{
Directorydir=awaitgetApplicationDocumentsDirectory();
DirectorycachePath=Directory("${dir.path}/imagecache/");
if(!cachePath.existsSync()){
cachePath.createSync();
}
returncachePath.path;
}
///判断是否有对应图片缓存文件存在
FuturegetFileBytes(Stringurl)async{
StringcacheDirPath=awaitgetCachePath();
StringurlMd5=getUrlMd5(url);
Filefile=File("$cacheDirPath/$urlMd5");
print("读取文件:${file.path}");
if(file.existsSync()){
returnawaitfile.readAsBytes();
}
returnnull;
}
///将下载的图片数据缓存到指定文件
FuturesaveBytesToFile(Stringurl,Uint8Listbytes)async{
StringcacheDirPath=awaitgetCachePath();
StringurlMd5=getUrlMd5(url);
Filefile=File("$cacheDirPath/$urlMd5");
if(!file.existsSync()){
file.createSync();
awaitfile.writeAsBytes(bytes);
}
}
}
这样就增加了文件缓存的功能,思路很简单,就是在获取网络图片之前先检查一下本地文件缓存目录中是否有缓存文件,如果有则不用再去下载,否则去下载图片,下载完成后立即将下载到的图片缓存到文件中供下次需要时使用。
工程的pubspec.yaml中需要增加以下依赖库
dependencies: path_provider:^0.4.1 crypto:^2.0.6
自定义ImageProvider使用
在创建图片Widget时使用带参数的非命名构造函数,指定image参数为自定义ImageProvider对象即可,代码示例如下
import'imageloader/network_image.dart'asnetwork;
WidgetgetNetworkImage(){
returnContainer(
color:Colors.blue,
width:200,
height:200,
child:Image(image:network.NetworkImage("https://flutter.dev/images/flutter-mono-81x100.png")),
);
}
写在最后
以上对Flutter中自带的Image小部件的网络图片加载流程进行了源码分析,了解了源码的设计思路之后,我们新增了简单的本地文件缓存功能,这使我们的网络图片加载同时具备了内存缓存和文件缓存两种能力,大大提升了用户体验,如果其他同学有更好的方案可以给作者留言交流。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。