作為系列文章的第十篇,本篇主要深入瞭解 Flutter 中圖片載入的流程,剝析圖片流程中有意思的片段,結尾再實現 Flutter 實現本地圖片快取的支援。
文章彙總地址:
在 Flutter 中,圖片的載入主要是通過 Image
控制元件實現的,而 Image
控制元件本身是一個 StatefulWidget ,通過前文我們可以快速想到, Image
肯定對應有它的 RenderObject 負責 layout 和 paint ,那麼這個過程中,圖片是如何變成畫面顯示出來的?
一、圖片流程
Flutter 的圖片載入流程其實“並不複雜”,具體可點選下方大圖檢視,以網路圖片載入為例子,先簡單總結,其中主要流程是:
- 1、首先
Image
通過ImageProvider
得到ImageStream
物件 - 2、然後
_ImageState
利用ImageStream
新增監聽,等待圖片資料 - 3、接著
ImageProvider
通過load
方法去載入並返回ImageStreamCompleter
物件 - 4、然後
ImageStream
會關聯ImageStreamCompleter
- 5、之後
ImageStreamCompleter
會通過 http 下載圖片,再經過PaintingBinding
編碼轉化後,得到ui.Codec
可繪製物件,並封裝成ImageInfo
返回 - 6、接著
ImageInfo
回撥到ImageStream
的監聽,設定給_ImageState
build 的RawImage
物件。 - 7、最後
RawImage
的RenderImage
通過 paint 繪製ImageInfo
中的ui.Codec
注意,這的
ui.Codec
和後面的ui.Image
等,只是因為 Flutter 中在匯入物件時,為了和其他型別區分而加入的重新命名:import 'dart:ui' as ui show Codec;
是不是感覺有點暈了?relax!後面我們將逐步理解這個流程。
在 Flutter 的圖片的載入流程中,主要有三個角色:
Image
:用於顯示圖片的 Widget,最後通過內部的RenderImage
繪製。ImageProvider
:提供載入圖片的方式如NetworkImage
、FileImage
、MemoryImage
、AssetImage
等,從而獲取ImageStream
,用於監聽結果。ImageStream
:圖片的載入物件,通過ImageStreamCompleter
最後會返回一個ImageInfo
,而ImageInfo
內包含有RenderImage
最後的繪製物件ui.Image
。
從上面的大圖流程可知,網路圖片是通過 NetworkImage
這個 Provider 去提供載入的,各類 Provider 的實現其實大同小異,其中主要需要實現的方法主要如下圖所示:
1、obtainKey
該方法主要用於標示當前 Provider
的存在,比如在 NetworkImage
中,這個方法返回的是 SynchronousFuture<NetworkImage>(this)
,也就是 NetworkImage
自己本身,並且得到的這個 key 在 ImageProvider
中,是用於作為記憶體快取的 key 值。
在 NetworkImage
中主要是通過 runtimeType
、url
、scale
這三個引數判斷兩個NetworkImage
是否相等,所以除了 url
,圖片的 scale
同樣會影響快取的物件哦。
2、load(T key)
load
方法顧名思義就是載入了,而該方法中所使用的 key ,毫無疑問就是上面 obtainKey
方法所提供的。
load
方法返回的是 ImageStreamCompleter
抽象物件,它主要是用於管理和通知 ImageStream
中得到的 dart:ui.Image
,比如在 NetworkImage
中的是子類 MultiFrameImageStreamCompleter
, 它可以處理多幀的動畫,如果圖片只有一針,那麼將執行一次都結束。
3、resolve
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
。
3、ImageStreamCompleter
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);
}
複製程式碼
三、其他補充
1、快取數量
在閒魚關於 Flutter 線上應用的記憶體分析文章中,有過對圖片載入對記憶體問題的詳細分析,其中就有一個是 ImageCache
的問題。
上面的流程我們知道, ImageCache
快取的是一個非同步物件,快取非同步載入物件的一個問題是,在圖片載入解碼完成之前,你無法知道到底將要消耗多少記憶體,並且大量的圖片載入,會導致的解碼任務需要產生大量的IO。
而在 Flutter 中, ImageCache
預設的快取大小是
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100
複製程式碼
所以簡單粗暴的做法是: PaintingBinding.instance.imageCache.maximumSize = 100;
同時在頁面不可見時暫停圖片的載入等。
2、.9圖
在 Image中,可以通過 centerSlice
配置引數設定.9圖效果哦。
自此,第十篇終於結束了!(///▽///)
資源推薦
- Github : github.com/CarGuo/
- 開源 Flutter 完整專案:github.com/CarGuo/GSYG…
- 開源 Flutter 多案例學習型專案: github.com/CarGuo/GSYF…
- 開源 Fluttre 實戰電子書專案:github.com/CarGuo/GSYF…