Flutter完整開發實戰詳解(10、 深刻圖片加載流程)

做爲系列文章的第十篇,本篇主要深刻了解 Flutter 中圖片加載的流程,剝析圖片流程中有意思的片斷,結尾再實現 Flutter 實現本地圖片緩存的支持。git

前文:github

在 Flutter 中,圖片的加載主要是經過 Image 控件實現的,而 Image 控件自己是一個 StatefulWidget ,經過前文咱們能夠快速想到, Image 確定對應有它的 RenderObject 負責 layoutpaint ,那麼這個過程當中,圖片是如何變成畫面顯示出來的?緩存

1、圖片流程

Flutter 的圖片加載流程其實「並不複雜」,具體可點擊下方大圖查看,以網絡圖片加載爲例子,先簡單總結,其中主要流程是:bash

  • 一、首先 Image 經過 ImageProvider 獲得 ImageStream 對象
  • 二、而後 _ImageState 利用 ImageStream 添加監聽,等待圖片數據
  • 三、接着 ImageProvider 經過 load 方法去加載並返回 ImageStreamCompleter 對象
  • 四、而後 ImageStream 會關聯 ImageStreamCompleter
  • 五、以後 ImageStreamCompleter 會經過 http 下載圖片,再通過 PaintingBinding 編碼轉化後,獲得 ui.Codec 可繪製對象,並封裝成 ImageInfo 返回
  • 六、接着 ImageInfo 回調到 ImageStream 的監聽,設置給 _ImageState build 的 RawImage 對象。
  • 七、最後 RawImageRenderImage 經過 paint 繪製 ImageInfo 中的 ui.Codec

注意,這的 ui.Codec 和後面的 ui.Image等,只是由於 Flutter 中在導入對象時,爲了和其餘類型區分而加入的重命名:import 'dart:ui' as ui show Codec;網絡

是否是感受有點暈了?relax!後面咱們將逐步理解這個流程。異步

點擊大圖查看

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

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

從上面的大圖流程可知,網絡圖片是經過 NetworkImage 這個 Provider 去提供加載的,各種 Provider 的實現其實大同小異,其中主要須要實現的方法主要以下圖所示:ide

一、obtainKey

該方法主要用於標示當前 Provider 的存在,好比在 NetworkImage 中,這個方法返回的是 SynchronousFuture<NetworkImage>(this),也就是 NetworkImage 本身自己,而且獲得的這個 key 在 ImageProvider 中,是用於做爲內存緩存的 key 值佈局

NetworkImage 中主要是經過 runtimeTypeurlscale 這三個參數判斷兩個NetworkImage 是否相等,因此除了 url ,圖片的 scale 一樣會影響緩存的對象哦。post

二、load(T key)

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

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

三、resolve

ImageProvider 的關鍵在於 resolve 方法,從流程圖咱們可知,該方法在 Image 的生命週期回調方法 didChangeDependenciesdidUpdateWidgetreassemble 裏會被調用,從下方源碼能夠看出,上面咱們所實現的 obtainKeyload 都會在這裏被調用

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

ImageStreamCompleter 是一個抽象對象,它主要是用於管理和通知 ImageStream ,處理圖片數據後獲得的包含有 dart:ui.Image 的對象 ImageInfo

接下來咱們看 NetworkImage 中的 ImageStreamCompleter 實現類 MultiFrameImageStreamCompleter 。以下圖代碼所示,MultiFrameImageStreamCompleter 主要經過 codec 參數得到渲染數據,而這個數據來源經過 _loadAsync 方法獲得,該方法主要經過 http 下載圖片後,對圖片數據經過 PaintingBinding 進行 ImageCodec 編碼處理,將圖片轉化爲引擎可繪製數據。

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

怎麼樣,如今再回過頭去看開頭的流程圖,有沒有一切明瞭的感受?

2、本地圖片緩存

經過上方流程的瞭解,咱們知道 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);
  }
複製代碼

3、其餘補充

一、緩存數量

在閒魚關於 Flutter 線上應用的內存分析文章中,有過對圖片加載對內存問題的詳細分析,其中就有一個是 ImageCache 的問題。

上面的流程咱們知道, ImageCache 緩存的是一個異步對象,緩存異步加載對象的一個問題是,在圖片加載解碼完成以前,你沒法知道到底將要消耗多少內存,而且大量的圖片加載,會致使的解碼任務須要產生大量的IO。

而在 Flutter 中, ImageCache 默認的緩存大小是

const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 
複製代碼

因此簡單粗暴的作法是: PaintingBinding.instance.imageCache.maximumSize = 100; 同時在頁面不可見時暫停圖片的加載等。

二、.9圖

在 Image中,能夠經過 centerSlice 配置參數設置.9圖效果哦。

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

資源推薦

完整開源項目推薦:
文章

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

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

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

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

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

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

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

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

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

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

《Flutter完整開發實戰詳解(11、全面深刻理解Stream)》

《跨平臺項目開源項目推薦》

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

《React Native 的將來與React Hooks》

咱們還會再見嗎?
相關文章
相關標籤/搜索