這篇文章將為大家詳細(xì)講解有關(guān)如何使用Flutter加載網(wǎng)絡(luò)圖片,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
創(chuàng)新互聯(lián)公司網(wǎng)站建設(shè)公司是一家服務(wù)多年做網(wǎng)站建設(shè)策劃設(shè)計(jì)制作的公司,為廣大用戶提供了成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作,成都網(wǎng)站設(shè)計(jì),一元廣告,成都做網(wǎng)站選創(chuàng)新互聯(lián)公司,貼合企業(yè)需求,高性價(jià)比,滿足客戶不同層次的需求一站式服務(wù)歡迎致電。
有參構(gòu)造函數(shù):
Image(Key key, @required this.image, ...)
開(kāi)發(fā)者可根據(jù)自定義的ImageProvider來(lái)創(chuàng)建Image。
命名構(gòu)造函數(shù):
Image.network(String src, ...)
src即是根據(jù)網(wǎng)絡(luò)獲取的圖片url地址。
Image.file(File file, ...)
file指本地一個(gè)圖片文件對(duì)象,安卓中需要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)化為圖片對(duì)象。
其中Image.network就是我們本篇分享的重點(diǎn) -- 加載網(wǎng)絡(luò)圖片。
Image.network源碼分析
下面通過(guò)源碼我們來(lái)看下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對(duì)象時(shí),會(huì)同時(shí)初始化實(shí)例變量image,image是一個(gè)ImageProvider對(duì)象,該ImageProvider就是我們所需要的圖片的提供者,它本身是一個(gè)抽象類,子類包括NetworkImage、FileImage、ExactAssetImage、AssetImage、MemoryImage等,網(wǎng)絡(luò)加載圖片使用的就是NetworkImage。
Image作為一個(gè)StatefulWidget其狀態(tài)由_ImageState控制,_ImageState繼承自State類,其生命周期方法包括initState()、didChangeDependencies()、build()、deactivate()、dispose()、didUpdateWidget()等。我們重點(diǎn)來(lái)_ImageState中函數(shù)的執(zhí)行。
由于插入渲染樹(shù)時(shí)會(huì)先調(diào)用initState()函數(shù),然后調(diào)用didChangeDependencies()函數(shù),_ImageState中并沒(méi)有重寫(xiě)initState()函數(shù),所以didChangeDependencies()函數(shù)會(huì)執(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()會(huì)被調(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)建了一個(gè)ImageStream對(duì)象,該對(duì)象是一個(gè)圖片資源的句柄,其持有著圖片資源加載完畢后的監(jiān)聽(tīng)回調(diào)和圖片資源的管理者。而其中的ImageStreamCompleter對(duì)象就是圖片資源的一個(gè)管理類,也就是說(shuō),_ImageState通過(guò)ImageStream和ImageStreamCompleter管理類建立了聯(lián)系。
再回頭看一下ImageStream對(duì)象是通過(guò)widget.image.resolve方法創(chuàng)建的,也就是對(duì)應(yīng)NetworkImage的resolve方法,我們查看NetworkImage類的源碼發(fā)現(xiàn)并沒(méi)有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通過(guò)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; }
通過(guò)以上代碼可以看到會(huì)通過(guò)key來(lái)查找緩存中是否存在,如果存在則返回,如果不存在則會(huì)通過(guò)執(zhí)行l(wèi)oader()方法創(chuàng)建圖片資源管理者,而后再將緩存圖片資源的監(jiān)聽(tīng)方法注冊(cè)到新建的圖片管理者中以便圖片加載完畢后做緩存處理。
根據(jù)上面的代碼調(diào)用PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);看出load()方法由ImageProvider對(duì)象實(shí)現(xiàn),這里就是NetworkImage對(duì)象,看下其具體實(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)建一個(gè)MultiFrameImageStreamCompleter對(duì)象并返回,這是一個(gè)多幀圖片管理器,表明Flutter是支持GIF圖片的。創(chuàng)建對(duì)象時(shí)的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)鍵,就是通過(guò)HttpClient對(duì)象對(duì)指定的url進(jìn)行下載操作,下載完成后根據(jù)圖片二進(jìn)制數(shù)據(jù)實(shí)例化圖像編解碼器對(duì)象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í)行完成后會(huì)調(diào)用_handleCodecReady函數(shù),函數(shù)內(nèi)容如下
void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); _decodeNextFrameAndSchedule(); }
方法中會(huì)將codec對(duì)象保存起來(lái),然后解碼圖片幀
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ù)中拿到圖片幀對(duì)象根據(jù)縮放比例創(chuàng)建ImageInfo對(duì)象,然后設(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, ); } } }
這時(shí)就會(huì)根據(jù)添加的監(jiān)聽(tīng)器來(lái)通知一個(gè)新的圖片需要渲染。那么這個(gè)監(jiān)聽(tīng)器是什么時(shí)候添加的呢,我們回頭看一下_ImageState類中的didChangeDependencies()方法內(nèi)容,執(zhí)行完_resolveImage();后會(huì)執(zhí)行_listenToStream();方法
void _listenToStream() { if (_isListeningToStream) return; _imageStream.addListener(_handleImageChanged); _isListeningToStream = true; }
該方法就向ImageStream對(duì)象中添加了監(jiān)聽(tīng)器_handleImageChanged,監(jiān)聽(tīng)方法如下
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; }); }
最終就是調(diào)用setState方法來(lái)通知界面刷新,將下載到的圖片渲染到界面上來(lái)了。
實(shí)際問(wèn)題
從以上源碼分析,我們應(yīng)該清楚了整個(gè)網(wǎng)絡(luò)圖片從加載到顯示的過(guò)程,不過(guò)使用這種原生的方式我們發(fā)現(xiàn)網(wǎng)絡(luò)圖片只是進(jìn)行了內(nèi)存緩存,如果殺掉應(yīng)用進(jìn)程再重新打開(kāi)后還是要重新下載圖片,這對(duì)于用戶而言,每次打開(kāi)應(yīng)用還是會(huì)消耗下載圖片的流量,不過(guò)我們可以從中學(xué)習(xí)到一些思路來(lái)自己設(shè)計(jì)網(wǎng)絡(luò)圖片加載框架,下面作者就簡(jiǎn)單的基于Image.network來(lái)進(jìn)行一下改造,增加圖片的磁盤(pán)緩存。
解決方案
我們通過(guò)源碼分析可知,圖片在緩存中未找到時(shí),會(huì)通過(guò)網(wǎng)絡(luò)直接下載獲取,而下載的方法是在NetworkImage類中,于是我們可以參考NetworkImage來(lái)自定義一個(gè)ImageProvider。
代碼實(shí)現(xiàn)
拷貝一份NetworkImage的代碼到新建的network_image.dart文件中,在_loadAsync方法中我們加入磁盤(pán)緩存的代碼。
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; } /// 判斷是否有對(duì)應(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); } } }
這樣就增加了文件緩存的功能,思路很簡(jiǎn)單,就是在獲取網(wǎng)絡(luò)圖片之前先檢查一下本地文件緩存目錄中是否有緩存文件,如果有則不用再去下載,否則去下載圖片,下載完成后立即將下載到的圖片緩存到文件中供下次需要時(shí)使用。
工程的pubspec.yaml中需要增加以下依賴庫(kù)
dependencies: path_provider: ^0.4.1 crypto: ^2.0.6
自定義ImageProvider使用
在創(chuàng)建圖片Widget時(shí)使用帶參數(shù)的非命名構(gòu)造函數(shù),指定image參數(shù)為自定義ImageProvider對(duì)象即可,代碼示例如下
import 'imageloader/network_image.dart' as network; Widget getNetworkImage() { return Container( color: Colors.blue, width: 200, height: 200, child: Image(image: network.NetworkImage("/upload/otherpic74/119303.png")), ); }
關(guān)于如何使用Flutter加載網(wǎng)絡(luò)圖片就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
當(dāng)前名稱:如何使用Flutter加載網(wǎng)絡(luò)圖片
網(wǎng)頁(yè)地址:http://www.rwnh.cn/article30/pgscpo.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供靜態(tài)網(wǎng)站、動(dòng)態(tài)網(wǎng)站、小程序開(kāi)發(fā)、品牌網(wǎng)站建設(shè)、外貿(mào)建站、Google
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)