flutter圖片組件核心類源碼解析
導語
在使用flutter 自帶圖片組件的過程中,大家有沒有考慮過flutter是如何加載一張網(wǎng)絡圖片的? 以及對自帶的圖片組件我們可以做些什么優(yōu)化?
問題
flutter 網(wǎng)絡圖片是怎么請求的?
圖片請求成功后是這么展示的? gif的每一幀是怎么支持展示的?
如何支持圖片的磁盤緩存?
接下來,讓我們帶著問題一起探究flutter 圖片組件的內(nèi)部原理
本文源碼分析以flutter-1.22版本為準,只涉及到dart端,c層圖片解碼不涉及
Image的核心類圖及其關(guān)系
自己重新畫一張

- Image,是一個statefulWidget,flutter image的核心入口類,包含了network,file,assert,memory這幾個主要的功能,分包對應網(wǎng)絡圖片,文件圖片,APP內(nèi)置assert圖片,從文件流解析圖片
- _ImageState,由于Image是statefulWidget,所以核心代碼都在_ImageState
- ImageStream ,處理圖片資源,ImageState和ImageStreamCompleter的橋梁
- ImageInfo ,圖片原生信息存儲者
- ImageStreamCompleter,可以理解為一幀幀解析圖片,并把解析的數(shù)據(jù)回調(diào)給展示方,主要有兩個實現(xiàn)類
- OneFrameImageStreamCompleter單幀圖片解析器(貌似沒在用)
- MultiFrameImageStreamCompleter多幀圖片解析器,源碼里所有圖片都是默認使用這個了
- ImageProvider,圖片加載器,不同的加載方式有不同的實現(xiàn)
- NetworkImage 網(wǎng)絡加載圖片
- MemoryImage 從二進制流加載圖片
- AssetImage 加載asset里的image
- FileImage 從文件中加載圖片
- ImageCache ,flutter自帶的圖片緩存,只有內(nèi)存緩存,官方自帶cache ,最大個數(shù)100,最大內(nèi)存100MB
- ScrollAwareImageProvider,避免圖片在快速滑動中加載
網(wǎng)絡圖片的加載過程
// 網(wǎng)絡圖片
Image.network(imgUrl, //圖片鏈接
width: w,
height: h),
)
上文中提到過,Image是個StatefulWidget,那核心邏輯看對應的ImageState,ImageState繼承自State,State的生命周期我們知道,首次初始化時按InitState()->didChangeDependencies->didUpdateWidget()-> build()順序執(zhí)行
ImageState的InitState沒做什么,圖片請求的發(fā)起是在didChangeDependencies里做的
// ImageState->didChangeDependencies
@override
void didChangeDependencies() {
// ios在輔助模式下的配置,不影響主流程,我們不分析
_updateInvertColors();
// 核心方法,開始請求解析圖片,從這里開始,provier,stream,completer開始悉數(shù)登場
_resolveImage();
// 這個判斷可以認為是,當前widget 在tree中是否還是激活狀態(tài)
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,
);
// 這里調(diào)用了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注冊監(jiān)聽,這個監(jiān)聽很重要,決定了后續(xù)的圖片展示(包括gif)
// 刷新當前圖片展示一次,例如幀數(shù),加載狀態(tài)等等
_updateSourceStream(newStream);
}
我們接著看ImageProvider的resolve方法
// 這方法初次看比較繞,其實就干了三個事
// 1. 創(chuàng)建了一個ImageStream
// 2. 創(chuàng)建一個Key,key由具體的provider自己實現(xiàn),這個key用在后面ImageCache里
// 3. 把接下來的流程封裝在一個Zone里,捕獲了同步異常和異步異常,不了解Zone的同學可以參考我另一篇文章
@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = createStream(configuration);
// 創(chuàng)建了key,把后續(xù)的流程封裝在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,這里有滾動控制加載的邏輯,但最終調(diào)用的還是ImageProvier的resolveStreamForKey
// ImageProvier -> resolveStreamForKey
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
// streem中已經(jīng)有completer了,從緩存中拿,
if (stream.completer != null) {
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => stream.completer!,
onError: handleError,
);
assert(identical(completer, stream.completer));
return;
}
// 如果是首次,新建一個completer,然后會執(zhí)行l(wèi)oad這個函數(shù),就是putIfAbsent的第二個入?yún)?
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有不同的實現(xiàn),此時我們關(guān)注NetworkImage的Provier里的實現(xiàn)
// 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, //加載過程的回調(diào)
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
// 這里就很清晰了吧,內(nèi)置的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);
}
// 二進制流數(shù)據(jù)回調(diào)
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();
}
}
至此,第一個問題回答完畢,那當圖片數(shù)據(jù)請求成功后,是怎么回調(diào)到ImageState并展示到界面中的呢?
網(wǎng)絡圖片數(shù)據(jù)的回調(diào)和展示過程
要看回調(diào)和展示,我們從終點ImageState的build方法開始看
// 很容易發(fā)現(xiàn)RawImage,RawImage是實際渲染圖片的widget,這么說其實也不對,RenderImage才是最終渲染的 // 可以看到RawImage的第一個參數(shù)_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設置了一個監(jiān)聽
// 設置了監(jiān)聽
_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, // 圖片加載互調(diào)
onError: widget.errorBuilder != null // 圖片加載錯誤互調(diào)
? (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 是什么時候回調(diào)的呢? 還記得第一步加載過程最后一步的MultiFrameImageStreamCompleter嗎?
// MultiFrameImageStreamCompleter就是支持gif的多幀解析器,還有一個OneFrameImageStreamCompleter,但已經(jīng)不用了
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就是圖片加載完的回調(diào),我們看看他內(nèi)部干了什么
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
// 捕獲錯誤并上報
});
// 監(jiān)聽回調(diào)
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的每幀是怎么支持的,關(guān)鍵就是MultiFrameImageStreamCompleter這個類, 接著看MultiFrameImageStreamCompleter的_handleCodecReady
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
// 看函數(shù)名就知道了,解析下一幀并執(zhí)行
_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;
}
// 幀數(shù)不等于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
// 調(diào)用了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 {
// 在這里回調(diào)了onImage,那這個回調(diào)是哪里注冊的呢? 回到ImageStream的addLister里
listener.onImage(image, false);
} catch (exception, stack) {
}
}
}
ImageStream的addLister里
void addListener(ImageStreamListener listener) {
// 這里破案了,_completer 不為null的時候,注冊了回調(diào),而ImageStream的completer在ImageStream被創(chuàng)建的還是就賦值了
// 所以前面的listener.onImage(image, false);最終會回調(diào)到ImageState里的_imageStreamListener
if (_completer != null)
return _completer!.addListener(listener);
_listeners ??= <ImageStreamListener>[];
_listeners!.add(listener);
}
至此,圖片的是展示流程也分析完畢,第二個問題也回答完了。
補上圖片內(nèi)存緩存的源碼分析
首先要說明的是,flutter內(nèi)存緩存默認只有內(nèi)存緩存,也就意味著如果殺進程重啟,圖片就需要重新加載了。
1.22的內(nèi)存緩存主要分三部分,相比1.17增加了一部分
_pendingImages 正在加載中的緩存,這個有什么作用呢? 假設Widget1加載了圖片A,Widget2也在這個時候加載了圖片A,那這時候Widget就復用了這個加載中的緩存
_cache 已經(jīng)加載成功的圖片緩存,這個很好理解
_liveImages 存活的圖片緩存,看代碼主要是在CacheImage之外再加一層緩存,在CacheImage被清楚后,
對于一張圖片,當首次加載時,首先會在_pendingImages中,注意此時圖片還未加載成功,所以如果有復用的情況,會命中_pendingImages,當圖片請求成功后,在_cache和_liveImages都會保存一份,此時_pendingImages會移除。 當超過緩存中的最大數(shù)時,會從_cache里按照LRU的規(guī)則刪除
如何支持圖片的磁盤緩存
在看完整個流程后,對磁盤緩存應該也有思路了。第一個是可以自定義ImageProvider,在圖片數(shù)據(jù)請求成功后寫入磁盤緩存,不過對于混合項目來說,更好的方式應該是替換圖片的網(wǎng)絡請求方式,利用channel和原生(Android ,ios)的圖片庫加載圖片,這樣可以復用原生圖片庫的磁盤緩存,但也有缺陷,在效率上會有降低,畢竟多了內(nèi)存的多次拷貝和channel通信。
總結(jié)
本文只是分析了Image.Network的加載和展示過程,而且也只是涉及到了dart端代碼??偟膩碚f,整個流程并不復雜,其他諸如Image.Memory,Image.File 原理都是一樣的,區(qū)別只是各自的ImageProvider不一樣,我們也可以自定義ImageProvider實現(xiàn)自己想要的效果。
以上就是flutter圖片組件核心類源碼解析的詳細內(nèi)容,更多關(guān)于flutter圖片組件核心類的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android webview與js交換JSON對象數(shù)據(jù)示例
js主動調(diào)用android的對象方式,android也無法返回給js一個jsonobject,需要js做一下轉(zhuǎn)換,具體代碼如下,感興趣的朋友可以參考下哈2013-06-06
Android CardView+ViewPager實現(xiàn)ViewPager翻頁動畫的方法
本篇文章主要介紹了Android CardView+ViewPager實現(xiàn)ViewPager翻頁動畫的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
2021最新Android筆試題總結(jié)美團Android崗職能要求
這篇文章主要介紹了2021最新Android筆試題總結(jié)以及美團Android崗職能要求,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08
Android Studio使用Kotlin時,修改代碼后運行不生效的解決方法
這篇文章主要介紹了Android Studio使用Kotlin時,修改代碼后運行不生效的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
android開發(fā)中ListView與Adapter使用要點介紹
項目用到ListView,由于要用到 ImageView ,圖片源不是在資源里面的,沒法使用資源 ID,因此無法直接使用SimpleAdapter,要自己寫一個Adapter。 在使用ListView和Adapter需要注意以下幾點2013-06-06
Android ListView的item中嵌套ScrollView的解決辦法
有時候,listview 的item要顯示的字段比較多,考慮到顯示問題,item外面不得不嵌套ScrollView來實現(xiàn),糾結(jié)怎么解決此問題呢?下面小編給大家分享下Android ListView的item中嵌套ScrollView的解決辦法,感興趣的朋友一起看看吧2016-10-10

