今天來學習一下 Flutter 自身是如何加載圖片和管理圖片的, Flutter 提供了一個圖片控制元件 Image,Image 定義了若干中加載圖片的方式,包括 Image.asset、Image.file、Image.network、Image.memory, Image內部維護了一個 ImageProvider物件,ImageProvider則真正維護整個圖片加載的作業,Widget 本身內部是體現在 RawImage中: ?
圖片控制元件
?
// 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,
);
return result;
這里可以看到 _imageInfo 決定 RawImage如何展示圖片, _imageInfo 則會在圖片的每一幀進行重新賦值: ?
// image.dart
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
}
}
那么圖片資訊是從哪里來的呢,它是由 _resolveImage 這個方法發起的,這個方法會在 _ImageState 的 didChangeDependencies、 didUpdateWidget和 reassemble方法進行呼叫, 也就是控制元件發生變化重繪狀態的時候,就會重新去決議圖片, ?
圖片決議
_resolveImage 邏輯如下:
void _resolveImage() {
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
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,
));
_updateSourceStream(newStream);
}
這里會用 ScrollAwareImageProvider 包裝一下,ScrollAwareImageProvider 的功能我們后面會介紹,這里先跳過, ?
//ImageProvider# resolve
ImageStream resolve(ImageConfiguration configuration) {
_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,
);
}
);
}
resolve 方法呼叫 _createErrorHandlerAndKey 來處理圖片加載的例外情況,當圖片正常加載的時候,會執行 resolveStreamForKey, ?
//resolveStreamForKey
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
if (stream.completer != null) {
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => stream.completer!,
one rror: handleError,
);
return;
}
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => load(key, PaintingBinding.instance!.instantiateImageCodec),
one rror: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}
Flutter 會把圖片快取相關的邏輯維護在 ImageCache這個物件,
快取管理
ImageCache里面有 3 個 map:

分別表示
- 正在加載的圖片
- 快取在記憶體的圖片
- 表示正活躍的圖片,Widget 狀態變化后可能會清空
?
新增快取
新增快取的時候會設定 map 的 key, key 由 ImageProvider 物件提供,例如:
- AssetImage 當包名和bundle一樣的時候,key可以認為是一樣的,
- NetworkImage 當圖片 url 和比例一樣的時候,key可以認為是一樣的,
ImageCache 實際上是一個單例物件,也就是 Flutter 的圖片快取管理是全域的,ImageCache 最重要的方法就是 putIfAbsent:
// 整理過核心邏輯的代碼
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener? one rror }) {
// 根據key從正在加載的map里獲取快取,如果有直接回傳
ImageStreamCompleter? result = _pendingImages[key]?.completer;
if (result != null) {
return result;
}
// 檢查記憶體快取,存在的話更新存活map
final _CachedImage? image = _cache.remove(key);
if (image != null) {
_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
_cache[key] = image;
return image.completer;
}
// 沒有快取,從 _live 里面取
final _CachedImage? liveImage = _liveImages[key];
if (liveImage != null) {
// 更新快取
_touch(key, liveImage, timelineTask);
return liveImage.completer;
}
// 3 個 map 都沒有獲取到快取的圖片
result = loader(); // 加載
_trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
_PendingImage? untrackedPendingImage;
//定義一個listener
void listener(ImageInfo? info, bool syncCall) {
// 加載的監聽
}
// 包裝一個listener
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;
}
listener 回呼的邏輯: 在 Image 狀態改變的時候,會觸發對 liveImages 的修改: ?
// Image
_imageStream.removeListener(_getListener());
// ImageStream
void removeListener(ImageStreamListener listener) {
for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {
callback();
}
_onLastListenerRemovedCallbacks.clear();
}
而在 _trackLiveImage 的時候,_LiveImage 都注冊了上面的這個 callback:
_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
這時候改圖片會從 _liveImages 里面移除,
由此可見,快取的優先級為 pending -> cache -> live -> load,圖片快取和獲取的流程如下圖所示:
快取清理
在更新快取大小的時候,還會進行快取大小的檢查:
void _checkCacheSize(TimelineTask? timelineTask) {
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key]!;
_currentSizeBytes -= image.sizeBytes!;
_cache.remove(key);
}
}
當當前快取總容量大于最大容量或者快取數量大于最大數量的時候,就會進行快取的清理, 所以上面使用快取的程序中,多次訪問的快取就會把key往后放,避免一上來就被清理掉, 所以 Flutter 自身的快取清理演算法也是遵循了 “最近最少使用” 的, 圖片快取的邏輯如下圖所示:

圖片加載
圖片加載主要依賴上面的 load方法進行,不同的 ImageProvider 子類有自己的實作,例如
AssetImage
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
debugLabel: key.name,
informationCollector: collector
);
NetworkImage
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
chunkEvents: chunkEvents.stream,
codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
scale: key.scale,
debugLabel: key.url,
informationCollector: _imageStreamInformationCollector(key));
邏輯基本一樣,具體特異的流程體現在 loadAsync里面: ?
// AssetImage _loadAsync
try {
data = await key.bundle.load(key.name);
} on FlutterError {
PaintingBinding.instance!.imageCache!.evict(key);
rethrow;
}
if (data == null) {
// 加載資料是null,清掉這個key的快取
PaintingBinding.instance!.imageCache!.evict(key);
throw StateError('Unable to read data');
}
return await decode(data.buffer.asUint8List());
/// NetworkImage _loadAsync
Future<ui.Codec> _loadAsync(
NetworkImage key,
image_provider.DecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents) {
final Uri resolved = Uri.base.resolve(key.url);
return ui.webOnlyInstantiateImageCodecFromUrl(resolved, // ignore: undefined_function
chunkCallback: (int bytes, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
}) as Future<ui.Codec>;
}
這里分別會從 bundle 里加載圖片和從網路拉取圖片, ?
滑動中處理
還記得上面提到的 ScrollAwareImageProvider嗎,這里會有一個關于滑動中的判斷: ?
if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
SchedulerBinding.instance.scheduleFrameCallback((_) {
scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
});
return;
}
當 if 里的邏輯成立,就把決議圖片的作業放到下一幀,recommendDeferredLoadingForContext 的具體邏輯: ?
static bool recommendDeferredLoadingForContext(BuildContext context) {
final _ScrollableScope widget =
context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()?.widget as _ScrollableScope;
if (widget == null) {
return false;
}
// 存在滑動的widget
return widget.position.recommendDeferredLoading(context);
}
這個會找到 Widget 樹里面最近的 _ScrollableScope,如果 ScrollableScope 處于快速滑動的時候,就回傳true,所以 flutter 在快速滑動的串列中是不會加載圖片的, ?
總結
到這里 Flutter 圖片的加載和快取管理就介紹完了,我們可以認識到幾個問題
- Flutter 本身是有圖片的記憶體快取,也是按照 LRU 的演算法去管理快取的,并且快取池有閾值,我們可以自己去設定我們想要的記憶體閾值,
- Flutter 本身沒有提供圖片的磁盤快取,APP 重啟之后圖片加載流程是會重新走的
Flutter學習資料以及Android進階學習大全可以找我免費領取哦
鏈接直達:https://jq.qq.com/?_wv=1027&k=7vXTe5CZ
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/353447.html
標籤:其他
上一篇:從用戶輸入創建物件
