Flutter入門系列(四)---Flutter圖片快取
Flutter圖片快取 | Image.network原始碼分析
原創: 郭海生 京東技術
原文地址:https://mp.weixin.qq.com/s/W5iu3VsNuvIygFbKM-giqA
隨著手機裝置硬體水平的飛速發展,使用者對於圖片的顯示要求也越來越高,稍微處理不好就會容易造成記憶體溢位等問題。所以我們在使用Image的時候,建立一個圖片快取機制已經是一個常態。Android目前提供了很豐富的圖片框架,像ImageLoader、Glide、Fresco等。對於Flutter而言,為了探其快取機制或者定製自己的快取框架,特從其Image入手進行突破。
>>>>
Image 的用法
Image是Flutter裡提供的顯示圖片的控制元件,類似Android裡ImageView,不過其用法有點類似Glide等圖片框架。
我們先看Image的用法。Flutter對Image控制元件提供了多種建構函式:
new Image 用於從ImageProvider獲取影象
new Image.asset 用於使用key從AssetBundle獲取影象
new Image.network 用於從URL地址獲取影象
new Image.file 用於從File獲取影象
我們只分析Image.network原始碼,分析理解完這個之後,其他的也是一樣的思路。
我們先從Image.network的用法入手:顯示一個網路圖片很簡單,直接通過Image.network攜帶一個url引數即可。
範例:
return new Scaffold(
appBar: new AppBar(
title: new Text("Image from Network"),
),
body: new Container(
child: new Column(
children: <Widget>[
// Load image from network
new Image.network(
'https://flutter.io/images/flutter-mark-square-100.png'),
],
)),
);
>>>>
Image結構UML類圖
我們首先看一下Image的UML類圖:
可以看到Image的框架結構還是有點兒複雜的,在你只調用一行程式碼的情況下,其實Flutter為你做了很多工作。
初步梳理下每個類概念:
-
StatefulWidget就是有狀態的Widget,是展示在頁面上的元素。
-
Image繼承於StatefulWidget,是來顯示和載入圖片。
-
State控制著StatefulWidget狀態改變的生命週期,當Widget被建立、Widget配置資訊改變或者Widget被銷燬等等,State的一系列方法會被呼叫。
-
_ImageState繼承於State,處理State生命週期變化以及生成Widget。
-
ImageProvider提供載入圖片的入口,不同的圖片資源載入方式不一樣,只要重寫其load方法即可。同樣,快取圖片的key值也有其生成。
-
NetWorkImage負責下載網路圖片的,將下載完成的圖片轉化成ui.Codec物件交給ImageStreamCompleter去處理解析。
-
ImageStreamCompleter就是逐幀解析圖片的。
-
ImageStream是處理Image Resource的,ImageState通過ImageStream與ImageStreamCompleter建立聯絡。ImageStream裡也儲存著圖片載入完畢的監聽回撥。
-
MultiFrameImageStreamCompleter就是多幀圖片解析器。
先把Image的框架結構瞭解一下,有助於下面我們更加清晰地分析程式碼。
>>>>
原始碼分析
我們看下Image.network都做了什麼:
-
class Image extends StatefulWidget {
-
Image.network(String src, {
-
Key key,
-
double scale = 1.0,
-
this.width,
-
this.height,
-
this.color,
-
this.colorBlendMode,
-
this.fit,
-
this.alignment = Alignment.center,
-
this.repeat = ImageRepeat.noRepeat,
-
this.centerSlice,
-
this.matchTextDirection = false,
-
this.gaplessPlayback = false,
-
Map<String, String> headers,
-
}) : image = new NetworkImage(src, scale: scale, headers: headers),
-
assert(alignment != null),
-
assert(repeat != null),
-
assert(matchTextDirection != null),
-
super(key: key);
-
......
我們看到Image是一個StatefulWidget物件,可以直接放到Container或者Column等容器裡,其屬性解釋如下:
-
width:widget的寬度
-
height:widget的高度
-
color:與colorBlendMode配合使用,將此顏色用BlendMode方式混合圖片
-
colorBlendMode:混合模式演算法
-
fit:與android:scaletype一樣,控制圖片如何resized/moved來匹對Widget的size
-
alignment:widget對齊方式
-
repeat:如何繪製未被影象覆蓋的部分
-
centerSlice:支援9patch,拉伸的中間的區域
-
matchTextDirection:繪製圖片的方向:是否從左到右
-
gaplessPlayback:圖片變化的時候是否展示老圖片或者什麼都不展示
-
headers:http請求頭
-
image:一個ImageProvide物件,在呼叫的時候已經例項化,這個類主要承擔了從網路載入圖片的功能。它是載入圖片的最重要的方法,不同的圖片載入方式(assert檔案載入、網路載入等等)也就是重寫ImageProvider載入圖片的方法(load())。
Image是一個StatefulWidget物件,所以我們看它的State物件:
-
class _ImageState extends State<Image> {
-
ImageStream _imageStream;
-
ImageInfo _imageInfo;
-
bool _isListeningToStream = false;
-
}
-
-
class ImageStream extends Diagnosticable {
-
ImageStreamCompleter get completer => _completer;
-
ImageStreamCompleter _completer;
-
-
List<ImageListener> _listeners;
-
-
/// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
-
void setCompleter(ImageStreamCompleter value) {
-
assert(_completer == null);
-
_completer = value;
-
print("setCompleter:::"+(_listeners==null).toString());
-
if (_listeners != null) {
-
final List<ImageListener> initialListeners = _listeners;
-
_listeners = null;
-
initialListeners.forEach(_completer.addListener);
-
}
-
}
-
-
/// Adds a listener callback that is called whenever a new concrete [ImageInfo]
-
void addListener(ImageListener listener) {
-
if (_completer != null)
-
return _completer.addListener(listener);
-
_listeners ??= <ImageListener>[];
-
_listeners.add(listener);
-
}
-
-
/// Stop listening for new concrete [ImageInfo] objects.
-
void removeListener(ImageListener listener) {
-
if (_completer != null)
-
return _completer.removeListener(listener);
-
assert(_listeners != null);
-
_listeners.remove(listener);
-
}
-
}
我們對_ImageState的兩個屬性物件解釋一下:
-
ImageStream是處理Image Resource的,ImageStream裡儲存著圖片載入完畢的監聽回撥,ImageStreamCompleter也是其成員,這樣ImageStream將圖片的解析流程交給了ImageStreamCompleter去處理。
-
ImageInfo包含了Image的資料來源資訊:width和height以及ui.Image。 將ImageInfo裡的ui.Image設定給RawImage就可以展示了。RawImage就是我們真正渲染的物件,是顯示ui.Image的一個控制元件,接下來我們會看到。
我們知道State的生命週期,首先State的initState執行,然後didChangeDependencies會執行,我們看到ImageState裡沒有重寫父類的initState,那我們看其didChangeDependencies():
-
@override
-
void didChangeDependencies() {
-
_resolveImage();
-
-
if (TickerMode.of(context))
-
_listenToStream();
-
else
-
_stopListeningToStream();
-
-
super.didChangeDependencies();
-
}
>>>>
_resolveImage方法解析
我們看到首先呼叫了resolveImage(),我們看下resolveImage方法:
-
void _resolveImage() {
-
final ImageStream newStream =
-
widget.image.resolve(createLocalImageConfiguration(
-
context,
-
size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null
-
));
-
assert(newStream != null);
-
_updateSourceStream(newStream);
-
}
這個方法是處理圖片的入口。widget.image這個就是上面的建立的NetworkImage物件,是個ImageProvider物件,呼叫它的resolve並且傳進去預設的ImageConfiguration。 我們看下resolve方法,發現NetworkImage沒有,果不其然,我們在其父類ImageProvider找到了:
-
ImageStream resolve(ImageConfiguration configuration) {
-
assert(configuration != null);
-
final ImageStream stream = new ImageStream();
-
T obtainedKey;
-
obtainKey(configuration).then<void>((T key) {
-
obtainedKey = key;
-
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
-
}).catchError(
-
(dynamic exception, StackTrace stack) async {
-
FlutterError.reportError(new FlutterErrorDetails(
-
exception: exception,
-
stack: stack,
-
library: 'services library',
-
context: 'while resolving an image',
-
silent: true, // could be a network error or whatnot
-
informationCollector: (StringBuffer information) {
-
information.writeln('Image provider: $this');
-
information.writeln('Image configuration: $configuration');
-
if (obtainedKey != null)
-
information.writeln('Image key: $obtainedKey');
-
}
-
));
-
return null;
-
}
-
);
-
return stream;
-
}
我們看到這個方法建立了ImageStream並返回,呼叫obtainKey返回一個攜帶NetworkImage的future,以後會作為快取的key使用,並且呼叫ImageStream的setCompleter的方法:
-
void setCompleter(ImageStreamCompleter value) {
-
assert(_completer == null);
-
_completer = value;
-
if (_listeners != null) {
-
final List<ImageListener> initialListeners = _listeners;
-
_listeners = null;
-
initialListeners.forEach(_completer.addListener);
-
}
-
}
這個方法就是給ImageStream設定一個ImageStreamCompleter物件,每一個ImageStream物件只能設定一次,ImageStreamCompleter是為了輔助ImageStream解析和管理Image圖片幀的,並且判斷是否有初始化監聽器,可以做一些初始化回撥工作。 我們繼續看下PaintingBinding.instance.imageCache.putIfAbsent方法:
-
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
-
assert(key != null);
-
assert(loader != null);
-
ImageStreamCompleter result = _pendingImages[key];
-
// Nothing needs to be done because the image hasn't loaded yet.
-
if (result != null)
-
return result;
-
// Remove the provider from the list so that we can move it to the
-
// recently used position below.
-
final _CachedImage image = _cache.remove(key);
-
if (image != null) {
-
_cache[key] = image;
-
return image.completer;
-
}
-
result = loader();
-
void listener(ImageInfo info, bool syncCall) {
-
// Images that fail to load don't contribute to cache size.
-
final int imageSize = info.image == null ? 0 : info.image.height * info.image.width * 4;
-
final _CachedImage image = new _CachedImage(result, imageSize);
-
_currentSizeBytes += imageSize;
-
_pendingImages.remove(key);
-
_cache[key] = image;
-
result.removeListener(listener);
-
_checkCacheSize();
-
}
-
if (maximumSize > 0 && maximumSizeBytes > 0) {
-
_pendingImages[key] = result;
-
result.addListener(listener);
-
}
-
return result;
-
}
這個是Flutter預設提供的記憶體快取api的入口方法,這個方法會先通過key獲取之前的ImageStreamCompleter物件,這個key就是NetworkImage物件,當然我們也可以重寫obtainKey方法自定義key,如果存在則直接返回,如果不存在則執行load方法載入ImageStreamCompleter物件,並將其放到首位(最少最近使用演算法)。
也就是說ImageProvider已經實現了記憶體快取:預設快取圖片的最大個數是1000,預設快取圖片的最大空間是10MiB。 第一次載入圖片肯定是沒有快取的,所以我們看下loader方法,我們看到ImageProvider是空方法,我們去看NetWorkImage,按照我們的預期確實在這裡:
-
@override
-
ImageStreamCompleter load(NetworkImage key) {
-
return new MultiFrameImageStreamCompleter(
-
codec: _loadAsync(key),
-
scale: key.scale,
-
informationCollector: (StringBuffer information) {
-
information.writeln('Image provider: $this');
-
information.write('Image key: $key');
-
}
-
);
-
}
-
//網路請求載入圖片的方法
-
Future<ui.Codec> _loadAsync(NetworkImage key) async {
-
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 new Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');
-
-
final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
-
if (bytes.lengthInBytes == 0)
-
throw new Exception('NetworkImage is an empty file: $resolved');
-
-
return await ui.instantiateImageCodec(bytes);
-
}
這個方法為我們建立了一個MultiFrameImageStreamCompleter物件,根據名字我們也能知道它繼承於ImageStreamCompleter。還記得ImageStreamCompleter是做什麼的嗎,就是輔助ImageStream管理解析Image的。
引數解析:
-
_loadAsync()是請求網路載入圖片的方法
-
scale是縮放係數
-
informationCollector是資訊收集物件的,提供錯誤或者其他日誌用
MultiFrameImageStreamCompleter是多幀的圖片處理載入器,我們知道Flutter的Image支援載入gif,通過MultiFrameImageStreamCompleter可以對gif檔案進行解析:
-
MultiFrameImageStreamCompleter({
-
@required Future<ui.Codec> codec,
-
@required double scale,
-
InformationCollector informationCollector
-
}) : assert(codec != null),
-
_informationCollector = informationCollector,
-
_scale = scale,
-
_framesEmitted = 0,
-
_timer = null {
-
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
-
FlutterError.reportError(new FlutterErrorDetails(
-
exception: error,
-
stack: stack,
-
library: 'services',
-
context: 'resolving an image codec',
-
informationCollector: informationCollector,
-
silent: true,
-
));
-
});
-
}
-
-
ui.Codec _codec;
-
final double _scale;
-
final InformationCollector _informationCollector;
-
ui.FrameInfo _nextFrame;
我們看到MultiFrameImageStreamCompleter拿到loadAsync返回的codec資料物件,通過handleCodecReady來處理資料,然後會呼叫_decodeNextFrameAndSchedule方法:
-
Future<Null> _decodeNextFrameAndSchedule() async {
-
try {
-
_nextFrame = await _codec.getNextFrame();
-
} catch (exception, stack) {
-
FlutterError.reportError(new FlutterErrorDetails(
-
exception: exception,
-
stack: stack,
-
library: 'services',
-
context: 'resolving an image frame',
-
informationCollector: _informationCollector,
-
silent: true,
-
));
-
return;
-
}
-
if (_codec.frameCount == 1) {
-
// This is not an animated image, just return it and don't schedule more
-
// frames.
-
_emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));
-
return;
-
}
-
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
-
}
通過codec.getNextFrame()去拿下一幀,對於靜態的圖片frameCount是1,直接用ImageInfo組裝image,交給emitFrame方法,這個方法裡會呼叫setImage,如下:
-
@protected
-
void setImage(ImageInfo image) {
-
_current = image;
-
if (_listeners.isEmpty)
-
return;
-
final List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);
-
for (ImageListener listener in localListeners) {
-
try {
-
listener(image, false);
-
} catch (exception, stack) {
-
_handleImageError('by an image listener', exception, stack);
-
}
-
}
-
}
setImage方法就是設定當前的ImageInfo並檢查監聽器列表,通知監聽器圖片已經載入完畢可以重新整理UI了。
對於動圖來說就是就是交給SchedulerBinding逐幀的去呼叫setImage,通知UI重新整理,程式碼就不貼了,有興趣的可以自行檢視下。 至此resolveImage呼叫流程我們算是講完了,接下來我們看listenToStream。
>>>>
_listenToStream方法解析
我們繼續分析didChangeDependencies方法,這個方法裡會判斷TickerMode.of(context)的值,這個值預設是true,和AnimationConrol有關,後續可以深入研究。然後呼叫_listenToStream()。 我們看下這個方法:
-
void _listenToStream() {
-
if (_isListeningToStream)
-
return;
-
_imageStream.addListener(_handleImageChanged);
-
_isListeningToStream = true;
-
}
這個就是新增圖片載入完畢的回撥器。還記得嗎,當圖片載入並解析完畢的時候,MultiFrameImageStreamCompleter的setImage方法會呼叫這裡傳過去的回撥方法。我們看下這裡回撥方法裡做了什麼:
-
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
-
setState(() {
-
_imageInfo = imageInfo;
-
});
-
}
很顯然就是拿到上層傳過來ImageInfo,呼叫setState更新UI 我們看下build方法:
-
Widget build(BuildContext context) {
-
return new RawImage(
-
image: _imageInfo?.image,
-
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,
-
);
-
}
就是用imageInfo和widget的資訊來封裝RawImage,RawImage是RenderObjectWidget物件,是應用程式真正渲染的物件,將咱們的圖片顯示到介面上。
>>>>
總結
梳理下流程:
-
從入口開始,Image是繼承於StatefulWidget,它為咱們實現好了State:_ImageState,並且提供了一個已經例項化的NetWorkImage物件,它是繼承於ImageProvider物件的。
-
ImageState建立完之後,ImageState通過呼叫resolveImage(),resolveImage()又會呼叫ImageProvider的resolve()方法返回一個ImageStream物件。_ImageState也註冊了監聽器給ImageStream,當圖片下載完畢後會執行回撥方法。
-
然後在ImageProvider的resolve()方法裡不僅建立了ImageStream還設定了ImageStream的setComplete方法去設定ImageStreamCompleter,在這裡去判斷是否有快取,沒有快取就呼叫load方法去建立ImageStreamCompleter並且新增監聽器為了執行載入完圖片之後的快取工作。ImageStreamCompleter是為了解析已經載入完成的Image的。
-
NetWorkImage實現了ImageProvider的load方法,是真正下載圖片的地方,建立了MultiFrameImageStreamCompleter物件,並且呼叫_loadAsync去下載圖片。當圖片下載完成後就呼叫UI的回撥方法,通知UI重新整理。
>>>>
最後
至此,對Image.network的原始碼分析到這裡也結束了,你也可以返回去看下Image的結構圖了。怎麼樣,分析完之後是不是對Flutter載入網路圖片的流程已經很瞭解了,也找到了Flutter快取的突破口,Flutter自身已經提供了記憶體快取(雖然不太完美),接下來你就可以新增你的硬碟快取或者定製你的圖片框架了。