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

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

在 Android 上 Flutter Image 主要占用的內(nèi)存不是 JVM 的內(nèi)存,而是 Graphics 相關(guān)的內(nèi)存,這樣的內(nèi)存調(diào)用可以最大程度利用 Native 內(nèi)存。

一、默認(rèn)流程

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

詳細(xì)圖片加載流程可見(jiàn):《十、 深入圖片加載流程)》

本身這個(gè)邏輯并沒(méi)有什么問(wèn)題,問(wèn)題就在于 Flutter 中對(duì)于圖片在內(nèi)存中的 Cache 對(duì)象是一個(gè) ImageStream 對(duì)象

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

所以一開(kāi)始最粗暴的情況是:通過(guò) PaintingBinding.instance 去設(shè)置 maximumSizemaximumSizeBytes,但是這種簡(jiǎn)單粗爆的處理方法并不能解決長(zhǎng)列表圖片加載的溢出問(wèn)題,因?yàn)樵陂L(zhǎng)列表中,快速滑動(dòng)的情況下可能會(huì)在一瞬間“并發(fā)”出大量圖片加載需求。

所以在 1.17 版本上,官方針對(duì)這種情況提供了場(chǎng)景化的處理方式: ScrollAwareImageProvider

二、ScrollAwareImageProvider

1.17 中可以看到,在 Image 控件中原本 _resolveImage 方法所使用的 imageProviderScrollAwareImageProvider 所代理,并且多了一個(gè)叫 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);
  }

其實(shí) ScrollAwareImageProvider 對(duì)象最主要的使用就是在 resolveStreamForKey 方法中,通過(guò) Scrollable.recommendDeferredLoadingForContext 方法去判斷當(dāng)前是不是需要推遲當(dāng)前幀畫(huà)面的加載,換言之就是:是否處于快速滑動(dòng)的過(guò)程

Scrollable.recommendDeferredLoadingForContext 作為一個(gè) static 方法,如何判斷當(dāng)前是不是處于列表的快速滑動(dòng)呢?

這就需要通過(guò)當(dāng)前 contextgetElementForInheritedWidgetOfExactType 方法去獲取 Scrollable 內(nèi)的 _ScrollableScope

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

獲取到 _ScrollableScope 就可以獲取到它內(nèi)部的 ScrollPosition , 進(jìn)而它的 ScrollPhysics 對(duì)應(yīng)的 recommendDeferredLoading 方法,判斷列表是否處于快速滑動(dòng)狀態(tài)。所以判斷是否快速滑動(dòng)的邏輯其實(shí)是在 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);
  }
  

關(guān)于 ScrollPhysics 的解釋可以看 《十八、 神奇的ScrollPhysics與Simulation》

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

@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 方法中,當(dāng) stream.completer != null 且存在緩存時(shí),直接就去加載原本已有的流程,如果快速滑動(dòng)過(guò)程中圖片還沒(méi)加載的,就先不加載。

Flutter 中為了防止 context 在圖片異步加載流程中持有導(dǎo)致內(nèi)存泄漏,又針對(duì) Image 封裝了一個(gè) DisposableBuildContext

DisposableBuildContext 是通過(guò)持有 State 來(lái)持有 context 的,并且在 dispose 時(shí)將 _state = null 設(shè)置為 null 來(lái)清除對(duì) State 的持有。所以可以看到 上述代碼中,context.context == null 時(shí)直接就 return 了。

另外前面介紹的 resolveStreamForKey 也是新增加的方法,在原本的 ImageProvider 進(jìn)行圖片加載時(shí),會(huì)通過(guò) ImageStream resolve 方法去得到并返回一個(gè) ImageStream

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

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

image

相關(guān)推薦:Merged Defer image decoding when scrolling fast #49389

資源推薦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,263評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,946評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,708評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,409評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,774評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,641評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,872評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,650評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373