Flutter 1.17 對列表圖片的優(yōu)化解析

相信 Flutter 的開發(fā)者應該遇到過,對于大量數(shù)據(jù)的列表進行圖片加載時,在 iOS 上很容易出現(xiàn) OOM的問題,這是因為 Flutter 特殊的圖片加載流程造成。

在 Android 上 Flutter Image 主要占用的內存不是 JVM 的內存,而是 Graphics 相關的內存,這樣的內存調用可以最大程度利用 Native 內存。

一、默認流程

Flutter 默認在進行圖片加載時,會先通過對應的 ImageProvider 去加載圖片數(shù)據(jù),然后通過 PaintingBinding 對數(shù)據(jù)進行編碼,之后返回包含編碼后圖片數(shù)據(jù)和信息的 ImageInfo 去實現(xiàn)繪制。

詳細圖片加載流程可見:《十、 深入圖片加載流程)》

本身這個邏輯并沒有什么問題,問題就在于 Flutter 中對于圖片在內存中的 Cache 對象是一個 ImageStream 對象。

Flutter 中 ImageCache 緩存的是一個異步對象,緩存異步加載對象的一個問題是:在圖片加載解碼完成之前,你無法知道到底將要消耗多少內存,并且大量的圖片加載,會導致的解碼任務需要產(chǎn)生大量的IO。

所以一開始最粗暴的情況是:通過 PaintingBinding.instance 去設置 maximumSizemaximumSizeBytes,但是這種簡單粗爆的處理方法并不能解決長列表圖片加載的溢出問題,因為在長列表中,快速滑動的情況下可能會在一瞬間“并發(fā)”出大量圖片加載需求。

所以在 1.17 版本上,官方針對這種情況提供了場景化的處理方式: ScrollAwareImageProvider。

二、ScrollAwareImageProvider

1.17 中可以看到,在 Image 控件中原本 _resolveImage 方法所使用的 imageProviderScrollAwareImageProvider 所代理,并且多了一個叫 DisposableBuildContext<State<Image>> 的 context 參數(shù)。那 ScrollAwareImageProvider 的作用是什么呢?

  void _resolveImage() {
    final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
      context: _scrollAwareContext,
      imageProvider: widget.image,
    );
    final ImageStream newStream =
      provider.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
      ));
    assert(newStream != null);
    _updateSourceStream(newStream);
  }

其實 ScrollAwareImageProvider 對象最主要的使用就是在 resolveStreamForKey 方法中,通過 Scrollable.recommendDeferredLoadingForContext 方法去判斷當前是不是需要推遲當前幀畫面的加載,換言之就是:是否處于快速滑動的過程。

Scrollable.recommendDeferredLoadingForContext 作為一個 static 方法,如何判斷當前是不是處于列表的快速滑動呢?

這就需要通過當前 contextgetElementForInheritedWidgetOfExactType 方法去獲取 Scrollable 內的 _ScrollableScope 。

_ScrollableScopeScrollable 內的一個 InheritedWidget ,而 Flutter 中的可滑動視圖內必然會有 Scrollable ,所以只要 Image 是在列表內,就可以通過 context.getElementForInheritedWidgetOfExactType<_ScrollableScope>() 去獲取到 _ScrollableScope

獲取到 _ScrollableScope 就可以獲取到它內部的 ScrollPosition , 進而它的 ScrollPhysics 對應的 recommendDeferredLoading 方法,判斷列表是否處于快速滑動狀態(tài)。所以判斷是否快速滑動的邏輯其實是在 ScrollPhysics。


  bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
    assert(velocity != null);
    assert(metrics != null);
    assert(context != null);
    if (parent == null) {
      final double maxPhysicalPixels = WidgetsBinding.instance.window.physicalSize.longestSide;
      return velocity.abs() > maxPhysicalPixels;
    }
    return parent.recommendDeferredLoading(velocity, metrics, context);
  }
  

關于 ScrollPhysics 的解釋可以看 《十八、 神奇的ScrollPhysics與Simulation》

然后回到 resolveStreamForKey 方法,可以看到當 Scrollable.recommendDeferredLoadingForContext 返回 true 時就等待,等待就是會通過 SchedulerBinding 在下一幀繪制時再次調用 resolveStreamForKey, 遞歸再走一遍 resolveStreamForKey 的邏輯,如果判斷此時不再是快速滑動,就走正常的圖片加載邏輯。

@override
  void resolveStreamForKey(
    ImageConfiguration configuration,
    ImageStream stream,
    T key,
    ImageErrorListener handleError,
  ) {
    if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
      imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
      return;
    }
    if (context.context == null) {
      return;
    }
    if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
        SchedulerBinding.instance.scheduleFrameCallback((_) {
          scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
        });
        return;
    }
    imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
  }

如上代碼所示,可以看到在 ScrollAwareImageProviderresolveStreamForKey 方法中,當 stream.completer != null 且存在緩存時,直接就去加載原本已有的流程,如果快速滑動過程中圖片還沒加載的,就先不加載。

Flutter 中為了防止 context 在圖片異步加載流程中持有導致內存泄漏,又針對 Image 封裝了一個 DisposableBuildContext 。

DisposableBuildContext 是通過持有 State 來持有 context 的,并且在 dispose 時將 _state = null 設置為 null 來清除對 State 的持有。所以可以看到 上述代碼中,context.context == null 時直接就 return 了。

另外前面介紹的 resolveStreamForKey 也是新增加的方法,在原本的 ImageProvider 進行圖片加載時,會通過 ImageStream resolve 方法去得到并返回一個 ImageStream。

resolveStreamForKey 將原本 imageCacheImageStreamCompleter 的流程抽象出來,并且在 ScrollAwareImageProvider 中重寫了 resolveStreamForKey 方法的執(zhí)行邏輯,這樣快速滑動時,圖片的下載和解碼可以被中斷,從而減少了不必要的內存占用。

雖然這種方法不能100%解決圖片加載時 OOM 的問題,但是很大程度優(yōu)化了列表中的圖片內存占用,官方提供的數(shù)據(jù)上看理論上可以在原本基礎上節(jié)省出 70% 的內存。

image

相關推薦:Merged Defer image decoding when scrolling fast #49389

資源推薦

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容