Flutter系統(tǒng)網(wǎng)絡(luò)圖片加載流程解析
Flutter原生支持在Image
組件上顯示網(wǎng)絡(luò)圖片,最簡單的使用方式如下,調(diào)用Image
的命名構(gòu)造方法Image.network
即可實現(xiàn)網(wǎng)絡(luò)圖片的下載顯示。
Widget image = Image.network(imageUrl);
那么,它內(nèi)部是如何實現(xiàn)的呢?是否有做緩存處理或其他優(yōu)化操作呢?帶著疑問,我們一起來看下它的底層究竟是如何實現(xiàn)的。
一、從構(gòu)造函數(shù)開始
我們以最簡單的調(diào)用方式舉例,當我們使用Image.network(imageUrl)
這種方式來顯示圖片時,Image
組件內(nèi)部image
屬性就會被賦值NetworkImage
。
// 此為簡化過的Image組件類結(jié)構(gòu) class Image extends StatefulWidget { Image.network( String src, ) : image = NetworkImage(src); // 圖片數(shù)據(jù)處理的基類 final ImageProvider image; }
這里引出了一個類叫NetworkImage
,它是ImageProvider
的子類,專門實現(xiàn)網(wǎng)絡(luò)圖片的下載和解析邏輯。當然你直接點進去看到的其實是個抽象類,并不是真正實現(xiàn)下載邏輯的地方,真正實現(xiàn)網(wǎng)絡(luò)圖片下載解析的在'_network_image_io.dart’
這個文件下。構(gòu)造函數(shù)知道這些就夠了。接下來就看Image
是在何時觸發(fā)網(wǎng)絡(luò)圖片的下載的。
二、圖片下載入口
Image
是一個StatefulWidget
,它又一個對應(yīng)的State
叫_ImageState
。在這個_ImageState
的生命周期中,控制著圖片的下載過程。
State的生命周期可以簡單的分為:構(gòu)造函數(shù) → initState → didChangeDependencies → build
因此,我們順著這個順序找,很快看到一個可疑的地方,didChangeDependencies
中的_resolveImage
方法。而TickerMode
則是用于控制動畫的,在這里被用于判斷是否禁用了動畫。關(guān)于TickerMode
的相關(guān)介紹,可以看下這篇文章
// 完整源碼 @override void didChangeDependencies() { _updateInvertColors(); // 處理圖片的入口 _resolveImage(); // 當動畫被禁用時,圖片也是無法顯示的,這個 if (TickerMode.of(context)) // 添加圖片流處理的監(jiān)聽 _listenToStream(); else _stopListeningToStream(keepStreamAlive: true); super.didChangeDependencies(); }
我們進入到_resolveImage
方法中去。
void _resolveImage() { // ScrollAwareImageProvider包裝了我們的NetworkImage final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>( context: _scrollAwareContext, imageProvider: widget.image, ); // 新建圖片流 final ImageStream newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, )); assert(newStream != null); // 更新圖片流 _updateSourceStream(newStream); }
_resolveImage
方法就做了三件事。
1、用ScrollAwareImageProvider
包裝了NetworkImage
2、創(chuàng)建圖片流對象ImageStream
3、更新圖片流
2.1、ScrollAwareImageProvider
ScrollAwareImageProvider
也是ImageProvider
的子類,它的作用很簡單,就是防止在快速滑動的時候加載圖片,當存在快速滑動時,會將解析圖片的工作放到下一幀處理。至于具體如何實現(xiàn),我們放在后面再提。
2.2、ImageConfiguration
ImageConfiguration
由方法createLocalImageConfiguration
創(chuàng)建,保存了圖片的基本配置信息,如Bundle
,屏幕項目比devicePixelRatio
,本地化local
,尺寸size
,平臺platform
等。
2.3、ImageStream
表示一個圖片流,可以添加觀察者ImageStreamCompleter
來監(jiān)聽圖片是否處理完成。一個圖片流可以添加多個觀察者。
ImageStream
由provider
的resolve
方法調(diào)用后創(chuàng)建。通過源碼可知,此處的provider
就是ScrollAwareImageProvider
對象。但是它內(nèi)部并沒有實現(xiàn)resolve
方法,因此此處調(diào)用的是父類ImageProvider
的resolve
方法。
三、圖片流和Key
以下代碼截取自ImageProvider,并且刪減了無關(guān)代碼。
ImageStream resolve(ImageConfiguration configuration) { // 創(chuàng)建流,這里直接調(diào)用了ImageStream的構(gòu)造函數(shù),并沒有用到configuration final ImageStream stream = createStream(configuration); // 關(guān)鍵在這里,這里會根據(jù)configuration創(chuàng)建一個唯一key _createErrorHandlerAndKey( configuration, // 成功的回調(diào) (T key, ImageErrorListener errorHandler) { resolveStreamForKey(configuration, stream, key, errorHandler); }, // 下面是錯誤回調(diào),可以不關(guān)注 (T? key, Object exception, StackTrace? stack) async { await null; // wait an event turn in case a listener has been added to the image stream. InformationCollector? collector; if (stream.completer == null) { stream.setCompleter(_ErrorImageCompleter()); } stream.completer!.reportError( exception: exception, stack: stack, context: ErrorDescription('while resolving an image'), silent: true, // could be a network error or whatnot informationCollector: collector, ); }, ); return stream; }
resolve
方法的作用是創(chuàng)建圖片流對象ImageStream
,并根據(jù)傳入的圖片配置信息configuration
,創(chuàng)建對應(yīng)的Key
,這個Key
用于圖片緩存。
那么這個key
到底是怎么創(chuàng)建的呢,我們進入到_createErrorHandlerAndKey
方法中查看。關(guān)鍵代碼如下,已刪除無關(guān)代碼。
Future<T> key; try { key = obtainKey(configuration); } catch (error, stackTrace) { handleError(error, stackTrace); return; } key.then<void>((T key) { obtainedKey = key; try { successCallback(key, handleError); } catch (error, stackTrace) { handleError(error, stackTrace); } }).catchError(handleError);
可以看到方法實現(xiàn)中調(diào)用了ImageProvider
的obtainKey
方法,而這個方法在ImageProvider
并沒有具體實現(xiàn),需要子類完成對應(yīng)的實現(xiàn)。
Future<T> obtainKey(ImageConfiguration configuration);
還記得上文的分析不,我們說傳入的imageProvider
實例是ScrollAwareImageProvider
對象,因此對應(yīng)的實現(xiàn)也要到這個類中去查找。很快,我們找到obtainKey
方法的實現(xiàn),可以看到它做了個透傳,具體是由它包裝的類也就是NetworkImage
來實現(xiàn)的。
@override Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
那么,我們就去NetworkImage
找obtainKey
。
注意下真正的NetworkImage
實現(xiàn)是在_network_image_io.dart
文件下的。
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) { return SynchronousFuture<NetworkImage>(this); }
到這,我們就知道了NetworkImage
的key
為SynchronousFuture
。
獲取到key后的下一步就是調(diào)用_createErrorHandlerAndKey
方法的successCallback
回調(diào)。從而觸發(fā)了下一個流程resolveStreamForKey
。
_createErrorHandlerAndKey( configuration, (T key, ImageErrorListener errorHandler) { // 拿到Key之后的回調(diào) resolveStreamForKey(configuration, stream, key, errorHandler); } )
四、根據(jù)key來處理圖片流
還是回到子類ScrollAwareImageProvider
中,它重寫了父類的resolveStreamForKey
方法,前文提到,ScrollAwareImageProvider
是用來防止列表在快速滑動的時候來加載圖片的,那么它是如何實現(xiàn)的?我們就從resolveStreamForKey
這個方法中來一探究竟。
// 以下代碼已去掉無關(guān)邏輯 @override void resolveStreamForKey( ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError, ) { // 滑動過快 if (Scrollable.recommendDeferredLoadingForContext(context.context!)) { SchedulerBinding.instance!.scheduleFrameCallback((_) { // 放入下一幀再嘗試處理,如果下一幀還是過快,那么將一直被推遲 scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError)); }); return; } // 當前可以加載,那么透傳給包裝的imageProvider來處理,這里是NetworkImage imageProvider.resolveStreamForKey(configuration, stream, key, handleError); }
Scrollable
用于滑動組件,它有個方法叫recommendDeferredLoadingForContext
,表示是否建議延遲加載。內(nèi)部最終是根據(jù)滑動速度和當前設(shè)備的最大物理尺寸的邊去比較,如果大于,表示速度過快,那么就建議延遲。具體邏輯在scroll_physics.dart
文件下。這里不多做介紹。
一旦當前應(yīng)用處于滑動狀態(tài),并且速度過快,那么,圖片的加載將會被推遲到下一幀再進行嘗試。因此我們說,當處于快速滑動時,圖片是無法加載的。
當判斷可以加載圖片時,操作流將會被移交給被包裝類imageProvider
,這里是NetworkImage
來處理。但是,NetworkImage
沒有實現(xiàn)resolveStreamForKey
方法,因此最終還是跑到了ImageProvider
類中的resolveStreamForKey
方法下。
@protected void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) { // 第一次進來還沒有設(shè)置completer,因此不會進入這個分支中 if (stream.completer != null) { final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => stream.completer!, onError: handleError, ); assert(identical(completer, stream.completer)); return; } final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, ); if (completer != null) { stream.setCompleter(completer); } }
當?shù)谝淮渭虞d網(wǎng)絡(luò)圖的時候,會直接走到下面這個邏輯中。這里涉及到一個很重要的類,ImageCache
。它是做圖片緩存用的。
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, );
4.1、ImageCache
圖片緩存類,只做了內(nèi)存緩存。它由PaintingBinding持有,是一個單利。它的內(nèi)部通過三個Map來緩存圖片。
// 加載中的圖片 final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{}; // 緩存中的圖片 final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{}; // 正在使用的圖片 final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{}
從圖片緩存器中獲取圖片的邏輯集中在putIfAbsent
方法中。以下代碼已經(jīng)去掉無關(guān)代碼。
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) { TimelineTask? timelineTask; TimelineTask? listenerTask; ImageStreamCompleter? result = _pendingImages[key]?.completer; // 正在加載,直接返回 if (result != null) { return result; } // 這邊有個小知識,dart中的Map是有順序的,因此利用這點可以實現(xiàn)LRU算法。 // 最近用到了這圖片,因此刪除對應(yīng)鍵值對,并更新,就能讓它的位置處于前面 final _CachedImage? image = _cache.remove(key); if (image != null) { // 更新 _liveImages _trackLiveImage( key, image.completer, image.sizeBytes, ); _cache[key] = image; return image.completer; } final _LiveImage? liveImage = _liveImages[key]; if (liveImage != null) { // 更新 _cache,這里還會根據(jù)最大緩存數(shù)量和大小來最限制 _touch( key, _CachedImage( liveImage.completer, sizeBytes: liveImage.sizeBytes, ), timelineTask, ); return liveImage.completer; } // 加載流程,這是個回調(diào),由各ImageProvider子類來實現(xiàn) try { result = loader(); // 下載完更新 _liveImages _trackLiveImage(key, result, null); } catch (error, stackTrace) { if (onError != null) { onError(error, stackTrace); return null; } else { rethrow; } } bool listenedOnce = false; _PendingImage? untrackedPendingImage; // 設(shè)置圖片加載監(jiān)聽,一旦加載完畢,那么會刪除_pendingImages下對應(yīng)的圖片,并移除監(jiān)聽。同時更新_cache和_liveImages void listener(ImageInfo? info, bool syncCall) { int? sizeBytes; if (info != null) { sizeBytes = info.sizeBytes; info.dispose(); } final _CachedImage image = _CachedImage( result!, sizeBytes: sizeBytes, ); _trackLiveImage(key, result, sizeBytes); if (untrackedPendingImage == null) { _touch(key, image, listenerTask); } else { image.dispose(); } final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key); if (pendingImage != null) { pendingImage.removeListener(); } listenedOnce = true; } // 設(shè)置加載監(jiān)聽,主要用來管理_pendingImages final ImageStreamListener streamListener = ImageStreamListener(listener); if (maximumSize > 0 && maximumSizeBytes > 0) { _pendingImages[key] = _PendingImage(result, streamListener); } else { untrackedPendingImage = _PendingImage(result, streamListener); } result.addListener(streamListener); return result; }
4.2、 load
一旦在ImageCache
中找不到緩存的圖片,就會通過loader
回調(diào)出來,走真正的下載流程。
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, // 本地圖片找不到,需要去對應(yīng)的ImageProvider子類里實現(xiàn)加載邏輯 () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, );
還是先看ScrollAwareImageProvider
類,里面實現(xiàn)了load
方法,并透傳給了NetworkImage
來實現(xiàn)。
@override ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode);
在NetworkImage
下,可以找到對應(yīng)的load
方法實現(xiàn)。里面有個_loadAsync
方法,它就是我們要找的圖片下載核心代碼。
@override ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); // 多幀圖片流管理器 return MultiFrameImageStreamCompleter( // 核心異步加載邏輯 codec: _loadAsync(key as NetworkImage, chunkEvents, decode), chunkEvents: chunkEvents.stream, scale: key.scale, debugLabel: key.url, informationCollector: () => <DiagnosticsNode>[ DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), DiagnosticsProperty<image_provider.NetworkImage>('Image key', key), ], ); }
五、圖片下載
饒了一大圈,終于來到了下載圖片的地方了??梢钥吹较螺d圖片的邏輯很簡單,創(chuàng)建一個下載的http請求,設(shè)置header,下載圖片。一旦下載成功,就會通過decode這個回調(diào)將圖片的二進制數(shù)據(jù)返回出去decode(bytes)
。
Future<ui.Codec> _loadAsync( NetworkImage key, StreamController<ImageChunkEvent> chunkEvents, image_provider.DecoderCallback decode, ) async { try { 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) { // The network may be only temporarily unavailable, or the file will be // added on the server later. Avoid having future calls to resolve // fail to check the network again. await response.drain<List<int>>(<int>[]); throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved); } final Uint8List bytes = await consolidateHttpClientResponseBytes( response, onBytesReceived: (int cumulative, int? total) { chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: cumulative, expectedTotalBytes: total, )); }, ); if (bytes.lengthInBytes == 0) throw Exception('NetworkImage is an empty file: $resolved'); return decode(bytes); } catch (e) { // Depending on where the exception was thrown, the image cache may not // have had a chance to track the key in the cache at all. // Schedule a microtask to give the cache a chance to add the key. scheduleMicrotask(() { PaintingBinding.instance!.imageCache!.evict(key); }); rethrow; } finally { chunkEvents.close(); } }
而回調(diào)出去的這些二進制數(shù)據(jù),是在MultiFrameImageStreamCompleter
中被處理的。MultiFrameImageStreamCompleter
是ImageStreamCompleter
的子類,用于管理多幀圖片的加載。
在MultiFrameImageStreamCompleter
的構(gòu)造方法中,我們可以看到它對codec
做了回調(diào)處理。而這個codec就是前面提到的_loadAsync
異步方法。
MultiFrameImageStreamCompleter({ required Future<ui.Codec> codec, required double scale, String? debugLabel, Stream<ImageChunkEvent>? chunkEvents, InformationCollector? informationCollector, }) : assert(codec != null), _informationCollector = informationCollector, _scale = scale { this.debugLabel = debugLabel; // 這里處理了_loadAsync的回調(diào) codec.then<void>(_handleCodecRead);
_handleCodecRead
方法中回判斷是否有觀察者,有就進入解碼流程。
void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); if (hasListeners) { // 存在觀察者,開始解碼 _decodeNextFrameAndSchedule(); } }
_decodeNextFrameAndSchedule
方法可以看成是圖片的解碼方法,當然實際解碼的地方位于更底層的Native。圖片解碼后會將信息保存在FrameInfo
中,由_nextFrame
持有。這里我們只考慮單幀圖片,不考慮gif圖。解碼后的信息會被封裝在ImageInfo
中,其中image
就是真正的圖片數(shù)據(jù)。并調(diào)用_emitFrame
方法,更新圖片信息。而_emitFrame
方法則主要是調(diào)用了setImage
來通知觀察者更新。我們直接看setImage
方法即可。
Future<void> _decodeNextFrameAndSchedule() async { _nextFrame?.image.dispose(); _nextFrame = null; try { // 解碼得到一幀圖片信息,保存在FrameInfo中 _nextFrame = await _codec!.getNextFrame(); } catch (exception, stack) { return; } // 當幀圖片就將數(shù)據(jù)封裝在ImageInfo中回調(diào)出去 if (_codec!.frameCount == 1) { if (!hasListeners) { return; } _emitFrame(ImageInfo( image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel, )); _nextFrame!.image.dispose(); _nextFrame = null; return; } // 多幀則繼續(xù)往下走 _scheduleAppFrame(); }
通過ImageStreamListener
通知更新,刷新界面展示。
void setImage(ImageInfo image) { _checkDisposed(); _currentImage?.dispose(); _currentImage = image; if (_listeners.isEmpty) return; // Make a copy to allow for concurrent modification. final List<ImageStreamListener> localListeners = List<ImageStreamListener>.of(_listeners); for (final ImageStreamListener listener in localListeners) { try { // 設(shè)置新圖篇,通知更新界面展示 listener.onImage(image.clone(), false); } catch (exception, stack) { reportError( context: ErrorDescription('by an image listener'), exception: exception, stack: stack, ); } } }
說到這里,我們好像還沒提到過什么時候設(shè)置的觀察者,好,我們再次回到最初的入口,_ImageState
組件的didChangeDependencies
方法中。
六、添加觀察者實現(xiàn)界面更新
這個觀察者就是通過_listenToStream
方法添加的。
@override void didChangeDependencies() { _updateInvertColors(); _resolveImage(); if (TickerMode.of(context)) // 添加觀察者 _listenToStream(); else _stopListeningToStream(keepStreamAlive: true); super.didChangeDependencies(); }
并且在創(chuàng)建觀察者ImageStreamListener
的時候,設(shè)置了onImage
的回調(diào)。
// 這里是獲取觀察者的入口 ImageStreamListener _getListener({bool recreateListener = false}) { if(_imageStreamListener == null || recreateListener) { _lastException = null; _lastStack = null; _imageStreamListener = ImageStreamListener( // 這個就是onImage的回調(diào) _handleImageFrame, onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, onError: widget.errorBuilder != null || kDebugMode ? (Object error, StackTrace? stackTrace) { setState(() { _lastException = error; _lastStack = stackTrace; }); } : null, ); } return _imageStreamListener!; }
onImage
的入?yún)⒈辉O(shè)置了_handleImageFrame
,因此當下載完圖片后調(diào)用的就是_handleImageFrame
方法。
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { // 更新圖片信息,實現(xiàn)圖片加載 _replaceImage(info: imageInfo); _loadingProgress = null; _lastException = null; _lastStack = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall; }); } void _replaceImage({required ImageInfo? info}) { _imageInfo?.dispose(); _imageInfo = info; }
到此,圖片下載和更新的流程已經(jīng)都串起來了。下載完的圖片存放在ImageInfo
中,在setState
后,會被設(shè)置進RawImage
組件中實現(xiàn)渲染。
總結(jié)
網(wǎng)絡(luò)圖片的加載邏輯可以分為以下幾個步驟:
1、根據(jù)圖片類型,生成對應(yīng)的key
2、根據(jù)key去全局的ImageCache下查找圖片緩存,命中則直接返回刷新
3、圖片緩存沒有命中,調(diào)用Http去下載圖片
4、下載完圖片后,將圖片的二進制數(shù)據(jù)回調(diào)出去觸發(fā)界面刷新,同時會做內(nèi)存緩存
5、在RawImage中顯示網(wǎng)絡(luò)圖片
到此這篇關(guān)于Flutter系統(tǒng)網(wǎng)絡(luò)圖片加載過程解析的文章就介紹到這了,更多相關(guān)Flutter圖片加載流程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Android平臺JSON預(yù)覽(JSON-handle)
這篇文章主要介紹了Android平臺JSON預(yù)覽(JSON-handle),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09Android中Fragment的加載方式與數(shù)據(jù)通信詳解
本文主要介紹了Android中Fragment的加載方式與數(shù)據(jù)通信的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03Android中RecycleView與ViewPager沖突的解決方法及原理
這篇文章主要給大家介紹了關(guān)于Android中RecycleView與ViewPager沖突的解決方法及原理的相關(guān)資料,以及ViewPager嵌套RecycleView卡頓問題的處理方法,文中通過示例代碼介紹的非常狎昵,需要的朋友可以參考下2018-07-07解決Android 6.0獲取wifi Mac地址為02:00:00:00:00:00問題
這篇文章主要介紹了Android 6.0獲取wifi Mac地址為02:00:00:00:00:00的解決方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-11-11