Flutter中網(wǎng)絡(luò)圖片加載和緩存的實(shí)現(xiàn)
前言
應(yīng)用開發(fā)中經(jīng)常會碰到網(wǎng)絡(luò)圖片的加載,通常我們會對圖片進(jìn)行緩存,以便下次加載同一張圖片時不用再重新下載,在包含有大量圖片的應(yīng)用中,會大幅提高圖片展現(xiàn)速度、提升用戶體驗(yàn)且為用戶節(jié)省流量。Flutter本身提供的Image Widget已經(jīng)實(shí)現(xiàn)了加載網(wǎng)絡(luò)圖片的功能,且具備內(nèi)存緩存的機(jī)制,接下來一起看一下Image的網(wǎng)絡(luò)圖片加載的實(shí)現(xiàn)。
重溫小部件Image
常用小部件Image中實(shí)現(xiàn)了幾種構(gòu)造函數(shù),已經(jīng)足夠我們?nèi)粘i_發(fā)中各種場景下創(chuàng)建Image對象使用了。
有參構(gòu)造函數(shù):
Image(Key key, @required this.image, ...)
開發(fā)者可根據(jù)自定義的ImageProvider來創(chuàng)建Image。
命名構(gòu)造函數(shù):
Image.network(String src, ...)
src即是根據(jù)網(wǎng)絡(luò)獲取的圖片url地址。
Image.file(File file, ...)
file指本地一個圖片文件對象,安卓中需要android.permission.READ_EXTERNAL_STORAGE權(quán)限。
Image.asset(String name, ...)
name指項(xiàng)目中添加的圖片資源名,事先在pubspec.yaml文件中有聲明。
Image.memory(Uint8List bytes, ...)
bytes指內(nèi)存中的圖片數(shù)據(jù),將其轉(zhuǎn)化為圖片對象。
其中Image.network就是我們本篇分享的重點(diǎn) -- 加載網(wǎng)絡(luò)圖片。
Image.network源碼分析
下面通過源碼我們來看下Image.network加載網(wǎng)絡(luò)圖片的具體實(shí)現(xiàn)。
Image.network(String src, { Key key, double scale = 1.0, . . }) : image = NetworkImage(src, scale: scale, headers: headers), assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), super(key: key); /// The image to display. final ImageProvider image;
首先,使用Image.network命名構(gòu)造函數(shù)創(chuàng)建Image對象時,會同時初始化實(shí)例變量image,image是一個ImageProvider對象,該ImageProvider就是我們所需要的圖片的提供者,它本身是一個抽象類,子類包括NetworkImage、FileImage、ExactAssetImage、AssetImage、MemoryImage等,網(wǎng)絡(luò)加載圖片使用的就是NetworkImage。
Image作為一個StatefulWidget其狀態(tài)由_ImageState控制,_ImageState繼承自State類,其生命周期方法包括initState()、didChangeDependencies()、build()、deactivate()、dispose()、didUpdateWidget()等。我們重點(diǎn)來_ImageState中函數(shù)的執(zhí)行。
由于插入渲染樹時會先調(diào)用initState()函數(shù),然后調(diào)用didChangeDependencies()函數(shù),_ImageState中并沒有重寫initState()函數(shù),所以didChangeDependencies()函數(shù)會執(zhí)行,看下didChangeDependencies()里的內(nèi)容
@override void didChangeDependencies() { _invertColors = MediaQuery.of(context, nullOk: true)?.invertColors ?? SemanticsBinding.instance.accessibilityFeatures.invertColors; _resolveImage(); if (TickerMode.of(context)) _listenToStream(); else _stopListeningToStream(); super.didChangeDependencies(); } _resolveImage()會被調(diào)用,函數(shù)內(nèi)容如下 void _resolveImage() { final ImageStream newStream = widget.image.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null )); assert(newStream != null); _updateSourceStream(newStream); }
函數(shù)中先創(chuàng)建了一個ImageStream對象,該對象是一個圖片資源的句柄,其持有著圖片資源加載完畢后的監(jiān)聽回調(diào)和圖片資源的管理者。而其中的ImageStreamCompleter對象就是圖片資源的一個管理類,也就是說,_ImageState通過ImageStream和ImageStreamCompleter管理類建立了聯(lián)系。
再回頭看一下ImageStream對象是通過widget.image.resolve方法創(chuàng)建的,也就是對應(yīng)NetworkImage的resolve方法,我們查看NetworkImage類的源碼發(fā)現(xiàn)并沒有resolve方法,于是查找其父類,在ImageProvider類中找到了。
ImageStream resolve(ImageConfiguration configuration) { assert(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.putIfAbsent(key, () => load(key), onError: handleError); if (completer != null) { stream.setCompleter(completer); } }).catchError(handleError); return stream; }
ImageStream中的圖片管理者ImageStreamCompleter通過PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);方法創(chuàng)建,imageCache是Flutter框架中實(shí)現(xiàn)的用于圖片緩存的單例,查看其中的putIfAbsent方法
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) { assert(key != null); assert(loader != null); ImageStreamCompleter result = _pendingImages[key]?.completer; // Nothing needs to be done because 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 used position below. final _CachedImage image = _cache.remove(key); if (image != null) { _cache[key] = image; return image.completer; } try { result = loader(); } catch (error, stackTrace) { if (onError != null) { onError(error, stackTrace); return null; } else { 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 increase 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 = _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來查找緩存中是否存在,如果存在則返回,如果不存在則會通過執(zhí)行l(wèi)oader()方法創(chuàng)建圖片資源管理者,而后再將緩存圖片資源的監(jiān)聽方法注冊到新建的圖片管理者中以便圖片加載完畢后做緩存處理。
根據(jù)上面的代碼調(diào)用PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);看出load()方法由ImageProvider對象實(shí)現(xiàn),這里就是NetworkImage對象,看下其具體實(shí)現(xiàn)代碼
@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'); } ); }
代碼中其就是創(chuàng)建一個MultiFrameImageStreamCompleter對象并返回,這是一個多幀圖片管理器,表明Flutter是支持GIF圖片的。創(chuàng)建對象時的codec變量由_loadAsync方法的返回值初始化,查看該方法內(nèi)容
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); }
這里才是關(guān)鍵,就是通過HttpClient對象對指定的url進(jìn)行下載操作,下載完成后根據(jù)圖片二進(jìn)制數(shù)據(jù)實(shí)例化圖像編解碼器對象Codec,然后返回。
那么圖片下載完成后是如何顯示到界面上的呢,下面看下MultiFrameImageStreamCompleter的構(gòu)造方法實(shí)現(xiàn)
MultiFrameImageStreamCompleter({ @required Future<ui.Codec> codec, @required double scale, InformationCollector informationCollector }) : assert(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, ); }); }
看,構(gòu)造方法中的代碼塊,codec的異步方法執(zhí)行完成后會調(diào)用_handleCodecReady函數(shù),函數(shù)內(nèi)容如下
void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); _decodeNextFrameAndSchedule(); }
方法中會將codec對象保存起來,然后解碼圖片幀
Future<void> _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只有一幀,則執(zhí)行_emitFrame函數(shù),從幀數(shù)據(jù)中拿到圖片幀對象根據(jù)縮放比例創(chuàng)建ImageInfo對象,然后設(shè)置顯示的圖片信息
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<ImageListener> localListeners = _listeners.map<ImageListener>( (_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, ); } } }
這時就會根據(jù)添加的監(jiān)聽器來通知一個新的圖片需要渲染。那么這個監(jiān)聽器是什么時候添加的呢,我們回頭看一下_ImageState類中的didChangeDependencies()方法內(nèi)容,執(zhí)行完_resolveImage();后會執(zhí)行_listenToStream();方法
void _listenToStream() { if (_isListeningToStream) return; _imageStream.addListener(_handleImageChanged); _isListeningToStream = true; }
該方法就向ImageStream對象中添加了監(jiān)聽器_handleImageChanged,監(jiān)聽方法如下
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; }); }
最終就是調(diào)用setState方法來通知界面刷新,將下載到的圖片渲染到界面上來了。
實(shí)際問題
從以上源碼分析,我們應(yīng)該清楚了整個網(wǎng)絡(luò)圖片從加載到顯示的過程,不過使用這種原生的方式我們發(fā)現(xiàn)網(wǎng)絡(luò)圖片只是進(jìn)行了內(nèi)存緩存,如果殺掉應(yīng)用進(jìn)程再重新打開后還是要重新下載圖片,這對于用戶而言,每次打開應(yīng)用還是會消耗下載圖片的流量,不過我們可以從中學(xué)習(xí)到一些思路來自己設(shè)計(jì)網(wǎng)絡(luò)圖片加載框架,下面作者就簡單的基于Image.network來進(jìn)行一下改造,增加圖片的磁盤緩存。
解決方案
我們通過源碼分析可知,圖片在緩存中未找到時,會通過網(wǎng)絡(luò)直接下載獲取,而下載的方法是在NetworkImage類中,于是我們可以參考NetworkImage來自定義一個ImageProvider。
代碼實(shí)現(xiàn)
拷貝一份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 /// 將下載的圖片數(shù)據(jù)保存到指定緩存文件中 await _cacheFileImage.saveBytesToFile(key.url, bytes); /// 新增代碼塊end return PaintingBinding.instance.instantiateImageCodec(bytes); }
代碼中注釋已經(jīng)表明了基于原有代碼新增的代碼塊,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<String> getCachePath() async { Directory dir = await getApplicationDocumentsDirectory(); Directory cachePath = Directory("${dir.path}/imagecache/"); if(!cachePath.existsSync()) { cachePath.createSync(); } return cachePath.path; } /// 判斷是否有對應(yīng)圖片緩存文件存在 Future<Uint8List> getFileBytes(String url) async { String cacheDirPath = await getCachePath(); String urlMd5 = getUrlMd5(url); File file = File("$cacheDirPath/$urlMd5"); print("讀取文件:${file.path}"); if(file.existsSync()) { return await file.readAsBytes(); } return null; } /// 將下載的圖片數(shù)據(jù)緩存到指定文件 Future saveBytesToFile(String url, Uint8List bytes) async { String cacheDirPath = await getCachePath(); String urlMd5 = getUrlMd5(url); File file = File("$cacheDirPath/$urlMd5"); if(!file.existsSync()) { file.createSync(); await file.writeAsBytes(bytes); } } }
這樣就增加了文件緩存的功能,思路很簡單,就是在獲取網(wǎng)絡(luò)圖片之前先檢查一下本地文件緩存目錄中是否有緩存文件,如果有則不用再去下載,否則去下載圖片,下載完成后立即將下載到的圖片緩存到文件中供下次需要時使用。
工程的pubspec.yaml中需要增加以下依賴庫
dependencies: path_provider: ^0.4.1 crypto: ^2.0.6
自定義ImageProvider使用
在創(chuàng)建圖片Widget時使用帶參數(shù)的非命名構(gòu)造函數(shù),指定image參數(shù)為自定義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小部件的網(wǎng)絡(luò)圖片加載流程進(jìn)行了源碼分析,了解了源碼的設(shè)計(jì)思路之后,我們新增了簡單的本地文件緩存功能,這使我們的網(wǎng)絡(luò)圖片加載同時具備了內(nèi)存緩存和文件緩存兩種能力,大大提升了用戶體驗(yàn),如果其他同學(xué)有更好的方案可以給作者留言交流。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
AndroidStuio插件開發(fā)適用于jetbrains全家桶
這篇文章主要介紹了AndroidStuio插件開發(fā)適用于jetbrains全家桶,本文通過實(shí)例給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12Kotlin基礎(chǔ)學(xué)習(xí)之循環(huán)和異常
最近在學(xué)習(xí)kotlin,Kotlin 是一個基于 JVM 的新的編程語言,下面這篇文章主要給大家介紹了關(guān)于Kotlin基礎(chǔ)學(xué)習(xí)之循環(huán)和異常的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-12-12Android編程實(shí)現(xiàn)只顯示圖片一部分的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)只顯示圖片一部分的方法,涉及Android針對圖片的局部顯示操作技巧,需要的朋友可以參考下2016-10-10Android 使用CoordinatorLayout實(shí)現(xiàn)滾動標(biāo)題欄效果的實(shí)例
下面小編就為大家?guī)硪黄狝ndroid 使用CoordinatorLayout實(shí)現(xiàn)滾動標(biāo)題欄效果的實(shí)例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03Android實(shí)現(xiàn)手勢劃定區(qū)域裁剪圖片
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)手勢劃定區(qū)域裁剪圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05Android之用PopupWindow實(shí)現(xiàn)彈出菜單的方法詳解
本篇文章是對在Android中,用PopupWindow實(shí)現(xiàn)彈出菜單的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06Android 中 MD5 的幾種生成方式(小結(jié))
這篇文章主要介紹了Android 中 MD5 的幾種生成方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03