在本書前面章節(jié)已經(jīng)介紹過Image
組件,并提到 Flutter 框架對(duì)加載過的圖片是有緩存的(內(nèi)存),默認(rèn)最大緩存數(shù)量是1000,最大緩存空間為100M。本節(jié)便詳細(xì)介紹 Image 的原理及圖片緩存機(jī)制,下面我們先看看ImageProvider
類。
我們已經(jīng)知道Image
組件的image
參數(shù)是一個(gè)必選參數(shù),它是ImageProvider
類型。下面我們便詳細(xì)介紹一下ImageProvider
,ImageProvider
是一個(gè)抽象類,定義了圖片數(shù)據(jù)獲取和加載的相關(guān)接口。它的主要職責(zé)有兩個(gè):
我們看看ImageProvider
抽象類的詳細(xì)定義:
abstract class ImageProvider<T> {
ImageStream resolve(ImageConfiguration configuration) {
// 實(shí)現(xiàn)代碼省略
}
Future<bool> evict({ ImageCache cache,
ImageConfiguration configuration = ImageConfiguration.empty }) async {
// 實(shí)現(xiàn)代碼省略
}
Future<T> obtainKey(ImageConfiguration configuration);
@protected
ImageStreamCompleter load(T key); // 需子類實(shí)現(xiàn)
}
load(T key)
方法
加載圖片數(shù)據(jù)源的接口,不同的數(shù)據(jù)源的加載方法不同,每個(gè)ImageProvider
的子類必須實(shí)現(xiàn)它。比如NetworkImage
類和AssetImage
類,它們都是ImageProvider
的子類,但它們需要從不同的數(shù)據(jù)源來加載圖片數(shù)據(jù):NetworkImage
是從網(wǎng)絡(luò)來加載圖片數(shù)據(jù),而AssetImage
則是從最終的應(yīng)用包里來加載(加載打到應(yīng)用安裝包里的資源圖片)。 我們以NetworkImage
為例,看看其 load 方法的實(shí)現(xiàn):
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), //調(diào)用
chunkEvents: chunkEvents.stream,
scale: key.scale,
... //省略無關(guān)代碼
);
}
我們看到,load
方法的返回值類型是ImageStreamCompleter
,它是一個(gè)抽象類,定義了管理圖片加載過程的一些接口,Image
Widget 中正是通過它來監(jiān)聽圖片加載狀態(tài)的(我們將在下面介紹Image
原理時(shí)詳細(xì)介紹)。
MultiFrameImageStreamCompleter
是 ImageStreamCompleter
的一個(gè)子類,是 flutter sdk 預(yù)置的類,通過該類,我們以方便、輕松地創(chuàng)建出一個(gè)ImageStreamCompleter
實(shí)例來做為load
方法的返回值。
我們可以看到,MultiFrameImageStreamCompleter
需要一個(gè)codec
參數(shù),該參數(shù)類型為Future<ui.Codec>
。Codec
是處理圖片編解碼的類的一個(gè) handler,實(shí)際上,它只是一個(gè) flutter engine API 的包裝類,也就是說圖片的編解碼邏輯不是在 Dart 代碼部分實(shí)現(xiàn),而是在 flutter engine中實(shí)現(xiàn)的。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(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
// 對(duì)圖片數(shù)據(jù)進(jìn)行解碼
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
}
}
可以看到_loadAsync
方法主要做了兩件事:
下載邏輯比較簡單:通過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 的包裝,實(shí)際上會(huì)調(diào)用 Flutter engine 的instantiateImageCodec
方法,源碼如下:
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
native 'instantiateImageCodec';
obtainKey(ImageConfiguration)
方法
該接口主要是為了配合實(shí)現(xiàn)圖片緩存,ImageProvider
從數(shù)據(jù)源加載完數(shù)據(jù)后,會(huì)在全局的ImageCache
中緩存圖片數(shù)據(jù),而圖片數(shù)據(jù)緩存是一個(gè) Map,而 Map 的 key 便是調(diào)用此方法的返回值,不同的 key 代表不同的圖片數(shù)據(jù)緩存。
resolve(ImageConfiguration)
方法
resolve
方法是ImageProvider
的暴露的給Image
的主入口方法,它接受一個(gè)ImageConfiguration
參數(shù),返回ImageStream
,即圖片數(shù)據(jù)流。我們重點(diǎn)看一下resolve
執(zhí)行流程:
ImageStream resolve(ImageConfiguration configuration) {
... //省略無關(guān)代碼
final ImageStream stream = ImageStream();
T obtainedKey; //
//定義錯(cuò)誤處理函數(shù)
Future<void> handleError(dynamic exception, StackTrace stack) async {
... //省略無關(guān)代碼
stream.setCompleter(imageCompleter);
imageCompleter.setError(...);
}
// 創(chuàng)建一個(gè)新Zone,主要是為了當(dāng)發(fā)生錯(cuò)誤時(shí)不會(huì)干擾MainZone
final Zone dangerZone = Zone.current.fork(...);
dangerZone.runGuarded(() {
Future<T> key;
// 先驗(yàn)證是否已經(jīng)有緩存
try {
// 生成緩存key,后面會(huì)根據(jù)此key來檢測(cè)是否有緩存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
// 緩存的處理邏輯在這里,記為A,下面詳細(xì)介紹
final ImageStreamCompleter completer = PaintingBinding.instance
.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
});
return stream;
}
ImageConfiguration
包含圖片和設(shè)備的相關(guān)信息,如圖片的大小、所在的AssetBundle
(只有打到安裝包的圖片存在)以及當(dāng)前的設(shè)備平臺(tái)、devicePixelRatio(設(shè)備像素比等)。Flutter SDK 提供了一個(gè)便捷函數(shù)createLocalImageConfiguration
來創(chuàng)建ImageConfiguration
對(duì)象:
ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {
return ImageConfiguration(
bundle: DefaultAssetBundle.of(context),
devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0,
locale: Localizations.localeOf(context, nullOk: true),
textDirection: Directionality.of(context),
size: size,
platform: defaultTargetPlatform,
);
}
我們可以發(fā)現(xiàn)這些信息基本都是通過Context
來獲取。
上面代碼 A 處就是處理緩存的主要代碼,這里的PaintingBinding.instance.imageCache
是 ImageCache
的一個(gè)實(shí)例,它是PaintingBinding
的一個(gè)屬性,而 Flutter 框架中的PaintingBinding.instance
是一個(gè)單例,imageCache
事實(shí)上也是一個(gè)單例,也就是說圖片緩存是全局的,統(tǒng)一由PaintingBinding.instance.imageCache
來管理。
下面我們看看ImageCache
類定義:
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
// 正在加載中的圖片隊(duì)列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 緩存隊(duì)列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 緩存數(shù)量上限(1000)
int _maximumSize = _kDefaultSize;
// 緩存容量上限 (100 MB)
int _maximumSizeBytes = _kDefaultSizeBytes;
// 緩存上限設(shè)置的setter
set maximumSize(int value) {...}
set maximumSizeBytes(int value) {...}
... // 省略部分定義
// 清除所有緩存
void clear() {
// ...省略具體實(shí)現(xiàn)代碼
}
// 清除指定key對(duì)應(yīng)的圖片緩存
bool evict(Object key) {
// ...省略具體實(shí)現(xiàn)代碼
}
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
assert(key != null);
assert(loader != null);
ImageStreamCompleter result = _pendingImages[key]?.completer;
// 圖片還未加載成功,直接返回
if (result != null)
return result;
// 有緩存,繼續(xù)往下走
// 先移除緩存,后再添加,可以讓最新使用過的緩存在_map中的位置更近一些,清理時(shí)會(huì)LRU來清除
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
try {
result = loader();
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
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ù)量超過最大值或緩存的大小超過最大緩存容量,會(huì)調(diào)用此方法清理到緩存上限以內(nèi)
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);
}
... //省略無關(guān)代碼
}
}
有緩存則使用緩存,沒有緩存則調(diào)用 load 方法加載圖片,加載成功后:
ImageStream
。load(T key)
方法從數(shù)據(jù)源加載圖片數(shù)據(jù),加載成功后先緩存,然后返回 ImageStream。
另外,我們可以看到ImageCache
類中有設(shè)置緩存上限的setter,所以,如果我們可以自定義緩存上限:
PaintingBinding.instance.imageCache.maximumSize=2000; //最多2000張
PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; //最大200M
現(xiàn)在我們看一下緩存的 key,因?yàn)?Map 中相同 key 的值會(huì)被覆蓋,也就是說 key 是圖片緩存的一個(gè)唯一標(biāo)識(shí),只要是不同 key,那么圖片數(shù)據(jù)就會(huì)分別緩存(即使事實(shí)上是同一張圖片)。那么圖片的唯一標(biāo)識(shí)是什么呢?跟蹤源碼,很容易發(fā)現(xiàn) key 正是ImageProvider.obtainKey()
方法的返回值,而此方法需要ImageProvider
子類去重寫,這也就意味著不同的ImageProvider
對(duì)key的定義邏輯會(huì)不同。其實(shí)也很好理解,比如對(duì)于NetworkImage
,將圖片的 url 作為 key 會(huì)很合適,而對(duì)于AssetImage
,則應(yīng)該將“包名+路徑”作為唯一的 key。下面我們以NetworkImage
為例,看一下它的obtainKey()
實(shí)現(xiàn):
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
代碼很簡單,創(chuàng)建了一個(gè)同步的 future,然后直接將自身做為 key 返回。因?yàn)?Map 中在判斷 key(此時(shí)是NetworkImage
對(duì)象)是否相等時(shí)會(huì)使用“==”運(yùn)算符,那么定義 key 的邏輯就是NetworkImage
的“==”運(yùn)算符:
@override
bool operator ==(dynamic other) {
... //省略無關(guān)代碼
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
很清晰,對(duì)于網(wǎng)絡(luò)圖片來說,會(huì)將其“url+縮放比例”作為緩存的 key。也就是說如果兩張圖片的 url 或 scale 只要有一個(gè)不同,便會(huì)重新下載并分別緩存。
另外,我們需要注意的是,圖片緩存是在內(nèi)存中,并沒有進(jìn)行本地文件持久化存儲(chǔ),這也是為什么網(wǎng)絡(luò)圖片在應(yīng)用重啟后需要重新聯(lián)網(wǎng)下載的原因。
同時(shí)也意味著在應(yīng)用生命周期內(nèi),如果緩存沒有超過上限,相同的圖片只會(huì)被下載一次。
上面主要結(jié)合源碼,探索了ImageProvider
的主要功能和原理,如果要用一句話來總結(jié)ImageProvider
功能,那么應(yīng)該是:加載圖片數(shù)據(jù)并進(jìn)行緩存、解碼。在此再次提醒讀者,F(xiàn)lutter 的源碼是非常好的第一手資料,建議讀者多多探索,另外,在閱讀源碼學(xué)習(xí)的同時(shí)一定要有總結(jié),這樣才不至于在源碼中迷失。
前面章節(jié)中我們介紹過Image
的基礎(chǔ)用法,現(xiàn)在我們更深入一些,研究一下Image
是如何和ImageProvider
配合來獲取最終解碼后的數(shù)據(jù),然后又如何將圖片繪制到屏幕上的。
本節(jié)換一個(gè)思路,我們先不去直接看Image
的源碼,而根據(jù)已經(jīng)掌握的知識(shí)來實(shí)現(xiàn)一個(gè)簡版的“Image
組件” MyImage
,代碼大致如下:
class MyImage extends StatefulWidget {
const MyImage({
Key key,
@required this.imageProvider,
})
: assert(imageProvider != null),
super(key: key);
final ImageProvider imageProvider;
@override
_MyImageState createState() => _MyImageState();
}
class _MyImageState extends State<MyImage> {
ImageStream _imageStream;
ImageInfo _imageInfo;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 依賴改變時(shí),圖片的配置信息可能會(huì)發(fā)生改變
_getImage();
}
@override
void didUpdateWidget(MyImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageProvider != oldWidget.imageProvider)
_getImage();
}
void _getImage() {
final ImageStream oldImageStream = _imageStream;
// 調(diào)用imageProvider.resolve方法,獲得ImageStream。
_imageStream =
widget.imageProvider.resolve(createLocalImageConfiguration(context));
//判斷新舊ImageStream是否相同,如果不同,則需要調(diào)整流的監(jiān)聽器
if (_imageStream.key != oldImageStream?.key) {
final ImageStreamListener listener = ImageStreamListener(_updateImage);
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
// Trigger a build whenever the image changes.
_imageInfo = imageInfo;
});
}
@override
void dispose() {
_imageStream.removeListener(ImageStreamListener(_updateImage));
super.dispose();
}
@override
Widget build(BuildContext context) {
return RawImage(
image: _imageInfo?.image, // this is a dart:ui Image object
scale: _imageInfo?.scale ?? 1.0,
);
}
}
上面代碼流程如下:
imageProvider.resolve
方法可以得到一個(gè)ImageStream
(圖片數(shù)據(jù)流),然后監(jiān)聽ImageStream
的變化。當(dāng)圖片數(shù)據(jù)源發(fā)生變化時(shí),ImageStream
會(huì)觸發(fā)相應(yīng)的事件,而本例中我們只設(shè)置了圖片成功的監(jiān)聽器_updateImage
,而_updateImage
中只更新了_imageInfo
。值得注意的是,如果是靜態(tài)圖,ImageStream
只會(huì)觸發(fā)一次時(shí)間,如果是動(dòng)態(tài)圖,則會(huì)觸發(fā)多次事件,每一次都會(huì)有一個(gè)解碼后的圖片幀。_imageInfo
更新后會(huì) rebuild,此時(shí)會(huì)創(chuàng)建一個(gè)RawImage
Widget。RawImage
最終會(huì)通過RenderImage
來將圖片繪制在屏幕上。如果繼續(xù)跟進(jìn)RenderImage
類,我們會(huì)發(fā)現(xiàn)RenderImage
的paint
方法中調(diào)用了paintImage
方法,而paintImage
方法中通過Canvas
的drawImageRect(…)
、drawImageNine(...)
等方法來完成最終的繪制。RawImage
來完成。
下面測(cè)試一下MyImage
:
class ImageInternalTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
MyImage(
imageProvider: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
),
)
],
);
}
}
運(yùn)行效果如圖14-4所示:
成功了! 現(xiàn)在,想必Image
Widget 的源碼已經(jīng)沒必要在花費(fèi)篇章去介紹了,讀者有興趣可以自行去閱讀。
本節(jié)主要介紹了 Flutter 圖片的加載、緩存和繪制流程。其中ImageProvider
主要負(fù)責(zé)圖片數(shù)據(jù)的加載和緩存,而繪制部分邏輯主要是由RawImage
來完成。 而Image
正是連接起ImageProvider
和RawImage
的橋梁。
更多建議: