Flutter完整開發實戰詳解(十、 深入圖片載入流程)

戀貓de小郭發表於2019-04-13

作為系列文章的第十篇,本篇主要深入瞭解 Flutter 中圖片載入的流程,剝析圖片流程中有意思的片段,結尾再實現 Flutter 實現本地圖片快取的支援。

前文:

在 Flutter 中,圖片的載入主要是通過 Image 控制元件實現的,而 Image 控制元件本身是一個 StatefulWidget ,通過前文我們可以快速想到, Image 肯定對應有它的 RenderObject 負責 layoutpaint ,那麼這個過程中,圖片是如何變成畫面顯示出來的?

一、圖片流程

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、最後 RawImageRenderImage 通過 paint 繪製 ImageInfo 中的 ui.Codec

注意,這的 ui.Codec 和後面的 ui.Image等,只是因為 Flutter 中在匯入物件時,為了和其他型別區分而加入的重新命名:import 'dart:ui' as ui show Codec;

是不是感覺有點暈了?relax!後面我們將逐步理解這個流程。

點選大圖檢視

在 Flutter 的圖片的載入流程中,主要有三個角色:

  • Image :用於顯示圖片的 Widget,最後通過內部的 RenderImage 繪製
  • ImageProvider:提供載入圖片的方式如 NetworkImageFileImageMemoryImageAssetImage 等,從而獲取 ImageStream ,用於監聽結果
  • ImageStream:圖片的載入物件,通過 ImageStreamCompleter 最後會返回一個 ImageInfo ,而 ImageInfo 內包含有 RenderImage 最後的繪製物件 ui.Image

從上面的大圖流程可知,網路圖片是通過 NetworkImage 這個 Provider 去提供載入的,各類 Provider 的實現其實大同小異,其中主要需要實現的方法主要如下圖所示:

Flutter完整開發實戰詳解(十、 深入圖片載入流程)

1、obtainKey

該方法主要用於標示當前 Provider 的存在,比如在 NetworkImage 中,這個方法返回的是 SynchronousFuture<NetworkImage>(this),也就是 NetworkImage 自己本身,並且得到的這個 key 在 ImageProvider 中,是用於作為記憶體快取的 key 值

NetworkImage 中主要是通過 runtimeTypeurlscale 這三個引數判斷兩個NetworkImage 是否相等,所以除了 url ,圖片的 scale 同樣會影響快取的物件哦。

2、load(T key)

load 方法顧名思義就是載入了,而該方法中所使用的 key ,毫無疑問就是上面 obtainKey 方法所提供的。

load 方法返回的是 ImageStreamCompleter 抽象物件,它主要是用於管理和通知 ImageStream 中得到的 dart:ui.Image ,比如在 NetworkImage 中的是子類 MultiFrameImageStreamCompleter , 它可以處理多幀的動畫,如果圖片只有一針,那麼將執行一次都結束。

3、resolve

ImageProvider 的關鍵在於 resolve 方法,從流程圖我們可知,該方法在 Image 的生命週期回撥方法 didChangeDependenciesdidUpdateWidgetreassemble 裡會被呼叫,從下方原始碼可以看出,上面我們所實現的 obtainKeyload 都會在這裡被呼叫

Flutter完整開發實戰詳解(十、 深入圖片載入流程)

這個有個有意思的物件,就是 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

Flutter完整開發實戰詳解(十、 深入圖片載入流程)

發現沒有,這裡和我們理解上的 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 編碼處理,將圖片轉化為引擎可繪製資料。

Flutter完整開發實戰詳解(十、 深入圖片載入流程)

而在 MultiFrameImageStreamCompleter 內部, ui.Codec 會被 ui.Image ,通過 ImageInfo 封裝起來,並逐步往回回撥到 _ImageState 中,然後通過 setState 將資料傳遞到 RenderImage 內部去繪製。

Flutter完整開發實戰詳解(十、 深入圖片載入流程)

怎麼樣,現在再回過頭去看開頭的流程圖,有沒有一切明瞭的感覺?

二、本地圖片快取

通過上方流程的瞭解,我們知道 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圖效果哦。

自此,第十篇終於結束了!(///▽///)

資源推薦

完整開源專案推薦:
文章

《Flutter完整開發實戰詳解(一、Dart語言和Flutter基礎)》

《Flutter完整開發實戰詳解(二、 快速開發實戰篇)》

《Flutter完整開發實戰詳解(三、 打包與填坑篇)》

《Flutter完整開發實戰詳解(四、Redux、主題、國際化)》

《Flutter完整開發實戰詳解(五、 深入探索)》

《Flutter完整開發實戰詳解(六、 深入Widget原理)》

《Flutter完整開發實戰詳解(七、 深入佈局原理)》

《Flutter完整開發實戰詳解(八、 實用技巧與填坑)》

《Flutter完整開發實戰詳解(九、 深入繪製原理)》

《Flutter完整開發實戰詳解(十、 深入圖片載入流程)》

《跨平臺專案開源專案推薦》

《移動端跨平臺開發的深度解析》

我們還會再見嗎?

相關文章