您好,登錄后才能下訂單哦!
這篇文章主要講解了“flutter圖片組件核心類源碼分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“flutter圖片組件核心類源碼分析”吧!
自己重新畫一張
Image,是一個statefulWidget,flutter image的核心入口類,包含了network,file,assert,memory這幾個主要的功能,分包對應網絡圖片,文件圖片,APP內置assert圖片,從文件流解析圖片
_ImageState,由于Image是statefulWidget,所以核心代碼都在_ImageState
ImageStream ,處理圖片資源,ImageState和ImageStreamCompleter的橋梁
ImageInfo ,圖片原生信息存儲者
ImageStreamCompleter,可以理解為一幀幀解析圖片,并把解析的數據回調給展示方,主要有兩個實現類
OneFrameImageStreamCompleter單幀圖片解析器(貌似沒在用)
MultiFrameImageStreamCompleter多幀圖片解析器,源碼里所有圖片都是默認使用這個了
ImageProvider,圖片加載器,不同的加載方式有不同的實現
NetworkImage 網絡加載圖片
MemoryImage 從二進制流加載圖片
AssetImage 加載asset里的image
FileImage 從文件中加載圖片
ImageCache ,flutter自帶的圖片緩存,只有內存緩存,官方自帶cache ,最大個數100,最大內存100MB
ScrollAwareImageProvider,避免圖片在快速滑動中加載
// 網絡圖片 Image.network(imgUrl, //圖片鏈接 width: w, height: h), )
上文中提到過,Image是個StatefulWidget,那核心邏輯看對應的ImageState,ImageState繼承自State,State的生命周期我們知道,首次初始化時按InitState()->didChangeDependencies->didUpdateWidget()-> build()順序執行
ImageState的InitState沒做什么,圖片請求的發起是在didChangeDependencies里做的
// ImageState->didChangeDependencies @override void didChangeDependencies() { // ios在輔助模式下的配置,不影響主流程,我們不分析 _updateInvertColors(); // 核心方法,開始請求解析圖片,從這里開始,provier,stream,completer開始悉數登場 _resolveImage(); // 這個判斷可以認為是,當前widget 在tree中是否還是激活狀態 if (TickerMode.of(context)) _listenToStream(); else _stopListeningToStream(); super.didChangeDependencies(); }
再看ImageState里的_resolveImage方法
void _resolveImage() { // ScrollAwareImageProvider代理模式,它本身也是繼承的ImageProvider, // 它的功能是防止在快速滾動時加載圖片 final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>( context: _scrollAwareContext, imageProvider: widget.image, ); // 這里調用了ImageProvider的resolve方法,圖片請求的主流程 final ImageStream newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null, )); assert(newStream != null); // 對resolve返回的Stream注冊監聽,這個監聽很重要,決定了后續的圖片展示(包括gif) // 刷新當前圖片展示一次,例如幀數,加載狀態等等 _updateSourceStream(newStream); }
我們接著看ImageProvider的resolve方法
// 這方法初次看比較繞,其實就干了三個事 // 1. 創建了一個ImageStream // 2. 創建一個Key,key由具體的provider自己實現,這個key用在后面ImageCache里 // 3. 把接下來的流程封裝在一個Zone里,捕獲了同步異常和異步異常,不了解Zone的同學可以參考我另一篇文章 @nonVirtual ImageStream resolve(ImageConfiguration configuration) { assert(configuration != null); final ImageStream stream = createStream(configuration); // 創建了key,把后續的流程封裝在zone里,源碼我不貼了,感興趣的同學自己看下 _createErrorHandlerAndKey( configuration, (T key, ImageErrorListener errorHandler) { resolveStreamForKey(configuration, stream, key, errorHandler); }, (T? key, dynamic exception, StackTrace? stack) async { await null; // wait an event turn in case a listener has been added to the image stream. final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter(); stream.setCompleter(imageCompleter); InformationCollector? collector; assert(() { collector = () sync* { yield DiagnosticsProperty<ImageProvider>('Image provider', this); yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration); yield DiagnosticsProperty<T>('Image key', key, defaultValue: null); }; return true; }()); imageCompleter.setError( exception: exception, stack: stack, context: ErrorDescription('while resolving an image'), silent: true, // could be a network error or whatnot informationCollector: collector, ); }, ); return stream; }
接著看resolveStreamForKey方法,在1.22里,默認的provider都是ScrollAwareImageProvider,ScrollAwareImageProvider重寫了resolveStreamForKey,這里有滾動控制加載的邏輯,但最終調用的還是ImageProvier的resolveStreamForKey
// ImageProvier -> resolveStreamForKey @protected void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) { // streem中已經有completer了,從緩存中拿, if (stream.completer != null) { final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => stream.completer!, onError: handleError, ); assert(identical(completer, stream.completer)); return; } // 如果是首次,新建一個completer,然后會執行load這個函數,就是putIfAbsent的第二個入參 final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, ); // 賦值,注意這里,后面講圖片展示的時候會說到這里 if (completer != null) { stream.setCompleter(completer); } }
接著看ImageProvider的load,load方法就是圖片的具體加載方法,不同的provider有不同的實現,此時我們關注NetworkImage的Provier里的實現
// NetworkImage @override ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { // Ownership of this controller is handed off to [_loadAsync]; it is that // method's responsibility to close the controller's stream when the image // has been loaded or an error is thrown. final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); // MultiFrameImageStreamCompleter是多幀解析器,默認使用的是就是這個,所以默認支持gif return MultiFrameImageStreamCompleter( codec: _loadAsync(key as NetworkImage, chunkEvents, decode), // 異步加載圖片,我們接著看這個方法 chunkEvents: chunkEvents.stream, //加載過程的回調 scale: key.scale, debugLabel: key.url, informationCollector: () { return <DiagnosticsNode>[ DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), DiagnosticsProperty<image_provider.NetworkImage>('Image key', key), ]; }, ); }
接著看NetworkImage的_loadAsync
// 這里就很清晰了吧,內置的HttpClient去加載 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) { // 請求失敗,報錯 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(); } }
至此,第一個問題回答完畢,那當圖片數據請求成功后,是怎么回調到ImageState并展示到界面中的呢?
要看回調和展示,我們從終點ImageState的build方法開始看
// 很容易發現RawImage,RawImage是實際渲染圖片的widget,這么說其實也不對,RenderImage才是最終渲染的 // 可以看到RawImage的第一個參數_imageInfo?.image,那_imageInfo?.image是什么時候賦值的? Widget result = RawImage( image: _imageInfo?.image, debugImageLabel: _imageInfo?.debugLabel, width: widget.width, height: widget.height, scale: _imageInfo?.scale ?? 1.0, color: widget.color, colorBlendMode: widget.colorBlendMode, fit: widget.fit, alignment: widget.alignment, repeat: widget.repeat, centerSlice: widget.centerSlice, matchTextDirection: widget.matchTextDirection, invertColors: _invertColors, isAntiAlias: widget.isAntiAlias, filterQuality: widget.filterQuality, );
還記得第一部分提到的_updateSourceStream(newStream);方法嗎?在這個方法里對ImageStrem設置了一個監聽
// 設置了監聽 _imageStream.addListener(_getListener()); // ImageStreamListener ImageStreamListener _getListener({bool recreateListener = false}) { if(_imageStreamListener == null || recreateListener) { _lastException = null; _lastStack = null; _imageStreamListener = ImageStreamListener( _handleImageFrame, // 每一幀圖片解析完,代表可以展示這一幀了 onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, // 圖片加載互調 onError: widget.errorBuilder != null // 圖片加載錯誤互調 ? (dynamic error, StackTrace stackTrace) { setState(() { _lastException = error; _lastStack = stackTrace; }); } : null, ); } return _imageStreamListener; }
接著看ImageState的_handleImageFrame
// 很簡單,就是setState,可以看到這里賦值了_imageInfo void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; _loadingProgress = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1; _wasSynchronouslyLoaded |= synchronousCall; }); }
那么這個_imageStreamListener 是什么時候回調的呢? 還記得第一步加載過程最后一步的MultiFrameImageStreamCompleter嗎?
// MultiFrameImageStreamCompleter就是支持gif的多幀解析器,還有一個OneFrameImageStreamCompleter,但已經不用了 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; // _handleCodecReady就是圖片加載完的回調,我們看看他內部干了什么 codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) { // 捕獲錯誤并上報 }); // 監聽回調 if (chunkEvents != null) { chunkEvents.listen(reportImageChunkEvent, onError: (dynamic error, StackTrace stack) { reportError( context: ErrorDescription('loading an image'), exception: error, stack: stack, informationCollector: informationCollector, silent: true, ); }, ); } }
這里回答了第二個問題,gif的每幀是怎么支持的,關鍵就是MultiFrameImageStreamCompleter這個類, 接著看MultiFrameImageStreamCompleter的_handleCodecReady
void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); if (hasListeners) { // 看函數名就知道了,解析下一幀并執行 _decodeNextFrameAndSchedule(); } }
MultiFrameImageStreamCompleter的_decodeNextFrameAndSchedule()
Future<void> _decodeNextFrameAndSchedule() async { try { // 獲得下一幀,這一步在C中處理 _nextFrame = await _codec!.getNextFrame(); } catch (exception, stack) { reportError( context: ErrorDescription('resolving an image frame'), exception: exception, stack: stack, informationCollector: _informationCollector, silent: true, ); return; } // 幀數不等于1,說明圖片有多幀 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, debugLabel: debugLabel)); return; } // 如果只有一幀,_scheduleAppFrame最終也會走到_emitFrame _scheduleAppFrame(); }
接著看MultiFrameImageStreamCompleter的_emitFrame
// 調用了setImage void _emitFrame(ImageInfo imageInfo) { setImage(imageInfo); _framesEmitted += 1; }
ImageStreamCompleter的setImage
@protected void setImage(ImageInfo image) { _currentImage = image; if (_listeners.isEmpty) return; // Make a copy to allow for concurrent modification. final List<ImageStreamListener> localListeners = List<ImageStreamListener>.from(_listeners); for (final ImageStreamListener listener in localListeners) { try { // 在這里回調了onImage,那這個回調是哪里注冊的呢? 回到ImageStream的addLister里 listener.onImage(image, false); } catch (exception, stack) { } } }
ImageStream的addLister里
void addListener(ImageStreamListener listener) { // 這里破案了,_completer 不為null的時候,注冊了回調,而ImageStream的completer在ImageStream被創建的還是就賦值了 // 所以前面的listener.onImage(image, false);最終會回調到ImageState里的_imageStreamListener if (_completer != null) return _completer!.addListener(listener); _listeners ??= <ImageStreamListener>[]; _listeners!.add(listener); }
至此,圖片的是展示流程也分析完畢,第二個問題也回答完了。
首先要說明的是,flutter內存緩存默認只有內存緩存,也就意味著如果殺進程重啟,圖片就需要重新加載了。
1.22的內存緩存主要分三部分,相比1.17增加了一部分
_pendingImages 正在加載中的緩存,這個有什么作用呢? 假設Widget1加載了圖片A,Widget2也在這個時候加載了圖片A,那這時候Widget就復用了這個加載中的緩存
_cache 已經加載成功的圖片緩存,這個很好理解
_liveImages 存活的圖片緩存,看代碼主要是在CacheImage之外再加一層緩存,在CacheImage被清楚后,
對于一張圖片,當首次加載時,首先會在_pendingImages中,注意此時圖片還未加載成功,所以如果有復用的情況,會命中_pendingImages,當圖片請求成功后,在_cache和_liveImages都會保存一份,此時_pendingImages會移除。 當超過緩存中的最大數時,會從_cache里按照LRU的規則刪除
在看完整個流程后,對磁盤緩存應該也有思路了。第一個是可以自定義ImageProvider,在圖片數據請求成功后寫入磁盤緩存,不過對于混合項目來說,更好的方式應該是替換圖片的網絡請求方式,利用channel和原生(Android ,ios)的圖片庫加載圖片,這樣可以復用原生圖片庫的磁盤緩存,但也有缺陷,在效率上會有降低,畢竟多了內存的多次拷貝和channel通信。
感謝各位的閱讀,以上就是“flutter圖片組件核心類源碼分析”的內容了,經過本文的學習后,相信大家對flutter圖片組件核心類源碼分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。