您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何使用Flutter加載網絡圖片,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
有參構造函數:
Image(Key key, @required this.image, ...)
開發者可根據自定義的ImageProvider來創建Image。
命名構造函數:
Image.network(String src, ...)
src即是根據網絡獲取的圖片url地址。
Image.file(File file, ...)
file指本地一個圖片文件對象,安卓中需要android.permission.READ_EXTERNAL_STORAGE權限。
Image.asset(String name, ...)
name指項目中添加的圖片資源名,事先在pubspec.yaml文件中有聲明。
Image.memory(Uint8List bytes, ...)
bytes指內存中的圖片數據,將其轉化為圖片對象。
其中Image.network就是我們本篇分享的重點 -- 加載網絡圖片。
Image.network源碼分析
下面通過源碼我們來看下Image.network加載網絡圖片的具體實現。
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命名構造函數創建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 void didChangeDependencies() { _invertColors = MediaQuery.of(context, nullOk: true)?.invertColors ?? SemanticsBinding.instance.accessibilityFeatures.invertColors; _resolveImage(); if (TickerMode.of(context)) _listenToStream(); else _stopListeningToStream(); super.didChangeDependencies(); } _resolveImage()會被調用,函數內容如下 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); }
函數中先創建了一個ImageStream對象,該對象是一個圖片資源的句柄,其持有著圖片資源加載完畢后的監聽回調和圖片資源的管理者。而其中的ImageStreamCompleter對象就是圖片資源的一個管理類,也就是說,_ImageState通過ImageStream和ImageStreamCompleter管理類建立了聯系。
再回頭看一下ImageStream對象是通過widget.image.resolve方法創建的,也就是對應NetworkImage的resolve方法,我們查看NetworkImage類的源碼發現并沒有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);方法創建,imageCache是Flutter框架中實現的用于圖片緩存的單例,查看其中的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來查找緩存中是否存在,如果存在則返回,如果不存在則會通過執行loader()方法創建圖片資源管理者,而后再將緩存圖片資源的監聽方法注冊到新建的圖片管理者中以便圖片加載完畢后做緩存處理。
根據上面的代碼調用PaintingBinding.instance.imageCache.putIfAbsent(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方法的返回值初始化,查看該方法內容
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); }
這里才是關鍵,就是通過HttpClient對象對指定的url進行下載操作,下載完成后根據圖片二進制數據實例化圖像編解碼器對象Codec,然后返回。
那么圖片下載完成后是如何顯示到界面上的呢,下面看下MultiFrameImageStreamCompleter的構造方法實現
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, ); }); }
看,構造方法中的代碼塊,codec的異步方法執行完成后會調用_handleCodecReady函數,函數內容如下
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只有一幀,則執行_emitFrame函數,從幀數據中拿到圖片幀對象根據縮放比例創建ImageInfo對象,然后設置顯示的圖片信息
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, ); } } }
這時就會根據添加的監聽器來通知一個新的圖片需要渲染。那么這個監聽器是什么時候添加的呢,我們回頭看一下_ImageState類中的didChangeDependencies()方法內容,執行完_resolveImage();后會執行_listenToStream();方法
void _listenToStream() { if (_isListeningToStream) return; _imageStream.addListener(_handleImageChanged); _isListeningToStream = true; }
該方法就向ImageStream對象中添加了監聽器_handleImageChanged,監聽方法如下
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; }); }
最終就是調用setState方法來通知界面刷新,將下載到的圖片渲染到界面上來了。
實際問題
從以上源碼分析,我們應該清楚了整個網絡圖片從加載到顯示的過程,不過使用這種原生的方式我們發現網絡圖片只是進行了內存緩存,如果殺掉應用進程再重新打開后還是要重新下載圖片,這對于用戶而言,每次打開應用還是會消耗下載圖片的流量,不過我們可以從中學習到一些思路來自己設計網絡圖片加載框架,下面作者就簡單的基于Image.network來進行一下改造,增加圖片的磁盤緩存。
解決方案
我們通過源碼分析可知,圖片在緩存中未找到時,會通過網絡直接下載獲取,而下載的方法是在NetworkImage類中,于是我們可以參考NetworkImage來自定義一個ImageProvider。
代碼實現
拷貝一份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 /// 將下載的圖片數據保存到指定緩存文件中 await _cacheFileImage.saveBytesToFile(key.url, bytes); /// 新增代碼塊end return PaintingBinding.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'; 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; } /// 判斷是否有對應圖片緩存文件存在 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; } /// 將下載的圖片數據緩存到指定文件 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); } } }
這樣就增加了文件緩存的功能,思路很簡單,就是在獲取網絡圖片之前先檢查一下本地文件緩存目錄中是否有緩存文件,如果有則不用再去下載,否則去下載圖片,下載完成后立即將下載到的圖片緩存到文件中供下次需要時使用。
工程的pubspec.yaml中需要增加以下依賴庫
dependencies: path_provider: ^0.4.1 crypto: ^2.0.6
自定義ImageProvider使用
在創建圖片Widget時使用帶參數的非命名構造函數,指定image參數為自定義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://cache.yisu.com/upload/information/20200623/125/119303.png")), ); }
關于如何使用Flutter加載網絡圖片就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。