本文基于1.12.13+hotfix.8版本原始碼分析,
目錄- 1、Image
- 2、ImageProvider
- 3、圖片資料加載ImageStream、ImageStreamCompleter
- 4、快取池PaintingBinding#imageCache
- 5、網路圖片加載
1、Image
點擊進入原始碼,可以看到Image繼承自StatefulWidget,那么重點自然在State里面,跟著生命周期走,可以發現在didUpdateWidget中呼叫了這個方法:
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);
}
void _updateSourceStream(ImageStream newStream) {
// ... 省略部分原始碼
if (_isListeningToStream)
_imageStream.addListener(_getListener());
}
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}
在這里呼叫了image(ImageProvider)的resolve方法獲取到一個ImageStream,并給這個流設定了監聽器,從名字上,不難猜出這是個圖片資料流,在listener拿到資料后會呼叫setState(() {})方法進行rebuild,這里不再貼代碼,
2、ImageProvider
在上面我們看到了Image是需要接收圖片資料進行繪制的,那么,這個資料是在哪里解碼的?又是哪里發送過來的?
帶著疑問,我們先進到ImageProvider的原始碼,可以發現其實這個類非常簡單,代碼量也不多,可以看看resolve方法的核心部分:
Future<T> key;
try {
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
one rror: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
可以看到,這里會異步獲取到一個key,然后從管理在PaintingBinding中的快取池查找圖片流,繼續看關鍵的obtainKey和load方法,去到定義的地方,可以發現這兩個都是子類實作的,從注釋中可以看到,obtainKey的功能就是根據傳入的ImageConfiguration生成一個獨一無二的key(廢話),而load方法則是將key轉換成為一個ImageStreamCompleter物件并開始加載圖片,
那么,我們從最簡單的MemoryImage入手,首先看看obtainKey:
@override
Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MemoryImage>(this);
}
可以看到,就只是把自己包了一層再回傳,并沒有什么特殊,接著看load:
@override
ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
);
}
Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
assert(key == this);
return decode(bytes);
}
同樣非常簡單,就是創建了一個ImageStreamCompleter的子類物件,同時傳入了一個包裝了解碼器的Future(這個解碼器是PaintingBinding.instance.instantiateImageCodec,內部呼叫native方法進行圖片解碼),
看到這里,相信基本有猜想了,資料和解碼器都提供了,看來ImageStreamCompleter就是我們要看的資料源提供者,
3、圖片資料加載ImageStream、ImageStreamCompleter
廢話不多說,直接看MultiFrameImageStreamCompleter,可以看到直接在建構式中獲取codec物件,在獲取到以后就會去獲取解碼資料,下面是簡化的代碼片段:
// 建構式中獲取codec
codec.then<void>(_handleCodecReady, one rror: (dynamic error, StackTrace stack) {// 略});
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
// 拿到codec之后解碼資料
_decodeNextFrameAndSchedule();
}
}
Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
// 略
return;
}
if (_codec.frameCount == 1) {
// 發送資料
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
_scheduleAppFrame();
}
看到這里,終于找到了發送資料的地方,_emitFrame里面會呼叫setImage,而后在setImage中會找到listener并將資料發送,而listener就是widgets.Image注冊的監聽器,
4、快取池PaintingBinding#imageCache
看完了加載流程,我們看看快取池的快取邏輯,回到ImageProvider的resolve方法,這里有個關鍵點,傳給PaintingBinding的是個創建方法,而非物體,進入其原始碼可以看到是先檢測cache中是否存在該物件,存在則直接回傳,不存在才會呼叫load方法進行創建:
final _CachedImage image = _cache.remove(key);
if (image != null) {
// 有快取就直接回傳
_cache[key] = image;
return image.completer;
}
try {
// 沒找到快取就調外面傳入的loader()進行創建
result = loader();
} // catch部分省略
并且,在剛創建時快取中的物件是個PendingImage,這東西可以理解為類似一個占位符的作用,當圖片資料加載完畢河駁換成實際資料物件CacheImage:
void listener(ImageInfo info, bool syncCall) {
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
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();
}
// 這里創建了一個PendingImage插入快取
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// 監聽加載狀態,result就是ImageStreamCompleter
result.addListener(streamListener);
}
5、網路圖片加載
看完最基本的圖片資料加載,接下來看看NetworkImage如何加載網路圖片,看核心的load方法:
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
// 關鍵點1,加載、決議資料
codec: _loadAsync(key, chunkEvents, decode),
// 關鍵點2,分塊下載事件流傳給completer用
chunkEvents: chunkEvents.stream,
scale: key.scale,
);
}
直接進入關鍵方法,看NetworkImage的_loadAsync方法:
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);
} finally {
chunkEvents.close();
}
}
這里有2個點:
(1)通過HttpClient進行圖片下載,下載失敗或者資料為空都會拋例外,這里要做好例外處理,另外,從上面的圖片快取邏輯可以看到,flutter默認是只有記憶體快取的,磁盤快取需要自己處理,可以在這里入手處理;
(2)通過consolidateHttpClientResponseBytes接收資料,并將下載進度轉成ImageChunkEvent發送出去,可以看看MultiFrameImageStreamCompleter對ImageChunkEvent的處理:
if (chunkEvents != null) {
chunkEvents.listen(
(ImageChunkEvent event) {
if (hasListeners) {
// 把這個事件傳遞給ImageStreamListener的onChunk方法
final List<ImageChunkListener> localListeners = _listeners
.map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)
.where((ImageChunkListener chunkListener) => chunkListener != null)
.toList();
for (ImageChunkListener listener in localListeners) {
listener(event);
}
}
}
);
}
順著_listeners的來源,一路往上找,最后可以看到onChunk方法是這里傳進來的:
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}
widget.loadingBuilder即自定義loading狀態的方法,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/211195.html
標籤:Dart
