做爲系列文章的第十篇,本篇主要深刻了解 Flutter 中圖片加載的流程,剝析圖片流程中有意思的片斷,結尾再實現 Flutter 實現本地圖片緩存的支持。git
前文:github
在 Flutter 中,圖片的加載主要是經過 Image
控件實現的,而 Image
控件自己是一個 StatefulWidget ,經過前文咱們能夠快速想到, Image
確定對應有它的 RenderObject 負責 layout 和 paint ,那麼這個過程當中,圖片是如何變成畫面顯示出來的?緩存
Flutter 的圖片加載流程其實「並不複雜」,具體可點擊下方大圖查看,以網絡圖片加載爲例子,先簡單總結,其中主要流程是:bash
Image
經過 ImageProvider
獲得 ImageStream
對象_ImageState
利用 ImageStream
添加監聽,等待圖片數據ImageProvider
經過 load
方法去加載並返回 ImageStreamCompleter
對象ImageStream
會關聯 ImageStreamCompleter
ImageStreamCompleter
會經過 http 下載圖片,再通過 PaintingBinding
編碼轉化後,獲得 ui.Codec
可繪製對象,並封裝成 ImageInfo
返回ImageInfo
回調到 ImageStream
的監聽,設置給 _ImageState
build 的 RawImage
對象。RawImage
的 RenderImage
經過 paint 繪製 ImageInfo
中的 ui.Codec
注意,這的
ui.Codec
和後面的ui.Image
等,只是由於 Flutter 中在導入對象時,爲了和其餘類型區分而加入的重命名:import 'dart:ui' as ui show Codec;
網絡
是否是感受有點暈了?relax!後面咱們將逐步理解這個流程。異步
在 Flutter 的圖片的加載流程中,主要有三個角色:async
Image
:用於顯示圖片的 Widget,最後經過內部的 RenderImage
繪製。ImageProvider
:提供加載圖片的方式如 NetworkImage
、FileImage
、MemoryImage
、AssetImage
等,從而獲取 ImageStream
,用於監聽結果。ImageStream
:圖片的加載對象,經過 ImageStreamCompleter
最後會返回一個 ImageInfo
,而 ImageInfo
內包含有 RenderImage
最後的繪製對象 ui.Image
。從上面的大圖流程可知,網絡圖片是經過 NetworkImage
這個 Provider 去提供加載的,各種 Provider 的實現其實大同小異,其中主要須要實現的方法主要以下圖所示:ide
該方法主要用於標示當前 Provider
的存在,好比在 NetworkImage
中,這個方法返回的是 SynchronousFuture<NetworkImage>(this)
,也就是 NetworkImage
本身自己,而且獲得的這個 key 在 ImageProvider
中,是用於做爲內存緩存的 key 值。佈局
在 NetworkImage
中主要是經過 runtimeType
、url
、scale
這三個參數判斷兩個NetworkImage
是否相等,因此除了 url
,圖片的 scale
一樣會影響緩存的對象哦。post
load
方法顧名思義就是加載了,而該方法中所使用的 key ,毫無疑問就是上面 obtainKey
方法所提供的。
load
方法返回的是 ImageStreamCompleter
抽象對象,它主要是用於管理和通知 ImageStream
中獲得的 dart:ui.Image
,好比在 NetworkImage
中的是子類 MultiFrameImageStreamCompleter
, 它能夠處理多幀的動畫,若是圖片只有一針,那麼將執行一次都結束。
ImageProvider
的關鍵在於 resolve
方法,從流程圖咱們可知,該方法在 Image
的生命週期回調方法 didChangeDependencies
、 didUpdateWidget
、 reassemble
裏會被調用,從下方源碼能夠看出,上面咱們所實現的 obtainKey
和 load
都會在這裏被調用
這個有個有意思的對象,就是
Zone
!由於在 Flutter 中,同步異常能夠經過try-catch捕獲,而異步異常如
Future
,是沒法被當前的 try-catch 直接捕獲的。因此在 Dart中
Zone
的概念,你能夠給執行對象指定一個Zone
,相似提供一個沙箱環境,而在這個沙箱內,你就能夠所有能夠捕獲、攔截或修改一些代碼行爲,好比全部未被處理的異常。
resolve
方法內主要是用到了 PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)
, PaintingBinding
是一個膠水類,主要是經過 Mixins 粘在 WidgetsFlutterBinding
上使用,而之前的篇章咱們說過, WidgetsFlutterBinding
就是咱們的啓動方法 runApp
的執行者。
因此圖片緩存是在PaintingBinding.instance.imageCache內單例維護的。
以下圖所示,putIfAbsent
方法內部,主要是經過 key
判斷內存中是否已有緩存、或者正在緩存的對象,若是是就返回該 ImageStreamCompleter
,否則就調用 loader
去加載並返回。
值得注意的是,此時的的 cache 是有兩個狀態的,由於返回的 ImageStreamCompleter
並不表明着圖片就加載完成,因此若是是首次加載,會先有 _PendingImage
用於標示該key的圖片處於加載中的狀態 ,而且添加一個 listener
, 用於圖片加載完成後,替換爲緩存 _CacheImage
。
發現沒有,這裏和咱們理解上的 Cache 概念稍微有點不一樣,之前咱們緩存的通常是 key - bitmap 對象,也就是實際繪製數據,而在 Flutter 中,緩存的僅是ImageStreamCompleter
對象,而不是實際繪製對象 dart:ui.Image
。
ImageStreamCompleter
是一個抽象對象,它主要是用於管理和通知 ImageStream
,處理圖片數據後獲得的包含有 dart:ui.Image
的對象 ImageInfo 。
接下來咱們看 NetworkImage
中的 ImageStreamCompleter
實現類 MultiFrameImageStreamCompleter
。以下圖代碼所示,MultiFrameImageStreamCompleter
主要經過 codec
參數得到渲染數據,而這個數據來源經過 _loadAsync
方法獲得,該方法主要經過 http 下載圖片後,對圖片數據經過 PaintingBinding
進行 ImageCodec
編碼處理,將圖片轉化爲引擎可繪製數據。
而在 MultiFrameImageStreamCompleter
內部, ui.Codec
會被 ui.Image
,經過 ImageInfo
封裝起來,並逐步往回回調到 _ImageState
中,而後經過 setState
將數據傳遞到 RenderImage
內部去繪製。
怎麼樣,如今再回過頭去看開頭的流程圖,有沒有一切明瞭的感受?
經過上方流程的瞭解,咱們知道 Flutter 實現了圖片的內存緩存,可是並無實現圖片的本地緩存,因此咱們入手的點,應該從 ImageProvider
開始。
經過上面對 NetworkImage
的分析,咱們知道圖片是在 _loadAsync
方法經過 http 下載的,因此最簡單的就是,咱們從 NetworkImage
cv 一份代碼,修改 _loadAsync
支持 http 下載前讀取本地緩存,下載後經過將數據保存在本地。
結合 flutter_cache_manager
插件,以下方代碼所示,就能夠快速簡單實現圖片的本地緩存:
Future<ui.Codec> _loadAsync(NetworkImage key) async {
assert(key == this);
/// add this start
/// flutter_cache_manager DefaultCacheManager
final fileInfo = await DefaultCacheManager().getFileFromCache(key.url);
if(fileInfo != null && fileInfo.file != null) {
final Uint8List cacheBytes = await fileInfo.file.readAsBytes();
if (cacheBytes != null) {
return PaintingBinding.instance.instantiateImageCodec(cacheBytes);
}
}
/// add this end
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('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');
final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
/// add this start
await DefaultCacheManager().putFile(key.url, bytes);
/// add this edn
return PaintingBinding.instance.instantiateImageCodec(bytes);
}
複製代碼
在閒魚關於 Flutter 線上應用的內存分析文章中,有過對圖片加載對內存問題的詳細分析,其中就有一個是 ImageCache
的問題。
上面的流程咱們知道, ImageCache
緩存的是一個異步對象,緩存異步加載對象的一個問題是,在圖片加載解碼完成以前,你沒法知道到底將要消耗多少內存,而且大量的圖片加載,會致使的解碼任務須要產生大量的IO。
而在 Flutter 中, ImageCache
默認的緩存大小是
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100
複製代碼
因此簡單粗暴的作法是: PaintingBinding.instance.imageCache.maximumSize = 100;
同時在頁面不可見時暫停圖片的加載等。
在 Image中,能夠經過 centerSlice
配置參數設置.9圖效果哦。
自此,第十篇終於結束了!(///▽///)
《Flutter完整開發實戰詳解(1、Dart語言和Flutter基礎)》
《Flutter完整開發實戰詳解(4、Redux、主題、國際化)》
《Flutter完整開發實戰詳解(6、 深刻Widget原理)》
《Flutter完整開發實戰詳解(10、 深刻圖片加載流程)》