一、前言
最近在做的項(xiàng)目中,總是用到Image組件,所以就了解了一下Image的源碼,順便記錄下來,和大家分享一下。
本文是基于1.12.13+hotfix.8的源碼,以加載網(wǎng)路圖片為例進(jìn)行解讀。畢竟自己還是個(gè)小白,如果有解讀不對(duì)的地方,歡迎指正。
二、Image
Image繼承了StatefulWidget,是用于顯示圖片的 Widget,最后通過內(nèi)部的 RenderImage 繪制。
先看看Image結(jié)構(gòu),以Image.network為例:
先簡單介紹一下這些類,后續(xù)我們會(huì)一一詳細(xì)介紹。
- Image用來顯示圖片。
- _ImageState處理生命周期,生成Widget。
- ImageProvider用來加載圖片,生成key。
- NetWorkImage是具體執(zhí)行下載的,將下載的圖片轉(zhuǎn)化成ui.Codec,然后由ImageStreamCompleter去處理。
- ImageStreamCompleter用來逐幀解析圖片。
- ImageStream是存儲(chǔ)加載結(jié)果監(jiān)聽器List的。
- MultiFrameImageStreamCompleter是多幀圖片解析器。
- ImageStreamListener 實(shí)際監(jiān)聽加載結(jié)果
下面我們開始看下源碼。
構(gòu)造函數(shù)
Image.network(
String src,{
Key key,
@required this.image,
this.frameBuilder,
this.loadingBuilder,
...
this.filterQuality = FilterQuality.low,
}): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
// ...
super(key: key);
Image.network以命名構(gòu)造函數(shù)創(chuàng)建Image對(duì)象時(shí),會(huì)同時(shí)初始化實(shí)例變量image。
- src:圖片的url
- image:必選參數(shù),一個(gè)ImageProvide對(duì)象,圖片的提供者,在調(diào)用的時(shí)候已經(jīng)實(shí)例化,稍后會(huì)具體介紹。
//ImageProvider初始化
class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
...
static ImageProvider<dynamic> resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider<dynamic> provider) {
if (cacheWidth != null || cacheHeight != null) {
return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
}
return provider;
}
}
State
作為一個(gè)StatefulWidget,最重要的當(dāng)然是State了。
@override
_ImageState createState() => _ImageState();
Image的主要構(gòu)成就是兩部分,和
接下來我們分別介紹一下這兩部分。
三、_ImageState
Image是一個(gè)StatefulWidget,狀態(tài)由_ImageState控制。_ImageState繼承自State,其生命周期方法包括initState()、didChangeDependencies()、build()、dispose()、didUpdateWidget()等。我們先來看看_ImageState中都做了些什么。
成員變量
class _ImageState extends State<Image> with WidgetsBindingObserver {
ImageStream _imageStream;
ImageInfo _imageInfo;
bool _isListeningToStream = false;
···
}
- _imageStream
處理Image Resource的,ImageStream里存儲(chǔ)著圖片加載完畢的監(jiān)聽回調(diào) - _imageInfo
Image的數(shù)據(jù)源信息:width和height以及ui.Image。 將ImageInfo里的ui.Image設(shè)置給RawImage就可以展示了。RawImage就是我們真正渲染的對(duì)象
生命周期函數(shù)
- initState
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);//監(jiān)聽生命周期
}
- didChangeDependencies
@override
void didChangeDependencies() {
...
_resolveImage();
if (TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream();
super.didChangeDependencies();
}
_resolveImage()方法是核心,我們來分析一下。
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 (_imageStream?.key == newStream?.key)
return;
if (_isListeningToStream)
_imageStream.removeListener(_getListener());
if (!widget.gaplessPlayback)
setState(() { _imageInfo = null; });
setState(() {
_loadingProgress = null;
_frameNumber = null;
_wasSynchronouslyLoaded = false;
});
_imageStream = newStream;
if (_isListeningToStream)
_imageStream.addListener(_getListener());
}
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}
1、 通過ImageProvider得到ImageStream 對(duì)象
2、 然后 _ImageState 利用 ImageStream 添加監(jiān)聽,等待圖片數(shù)據(jù)
- didUpdateWidget
@override
void didUpdateWidget(Image oldWidget) {
super.didUpdateWidget(oldWidget);
if (_isListeningToStream &&
(widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
_imageStream.removeListener(_getListener(oldWidget.loadingBuilder));
_imageStream.addListener(_getListener());
}
if (widget.image != oldWidget.image)
_resolveImage();
}
- build
@override
Widget build(BuildContext context) {
Widget result = RawImage(
image: _imageInfo?.image,
...
);
if (!widget.excludeFromSemantics) {
result = Semantics(
...
);
}
...
return result;
}
四、ImageProvider
ImageProvider是一個(gè)抽象類,提供圖片數(shù)據(jù)獲取和加載的的接口,NetworkImage 、AssetImage 等均實(shí)現(xiàn)了這個(gè)接口。
它主要有兩個(gè)功能:
- 提供圖片數(shù)據(jù)源
- 緩存圖片
abstract class ImageProvider<T> {
//接收ImageConfiguration參數(shù),返回ImageStream-圖片數(shù)據(jù)流
ImageStream resolve(ImageConfiguration configuration) {
...
}
//清除指定key對(duì)應(yīng)的圖片緩存
Future<bool> evict({ ImageCache cache,ImageConfiguration configuration = ImageConfiguration.empty }) async {
...
}
//需要ImageProvider子類實(shí)現(xiàn),不同的ImageProvider對(duì)key的定義邏輯不同
Future<T> obtainKey(ImageConfiguration configuration);
// 需ImageProvider子類實(shí)現(xiàn),加載圖片數(shù)據(jù)
@protected
ImageStreamCompleter load(T key);
}
- resolve
獲取數(shù)據(jù)流 - evict
清除緩存 - obtainKey
配合實(shí)現(xiàn)圖片緩存 - load
加載圖片數(shù)據(jù)源
4.1 resolve方法解析
#ImageProvider
ImageStream resolve(ImageConfiguration configuration) {
//1、創(chuàng)建圖片數(shù)據(jù)流
final ImageStream stream = ImageStream();
T obtainedKey; //
//2、錯(cuò)誤處理
Future<void> handleError(dynamic exception, StackTrace stack) async {
...
stream.setCompleter(imageCompleter);
imageCompleter.setError(...);
}
//3、創(chuàng)建一個(gè)新Zone,用來處理發(fā)生的錯(cuò)誤,不干擾MainZone
final Zone dangerZone = Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
handleError(error, stackTrace);
}
)
);
dangerZone.runGuarded(() {
// 4、判斷是否有緩存的相關(guān)邏輯
Future<T> key;
try {
// 5、生成key,后續(xù)會(huì)用此key判斷是否有緩存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
// 6、緩存處理邏輯
obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
onError: handleError,
);
if (completer != null) {
//7、stream設(shè)置ImageStreamCompleter對(duì)象
stream.setCompleter(completer);
}
}).catchError(handleError);
});
return stream;
}
這段代碼中,我們需要重點(diǎn)看四個(gè)點(diǎn),
- ImageStream
- ImageCache
- obtainKey 方法
- ImageStreamCompleter
ImageStream
存儲(chǔ)ImageStreamCompleter,監(jiān)聽圖片加載結(jié)果。
ImageCache
在resolve 方法中調(diào)用了PaintingBinding.instance.imageCache.putIfAbsent方法(注釋6處),這里的PaintingBinding.instance.imageCache 是 ImageCache的一個(gè)實(shí)例。PaintingBinding.instance和imageCache是單例的,所以說圖片緩存是項(xiàng)目全局的。
const int _kDefaultSize = 1000;// 最大緩存數(shù)量,默認(rèn)1000
const int _kDefaultSizeBytes = 100 << 20; // 最大緩存容量,默認(rèn)100 MB
class ImageCache {
// 正在加載中的圖片隊(duì)列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 緩存隊(duì)列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 最大緩存數(shù)量,默認(rèn)1000
int _maximumSize = _kDefaultSize;
// 最大緩存容量,默認(rèn)100 MB
int _maximumSizeBytes = _kDefaultSizeBytes;
... // 省略部分代碼
// 清除全部緩存
void clear() {
...
}
// 根據(jù)key清楚緩存
bool evict(Object key) {
// ...省略代碼
}
//重點(diǎn)方法
// 參數(shù) key用來獲取緩存,loader()加載回調(diào)方法,onError加載失敗回調(diào)
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
//_pendingImage 用于標(biāo)示該key的圖片處于加載中的狀態(tài)
ImageStreamCompleter result = _pendingImages[key]?.completer;
// 圖片還未加載成功,直接返回
if (result != null)
return result;
// 先移除緩存,拿到移除的緩存對(duì)象
final _CachedImage image = _cache.remove(key);
//把最近一次使用過的緩存在_map中
if (image != null) {
_cache[key] = image;
return image.completer;
}
//沒有緩存,使用loader()方法加載
try {
result = loader();
} catch (error, stackTrace) {
...
}
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();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
}
return result;
}
// 當(dāng)超過緩存最大數(shù)量或最大緩存容量,調(diào)用此方法清理到緩存,保持著最大數(shù)量和容量
void _checkCacheSize() {
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key];
_currentSizeBytes -= image.sizeBytes;
_cache.remove(key);
}
...
}
}
putIfAbsent方法主要是先通過 key 判斷內(nèi)存中正在緩存的對(duì)象或者是否有緩存,如果有就返回該對(duì)象的ImageStreamCompleter ,否則就調(diào)用 loader 去加載并返回ImageStreamCompleter。
這里提醒大家兩個(gè)地方:
- 圖片緩存是在內(nèi)存中,沒有進(jìn)行本地存儲(chǔ)。
- 應(yīng)用生命周期內(nèi),如果緩存沒有超過上限,相同的圖片(key相同)只會(huì)被下載一次。
ImageStreamCompleter
putIfAbsent的返回值返回了ImageStreamCompleter,而resolve方法中,最后調(diào)用了ImageStream的setCompleter的方法,給ImageStream設(shè)置一個(gè)ImageStreamCompleter對(duì)象。
#ImageStream
void setCompleter(ImageStreamCompleter value) {
assert(_completer == null);
_completer = value;
if (_listeners != null) {
final List<ImageStreamListener> initialListeners = _listeners;
_listeners = null;
initialListeners.forEach(_completer.addListener);
}
}
ImageStreamCompleter是一個(gè)抽象類,定義了管理圖片加載過程的一些接口,Image Widget中正是通過它來監(jiān)聽圖片加載狀態(tài)的。每一個(gè)ImageStream對(duì)象只能設(shè)置一次,ImageStreamCompleter是為了輔助ImageStream解析和管理Image圖片幀的,并且判斷是否有初始化監(jiān)聽器,可以做一些初始化回調(diào)工作。
abstract class ImageStreamCompleter extends Diagnosticable {
final List<_ImageListenerPair> _listeners = <_ImageListenerPair>[];
ImageInfo _currentImage;
FlutterErrorDetails _currentError;
void addListener(ImageListener listener, { ImageErrorListener onError }) {...}
void removeListener(ImageListener listener) {... }
void reportError(...) {... }
@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 (ImageStreamListener listener in localListeners) {
try {
listener.onImage(image, false);
} catch (exception, stack) {
reportError(
...
);
}}}}
4.2 obtainKey
key是圖片緩存的一個(gè)唯一標(biāo)識(shí),也是判斷該圖片是否應(yīng)該被緩存的唯一條件。這個(gè)key就是ImageProvider.obtainKey()方法的返回值,不同類型的ImageProvider對(duì)key的定義邏輯會(huì)不同,所以此方法需要ImageProvider子類去重寫。我們以NetworkImage為例,看一下它的obtainKey()實(shí)現(xiàn):
#NetworkImage
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
其實(shí)就是創(chuàng)建一個(gè)future,然后將NetworkImage自身做為key返回。
那么又是如何判斷key是否相等的呢?
#NetworkImage
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
在NetworkImage中,是將url+ scale(縮放比例)作為緩存中的key。只有url和scale相等,才算是有緩存。也就是說如果兩張圖片的url或scale只要有一個(gè)不同,便會(huì)重新下載并分別緩存。
4.3 load(T key)方法解析
load()是ImageProvider加載圖片數(shù)據(jù)源的接口,不同ImageProvider的數(shù)據(jù)源加載方法不同,每個(gè)ImageProvider的子類必須實(shí)現(xiàn)它。比如NetworkImage類和AssetImage類,它們都是ImageProvider的子類,NetworkImage是從網(wǎng)絡(luò)來加載圖片數(shù)據(jù),AssetImage則是從最終的應(yīng)用包里來加載。
我們以NetworkImage為例,看看其load方法的實(shí)現(xiàn):
#NetworkImage
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), //調(diào)用_loadAsync
chunkEvents: chunkEvents.stream,
scale: key.scale,
...
);
}
MultiFrameImageStreamCompleter 是一個(gè)多幀圖片管理器,是ImageStreamCompleter的一個(gè)子類。
MultiFrameImageStreamCompleter 需要一個(gè)Future<ui.Codec>類型的參數(shù)——codec。Codec 是處理圖片編解碼的類的一個(gè)handler,是一個(gè)flutter engine API 的包裝類。圖片的編解碼的邏輯不是在Dart 代碼部分實(shí)現(xiàn),而是在flutter engine中實(shí)現(xiàn)的。
MultiFrameImageStreamCompleter({
@required Future<ui.Codec> codec,
@required double scale,
Stream<ImageChunkEvent> chunkEvents,
InformationCollector informationCollector,
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale {
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError(...);
});
if (chunkEvents != null) {
chunkEvents.listen(
(ImageChunkEvent event) {
if (hasListeners) {
// Make a copy to allow for concurrent modification.
final List<ImageChunkListener> localListeners = _listeners
.map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)
.where((ImageChunkListener chunkListener) => chunkListener != null)
.toList();
for (ImageChunkListener listener in localListeners) {
listener(event);
}
}
}, onError: (dynamic error, StackTrace stack) {//...},
);
}
}
Codec類部分定義如下:
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此類由flutter engine創(chuàng)建,不應(yīng)該手動(dòng)實(shí)例化此類或直接繼承此類。
@pragma('vm:entry-point')
Codec._();
/// 圖片中的幀數(shù)(動(dòng)態(tài)圖會(huì)有多幀)
int get frameCount native 'Codec_frameCount';
/// 動(dòng)畫重復(fù)的次數(shù),0 -只執(zhí)行一次,-1-循環(huán)執(zhí)行
int get repetitionCount native 'Codec_repetitionCount';
/// 獲取下一個(gè)動(dòng)畫幀
Future<FrameInfo> getNextFrame() {
return _futurize(_getNextFrame);
}
String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
}
我們可以看到Codec最終的結(jié)果是一個(gè)或多個(gè)(動(dòng)圖)幀,而這些幀最終會(huì)繪制到屏幕上。
MultiFrameImageStreamCompleter 的 codec參數(shù)值為_loadAsync方法的返回值,我們繼續(xù)看_loadAsync方法的實(shí)現(xiàn):
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
//下載圖片
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 Exception(...);
// 接收?qǐng)D片數(shù)據(jù)
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
// 下載進(jìn)度
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
// 對(duì)圖片數(shù)據(jù)進(jìn)行解碼
return decode(bytes);//PaintingBinding.instance.instantiateImageCodec(bytes)
} finally {
chunkEvents.close();
}
}
_loadAsync方法主要做了兩件事:
- 下載圖片。
- 對(duì)下載的圖片數(shù)據(jù)進(jìn)行解碼。
下載邏輯比較簡單:通過HttpClient從網(wǎng)上下載圖片,另外下載請(qǐng)求會(huì)設(shè)置一些自定義的header,開發(fā)者可以通過NetworkImage的headers命名參數(shù)來傳遞。
在圖片下載完成后調(diào)用了PaintingBinding.instance.instantiateImageCodec(bytes)對(duì)圖片進(jìn)行解碼,instantiateImageCodec(...)也是一個(gè)Native API的包裝,會(huì)調(diào)用Flutter engine的instantiateImageCodec方法,源碼如下:
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
native 'instantiateImageCodec';
codec的異步方法執(zhí)行完成后會(huì)調(diào)用_handleCodecReady函數(shù)。
//MultiFrameImageStreamCompleter
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
該方法將codec對(duì)象保存起來,然后解碼圖片幀
#MultiFrameImageStreamCompleter
Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
reportError(...);
return;
}
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));
return;
}
_scheduleAppFrame();
}
如果只有一幀,則執(zhí)行_emitFrame函數(shù)。從幀數(shù)據(jù)中拿到圖片幀對(duì)象根據(jù)縮放比例創(chuàng)建ImageInfo對(duì)象,然后設(shè)置顯示的圖片信息
#MultiFrameImageStreamCompleter
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}
#ImageStreamCompleter
@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 (ImageStreamListener listener in localListeners) {
try {
listener.onImage(image, false);
} catch (exception, stack) {
reportError(...);
}
}
}
五、Image加載流程總結(jié)
整個(gè)流程大概如下:
- Image構(gòu)造函數(shù)先實(shí)例化一個(gè)ImageProvider
- 在_ImageState的didChangeDependencies方法中通過ImageProvider的resolve方法創(chuàng)建ImageStream對(duì)象,并關(guān)聯(lián)一個(gè)ImageStreamCompleter,之后添加用于監(jiān)聽加載流程的ImageStreamListener1。
- 在獲取ImageStreamCompleter的過程中,如果有緩存,就從緩存中獲取ImageStreamCompleter,如果沒有緩存,就調(diào)用ImageProvider的load方法去加載圖片并返回ImageStreamCompleter對(duì)象,然后給ImageStreamCompleter添加ImageStreamListener2。
- load方法執(zhí)行中會(huì)通過 http 下載圖片,再經(jīng)過PaintingBinding 編碼轉(zhuǎn)化后,得到ui.Codec可繪制對(duì)象,然后MultiFrameImageStreamCompleter調(diào)用_handleCodecReady方法把ui.Codec封裝成ImageInfo。
- 接著MultiFrameImageStreamCompleter會(huì)調(diào)用setImage方法,此方法觸發(fā)加載監(jiān)聽ImageStreamListener1和ImageStreamListener2。
- ImageStreamListener1回調(diào)到_ImageState,將ImageInfo保存, ImageStreamListener2的回調(diào)會(huì)把Image緩存下來。
- _ImageState的 build方法中的會(huì)根據(jù)ImageInfo構(gòu)建一個(gè) RawImage 對(duì)象。
- 最后 RawImage中的 RenderImage 通過paint方法繪制Widget。
六、如何減輕圖片帶來的內(nèi)存壓力?
//修改緩存最大值
const int _kDefaultSize = 100;
const int _kDefaultSizeBytes = 50 << 20;
//退出頁面清除緩存
@override
void dispose() {
PaintingBinding.instance.imageCache.clear();
super.dispose();
}
七、添加磁盤緩存
上面我們已經(jīng)知道,Image只有內(nèi)存緩存,沒有本地緩存。那么我們?nèi)绾翁砑颖镜鼐彺婺兀科鋵?shí)只需要改進(jìn)NetWorkImage的_loadAsync方法。
Future<ui.Codec> _loadAsync(NetworkImage key,StreamController<ImageChunkEvent> chunkEvents, image_provider.DecoderCallback decode,) async {
try {
assert(key == this);
//--------新增代碼1 begin--------------
// 判斷是否有本地緩存
final Uint8List cacheImageBytes = await ImageCacheUtil.getImageBytes(key.url);
if(cacheImageBytes != null) {
return decode(cacheImageBytes);
}
//--------新增代碼1 end--------------
//...省略
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
//--------新增代碼2 begin--------------
// 緩存圖片數(shù)據(jù)到本地,需要定制具體的緩存策略
await ImageCacheUtil.saveImageBytesToLocal(key.url, bytes);
//--------新增代碼2 end--------------
return decode(bytes);
} finally {
chunkEvents.close();
}
}