找回密码
 会员注册
查看: 25|回复: 0

Fluttercached_network_image图片加载流程分析

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64454
发表于 2024-10-11 20:14:11 | 显示全部楼层 |阅读模式
前言 一天测试小姐姐拿着手机过来说,你这里图片下载有问题呀,为什么没有网络(开飞行模式)也弹Toast提示下载成功呀?下意识反应,肯定是Toast提示弹早了,刚点击按钮,还没开始下载就弹了Toast,赶紧拿手机过来操作验证一波。确实没有网络,弹了下载完成提示,去相册检查一下,嗯?图片下载成功了,还有这种操作?赶紧检查一下代码,发现项目中使用的cached_network_image三方库加载的图片,从名字上可以判断,这是一个缓存网络图片的加载框架。所以应该是图片显示出来以后就被缓存到本地了,实际下载的流程并未走网络请求,为了验证想法,看了下框架加载图片流程,总结出下文。使用 组件CachedNetworkImage可以支持直接使用或者通过ImageProvider。引入依赖dependencies:cached_network_image:^3.1.0执行flutter pub get,项目中使用Import itimport'package:cached_network_image/cached_network_image.dart';添加占位图CachedNetworkImage(imageUrl:"http://via.placeholder.com/350x150",placeholdercontext,url)=>CircularProgressIndicator(),errorWidgetcontext,url,error)=>Icon(Icons.error),),进度条展示CachedNetworkImage(imageUrl:"http://via.placeholder.com/350x150",progressIndicatorBuildercontext,url,downloadProgress)=>CircularProgressIndicator(value:downloadProgress.progress),errorWidgetcontext,url,error)=>Icon(Icons.error),),原生组件Image配合Image(image:CachedNetworkImageProvider(url))使用占位图并提供provider给其他组件使用CachedNetworkImage(imageUrl:"http://via.placeholder.com/200x150",imageBuildercontext,imageProvider)=>Container(decoration:BoxDecoration(imageecorationImage(image:imageProvider,fit:BoxFit.cover,colorFilter:ColorFilter.mode(Colors.red,BlendMode.colorBurn)),),),placeholdercontext,url)=>CircularProgressIndicator(),errorWidgetcontext,url,error)=>Icon(Icons.error),),这样就可以加载网络图片了,而且,图片加载完成时,就被缓存到本地了,首先看下图片的加载流程官网说了,它现在不包含缓存,缓存功能实际上是另一个库flutter_cache_manager中实现的原理 加载&显示这里我们仅梳理图片加载和缓存的主流程,对于一些其他分支流程,或无关参数不做过多分析首先,页面上使用的构造函数接收了一个必传参数imageUrl,用于生成ImageProvider提供图片加载classCachedNetworkImageextendsStatelessWidget{///image提供finalCachedNetworkImageProvider_image;///构造函数CachedNetworkImage({Keykey,@requiredthis.imageUrl,///省略部分this.cacheManager,///...}):assert(imageUrl!=null),///..._image=CachedNetworkImageProvider(imageUrl,headers:httpHeaders,cacheManager:cacheManager,cacheKey:cacheKey,imageRenderMethodForWeb:imageRenderMethodForWeb,maxWidth:maxWidthDiskCache,maxHeight:maxHeightDiskCache,),super(key:key);@overrideWidgetbuild(BuildContextcontext){varoctoPlaceholderBuilder=placeholder!=null_octoPlaceholderBuilder:null;varoctoProgressIndicatorBuilder=progressIndicatorBuilder!=null_octoProgressIndicatorBuilder:null;///...returnOctoImage(image:_image,///...);}}这里可以看到,构造函数初始化了一个本地变量_image 类型是CachedNetworkImageProvider,它继承ImageProvider提供图片加载,看下它的构造函数///提供网络图片加载Provider并缓存abstractclassCachedNetworkImageProviderextendsImageProvider{///CreatesanobjectthatfetchestheimageatthegivenURL.constfactoryCachedNetworkImageProvider(Stringurl,{intmaxHeight,intmaxWidth,StringcacheKey,doublescale,@Deprecated('ErrorListenerisdeprecated,uselistenersontheimagestream')ErrorListenererrorListener,Mapheaders,BaseCacheManagercacheManager,ImageRenderMethodForWebimageRenderMethodForWeb,})=image_provider.CachedNetworkImageProvider;///可选cacheManager.默认使用DefaultCacheManager()///当运行在web时,cacheManager没有使用.BaseCacheManagergetcacheManager;///请求url.Stringgeturl;///缓存keyStringgetcacheKey;///...@overrideImageStreamCompleterload(CachedNetworkImageProviderkey,DecoderCallbackdecode);}它的构造函数调用了image_provider.CachedNetworkImageProvider的实例在_image_provider_io.dart中是加载的具体实现类///IOimplementationoftheCachedNetworkImageProvider;theImageProviderto///loadnetworkimagesusingacache.classCachedNetworkImageProviderextendsImageProviderimplementsimage_provider.CachedNetworkImageProvider{///CreatesanImageProviderwhichloadsanimagefromthe[url],usingthe[scale].///Whentheimagefailstoload[errorListener]iscalled.constCachedNetworkImageProvider(this.url,{///...}):assert(url!=null),assert(scale!=null);@overridefinalBaseCacheManagercacheManager;///...@overrideFutureobtainKey(ImageConfigurationconfiguration){returnSynchronousFuture(this);}///核心方法加载图片入口@overrideImageStreamCompleterload(image_provider.CachedNetworkImageProviderkey,DecoderCallbackdecode){finalchunkEvents=StreamController();///多图加载returnMultiImageStreamCompleter(codec:_loadAsync(key,chunkEvents,decode),chunkEvents:chunkEvents.stream,scale:key.scale,informationCollector)sync*{yieldDiagnosticsProeperty('Imageproviderthis\nImagekeykey',this,styleiagnosticsTreeStyle.errorProperty,);},);}这里的load方法即是图片加载的启动入口,它会在页面可见时被调用它返回了一个MultiImageStreamCompleter传入_loadAsync,看下这个方法///异步加载Stream_loadAsync(CachedNetworkImageProviderkey,StreamControllerchunkEvents,DecoderCallbackdecode,)async*{assert(key==this);try{///默认缓存管理器varmngr=cacheManagerDefaultCacheManager();assert(mngrisImageCacheManager||(maxWidth==null&maxHeight==null),'ToresizetheimagewithaCacheManagerthe''CacheManagerneedstobeanImageCacheManager.maxWidthand''maxHeightwillbeignoredwhenanormalCacheManagerisused.');///下载逻辑放在ImageCacheManager,得到下载streamvarstream=mngrisImageCacheManagermngr.getImageFile(key.url,maxHeight:maxHeight,maxWidth:maxWidth,withProgress:true,headers:headers,key:key.cacheKey):mngr.getFileStream(key.url,withProgress:true,headers:headers,key:key.cacheKey);awaitfor(varresultinstream){if(resultisFileInfo){varfile=result.file;varbytes=awaitfile.readAsBytes();vardecoded=awaitdecode(bytes);///下载完成返回结果yielddecoded;}}}catch(e){///...}finally{awaitchunkEvents.close();}}}这里我们看到了默认缓存管理器cacheManager创建的地方,为DefaultCacheManager,那么它如何缓存的呢,后边再分析。下载的逻辑也是放在了ImageCacheManager下了,返回结果是一个stream完成多图下载的支持,下载完成通过yield 返回给ui解码最终显示。MultiImageStreamCompleter支持多图加载继承自ImageStreamCompleter///AnImageStreamCompleterwithsupportforloadingmultipleimages.classMultiImageStreamCompleterextendsImageStreamCompleter{///TheconstructortocreateanMultiImageStreamCompleter.The[codec]///shouldbeastreamwiththeimagesthatshouldbeshown.The///[chunkEvents]shouldindicatethe[ImageChunkEvent]softhefirstimage///toshow.MultiImageStreamCompleter({@requiredStreamcodec,@requireddoublescale,StreamchunkEvents,InformationCollectorinformationCollector,}):assert(codec!=null),_informationCollector=informationCollector,_scale=scale{///显示逻辑codec.listen((event){if(_timer!=null){_nextImageCodec=event;}else{_handleCodecReady(event);}},onErrordynamicerror,StackTracestack){reportError(context:ErrorDescription('resolvinganimagecodec'),exception:error,stack:stack,informationCollector:informationCollector,silent:true,);});///...}}///处理解码完成void_handleCodecReady(ui.Codeccodec){_codec=codec;assert(_codec!=null);if(hasListeners){_decodeNextFrameAndSchedule();}}///解码下一帧并绘制Future_decodeNextFrameAndSchedule()async{try{_nextFrame=await_codec.getNextFrame();}catch(exception,stack){reportError(context:ErrorDescription('resolvinganimageframe'),exception:exception,stack:stack,informationCollector:_informationCollector,silent:true,);return;}if(_codec.frameCount==1){//ImageStreamCompleterlistenersremovedwhilewaitingfornextframeto//bedecoded.//There'snoreasontoemittheframewithoutactivelisteners.if(!hasListeners){return;}//Thisisnotananimatedimage,justreturnitanddon'tschedulemore//frames._emitFrame(ImageInfo(image:_nextFrame.image,scale:_scale));return;}_scheduleAppFrame();}}这里做了显示逻辑,和最终转化成flutter上帧的处理,_scheduleAppFrame完成发送帧的处理下载&缓存上边的mngr调用了ImageCacheManager中的getImageFile方法现在就到了flutter_cache_manager这个三方库当中,它是被隐式依赖的,文件是image_cache_manager.dartmixinImageCacheManageronBaseCacheManager{StreamgetImageFile(Stringurl,{Stringkey,Mapheaders,boolwithProgress,intmaxHeight,intmaxWidth,})async*{if(maxHeight==null&maxWidth==null){yield*getFileStream(url,key:key,headers:headers,withProgress:withProgress);return;}///...}///...}getFileStream方法实现在子类cache_manager.dart文件中的CacheManagerclassCacheManagerimplementsBaseCacheManager{///缓存管理CacheStore_store;///GettheunderlyingstorehelperCacheStoregetstore=>_store;///下载管理WebHelper_webHelper;///GettheunderlyingwebhelperWebHelpergetwebHelper=>_webHelper;///从下载或者缓存读取file返回stream@overrideStreamgetFileStream(Stringurl,{Stringkey,Mapheaders,boolwithProgress}){key=url;finalstreamController=StreamController();_pushFileToStream(streamController,url,key,headers,withProgressfalse);returnstreamController.stream;}Future_pushFileToStream(StreamControllerstreamController,Stringurl,Stringkey,Mapheaders,boolwithProgress)async{key=url;FileInfocacheFile;try{///缓存判断cacheFile=awaitgetFileFromCache(key);if(cacheFile!=null){///有缓存直接返回streamController.add(cacheFile);withProgress=false;}}catch(e){print('CacheManager:Failedtoloadcachedfilefor$urlwitherror:\n$e');}///没有缓存或者过期下载if(cacheFile==null||cacheFile.validTill.isBefore(DateTime.now())){try{awaitfor(varresponsein_webHelper.downloadFile(url,key:key,authHeaders:headers)){if(responseisDownloadProgress&withProgress){streamController.add(response);}if(responseisFileInfo){streamController.add(response);}}}catch(e){assert((){print('CacheManager:Failedtodownloadfilefrom$urlwitherror:\n$e');returntrue;}());if(cacheFile==null&streamController.hasListener){streamController.addError(e);}}}unawaited(streamController.close());}}缓存判断逻辑在CacheStore提供两级缓存classCacheStore{DurationcleanupRunMinInterval=constDuration(seconds:10);///未下载完成缓存final_futureCache=>{};///已下载完缓存final_memCache={};///...FuturegetFile(Stringkey,{boolignoreMemCache=false})async{finalcacheObject=awaitretrieveCacheData(key,ignoreMemCache:ignoreMemCache);if(cacheObject==null||cacheObject.relativePath==null){returnnull;}finalfile=awaitfileSystem.createFile(cacheObject.relativePath);returnFileInfo(file,FileSource.Cache,cacheObject.validTill,cacheObject.url,);}FutureputFile(CacheObjectcacheObject)async{_memCache[cacheObject.key]=cacheObject;await_updateCacheDataInDatabase(cacheObject);}FutureretrieveCacheData(Stringkey,{boolignoreMemCache=false})async{///判断是否已缓存过if(!ignoreMemCache&_memCache.containsKey(key)){if(await_fileExists(_memCache[key])){return_memCache[key];}}///未缓存的已加入futureCache中的key直接返回if(!_futureCache.containsKey(key)){finalcompleter=Completer();///未加入的添加到futureCacheunawaited(_getCacheDataFromDatabase(key).then((cacheObject)async{if(cacheObject!=null&!await_fileExists(cacheObject)){finalprovider=await_cacheInfoRepository;awaitprovider.delete(cacheObject.id);cacheObject=null;}_memCache[key]=cacheObject;completer.complete(cacheObject);unawaited(_futureCache.remove(key));}));_futureCache[key]=completer.future;}return_futureCache[key];}///...///更新到数据库Future_updateCacheDataInDatabase(CacheObjectcacheObject)async{finalprovider=await_cacheInfoRepository;returnprovider.updateOrInsert(cacheObject);}}_cacheInfoRepository缓存仓库是CacheObjectProvider使用的数据库缓存对象classCacheObjectProviderextendsCacheInfoRepositorywithCacheInfoRepositoryHelperMethods{Databasedb;String_path;StringdatabaseName;CacheObjectProvider({Stringpath,this.databaseName}):_path=path;///打开@overrideFutureopen()async{if(!shouldOpenOnNewConnection()){returnopenCompleter.future;}varpath=await_getPath();awaitFile(path).parent.create(recursive:true);db=awaitopenDatabase(path,version:3,onCreateDatabasedb,intversion)async{awaitdb.execute('''createtable$_tableCacheObject(${CacheObject.columnId}integerprimarykey,${CacheObject.columnUrl}text,${CacheObject.columnKey}text,${CacheObject.columnPath}text,${CacheObject.columnETag}text,${CacheObject.columnValidTill}integer,${CacheObject.columnTouched}integer,${CacheObject.columnLength}integer);createuniqueindex$_tableCacheObject${CacheObject.columnKey}ON$_tableCacheObject(${CacheObject.columnKey});''');},onUpgrade:(Databasedb,intoldVersion,intnewVersion)async{///...returnopened();}@overrideFutureupdateOrInsert(CacheObjectcacheObject){if(cacheObject.id==null){returninsert(cacheObject);}else{returnupdate(cacheObject);}}@overrideFutureinsert(CacheObjectcacheObject,{boolsetTouchedToNow=true})async{varid=awaitdb.insert(_tableCacheObject,cacheObject.toMap(setTouchedToNow:setTouchedToNow),);returncacheObject.copyWith(id:id);}@overrideFutureget(Stringkey)async{Listmaps=awaitdb.query(_tableCacheObject,columns:null,where:'${CacheObject.columnKey}=',whereArgs:[key]);if(maps.isNotEmpty){returnCacheObject.fromMap(maps.first.cast());}returnnull;}@overrideFuturedelete(intid){returndb.delete(_tableCacheObject,where:'${CacheObject.columnId}=',whereArgs:[id]);}}可见数据库缓存的是CacheObject对象,保存了url、key、relativePath等信息classCacheObject{staticconstcolumnId='_id';staticconstcolumnUrl='url';staticconstcolumnKey='key';staticconstcolumnPath='relativePath';staticconstcolumnETag='eTag';staticconstcolumnValidTill='validTill';staticconstcolumnTouched='touched';staticconstcolumnLength='length';}没有缓存下调用了_webHelper.downloadFile方法classWebHelper{WebHelper(this._store,FileServicefileFetcher):_memCache={},fileFetcher=fileFetcherHttpFileService();finalCacheStore_store;@visibleForTestingfinalFileServicefileFetcher;finalMap>_memCache;finalQueue_queue=Queue();///DownloadthefilefromtheurlStreamdownloadFile(Stringurl,{Stringkey,MapauthHeaders,boolignoreMemCache=false}){key=url;if(!_memCache.containsKey(key)||ignoreMemCache){varsubject=BehaviorSubject();_memCache[key]=subject;///下载或者加入队列unawaited(_downloadOrAddToQueue(url,key,authHeaders));}return_memCache[key].stream;}Future_downloadOrAddToQueue(Stringurl,Stringkey,MapauthHeaders,)async{//如果太多请求被执行,加入队列等待if(concurrentCalls>=fileFetcher.concurrentFetches){_queue.add(QueueItem(url,key,authHeaders));return;}concurrentCalls++;varsubject=_memCache[key];try{awaitfor(varresultin_updateFile(url,key,authHeaders:authHeaders)){subject.add(result);}}catch(e,stackTrace){subject.addError(e,stackTrace);}finally{concurrentCalls--;awaitsubject.close();_memCache.remove(key);_checkQueue();}}///下载资源Stream_updateFile(Stringurl,Stringkey,{MapauthHeaders})async*{varcacheObject=await_store.retrieveCacheData(key);cacheObject=cacheObject==nullCacheObject(url,key:key):cacheObject.copyWith(url:url);///请求得到responsefinalresponse=await_download(cacheObject,authHeaders);yield*_manageResponse(cacheObject,response);}Stream_manageResponse(CacheObjectcacheObject,FileServiceResponseresponse)async*{///...if(statusCodesNewFile.contains(response.statusCode)){intsavedBytes;awaitfor(varprogressin_saveFile(newCacheObject,response)){savedBytes=progress;yieldDownloadProgress(cacheObject.url,response.contentLength,progress);}newCacheObject=newCacheObject.copyWith(length:savedBytes);}///加入缓存unawaited(_store.putFile(newCacheObject).then((_){if(newCacheObject.relativePath!=oldCacheObject.relativePath){_removeOldFile(oldCacheObject.relativePath);}}));finalfile=await_store.fileSystem.createFile(newCacheObject.relativePath,);yieldFileInfo(file,FileSource.Online,newCacheObject.validTill,newCacheObject.url,);}Stream_saveFile(CacheObjectcacheObject,FileServiceResponseresponse){varreceivedBytesResultController=StreamController();unawaited(_saveFileAndPostUpdates(receivedBytesResultController,cacheObject,response,));returnreceivedBytesResultController.stream;}Future_saveFileAndPostUpdates(StreamControllerreceivedBytesResultController,CacheObjectcacheObject,FileServiceResponseresponse)async{///根据路径创建filefinalfile=await_store.fileSystem.createFile(cacheObject.relativePath);try{varreceivedBytes=0;///写文件finalsink=file.openWrite();awaitresponse.content.map((s){receivedBytes+=s.length;receivedBytesResultController.add(receivedBytes);returns;}).pipe(sink);}catch(e,stacktrace){receivedBytesResultController.addError(e,stacktrace);}awaitreceivedBytesResultController.close();}}总结 cached_network_image图片加载流程依赖ImageProvider,缓存和下载逻辑放在另一个库flutter_cache_manager下载文件在WebHelper中提供队列管理,依赖传入FileService做具体获取文件方便扩展默认实现HttpFileService,下载完成后路径保存在CacheObject保存在sqflite数据库
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:54 , Processed in 0.869324 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表