天天看點

閑魚Flutter圖檔架構架構演進(超詳細)

作者:閑魚技術-意境

1.那些年

圖檔對一個端側研發來說是一老生常談的話題了。閑魚作為業界在Flutter技術方向上最早一批投入的團隊。從使用Flutter之初,圖檔就是我們核心關注和重點優化的功能。圖檔算是閑魚業務場景下最為重要的内容表現形式之一。圖檔展示體驗的好壞會對閑魚使用者的使用體驗産生巨大影響。你們是否也曾遇到過:

  • 圖檔加載記憶體占用過多?
  • 使用flutter以後本地資源重複,使用率不高?
  • 混合方案下Flutter原生圖檔加載效率不高?

針對上述問題,從第一版Flutter業務上線開始,閑魚對圖檔架構的優化就從未停止。從開始的原生優化,到後面黑科技的外接紋理;從記憶體占用,到包大小;文本會逐一介紹。希望其中的優化思路和手段,能給大家帶去一些啟發。

2. 原生模式

從技術層面看圖檔加載,其實簡單來說,追求的是無非是加載的效率的最大化—用盡可能小的資源成本,盡可能快地加載盡可能多的圖檔。

閑魚圖檔的第一個版本其實基本上是純原生的方案。如果你不想魔改很多底層的邏輯,原生方案肯定是最簡單和經濟的方案。原生方案的功能子產品如下:

閑魚Flutter圖檔架構架構演進(超詳細)

如果你啥都沒做直接上了,那麼你可能會發現效果并沒有達到你預期的那麼美好。那麼如果從原生的方案入手,我們有哪些具體的優化手段呢?

2.1. 設定圖檔緩存

沒錯猜對了,是緩存。對于圖檔加載,最能想到的方案就是使用緩存。首先原生Image的元件是支援自定義圖檔緩存的,具體的實作類是ImageCache。ImageCache的設定次元是兩個方向: 1.緩存圖檔的張數。通過maximumSize設定。預設是1000張。2. 緩存空間的大小。 通過maximumSizeBytes 來設定。預設值100M。相比張數的限制,其實大小的設定方式更加符合我們的最終的預期。

通過合理設定ImageCache的大小,能充分利用緩存機制加速圖檔加載。不僅如此,閑魚在這個點上還做了額外兩個重要優化:

  1. 低端手機适配

在上線以後,我們陸續收到線上輿情的回報,發現全部機型設定同一個緩存大小的做法并非最優。特别是大緩存設定在低端機器上面,不僅會出現體驗變差,甚至還會影響穩定性。基于實際情況,我們實作了一個能從Native側擷取機器基礎資訊的Flutter 插件。通過擷取的資訊,我們根據不同手機的配置設定不同的緩存政策。在低端機器上面适當降低圖檔緩存的大小,同時在高端手機上将其适當放大。這樣能在不同配置的手機上擷取最優的緩存性能。

  1. 磁盤緩存

熟悉APP開發的同學都知道,成熟的圖檔加載架構一般都有多級緩存。除了常見的記憶體緩存,一般都會配置一個檔案緩存。從加載效率上來說,是通過空間換時間,提升加載速度。從穩定性來說,這又不會過分占用寶貴的記憶體資源,出現OOM。但是可惜的是,Flutter自帶的圖檔加載架構并沒有獨立的磁盤緩存。是以我們在原生方案的基礎上擴充了磁盤緩存能力。

在具體的架構實作上,我們并沒有完全自己撸一個磁盤緩存。我們的政策還是複用現有能力。首先我們将Native圖檔加載架構的磁盤緩存的功能通過接口暴露出來。然後通過橋接的方式,将Native 磁盤緩存能力嫁接到Flutter層。Flutter側進行圖檔加載的時候,如果記憶體沒有命中,就去磁盤緩存中進行二次搜尋。如果都沒有命中才會走網絡請求。

通過增加磁盤緩存,Flutter圖檔加載效率進一步提升。

閑魚Flutter圖檔架構架構演進(超詳細)

2.2. 設定CDN優化

CDN 優化是另一個非常重要圖檔優化手段。CDN優化的效率提升主要是:最小化傳輸圖檔的大小。常見政策包括:

  1. 根據顯示大小裁剪

簡單來說,你要加載圖檔的真實尺寸,可能會大于你實際展示視窗的大小。那麼你就沒必要加載完整大圖,你隻需要加載一個能覆寫視窗大小的圖檔即可。通過這種方式,裁剪掉不需要的部分,就能最小化傳輸圖檔的大小。從端側角度來說,一來可以提升加載速度,二來可以降低記憶體占用。

  1. 适當壓縮圖檔大小

這裡主要是根據實際情況增加圖檔壓縮的比例。在不影響顯示效果的情況下,通過壓縮進一步降低圖檔的大小。

  1. 圖檔格式

建議優先使用webp這樣格式,圖檔資源相對小。Flutter原生支援webp(包括動圖)。這裡特别強調一下webp動圖不僅大小要比gif小很多,而且還對透明效果有更好的支援。webp動圖是gif方案比較理想的一種替代方案。

閑魚Flutter圖檔架構架構演進(超詳細)

基于上述原因,閑魚圖檔架構在Flutter側實作了一套CDN尺寸比對的算法。通過該算法,請求圖檔會根據實際顯示的大小,自動比對到最合适的尺寸上并适當壓縮。如果圖檔格式允許,圖檔盡可能轉化成webp格式下發。這樣cdn圖檔的傳輸就能盡可能高效。

2.3. 其他優化

除了上面的政策,Flutter還有一些其他的手段可以優化圖檔的性能。

  1. 圖檔預加載

如果你想在展示的圖檔的盡可能的快,官方也提供了一套預加載的機制:precacheImage。precacheImage能預先将圖檔加載到記憶體,真正使用的時候就能秒出了。

  1. Element複用優化

其實這個算是一個Flutter通用的優化方案。複寫didWidgetUpdate方案,通過比較前後兩次widget中針對圖檔的描述是否一緻,來決定是否重新渲染Element。這樣能避免同一個圖檔,不必要的反複渲染。

  1. 長清單優化

一般情況下,Listview是flutter最為常見的滾動容器。在Listview中的性能好壞,直接影響最終的使用者體驗。

Flutter的Listview跟Native的實作思路并不相同。其最大的特點是有一個viewPort的概念。超出viewPort的部分會被強制回收掉。

基于上述的原理,我們有兩點建議:

  1. cell拆分

盡量避免大型的cell出現,這樣能大幅降低cell頻繁建立過程中的性能損耗。其實這裡影響的不僅僅是圖檔加載過程。文字,視訊等其他元件也都應該避免cell過于複雜導緻的性能問題。

  1. 合理使用緩沖區

ListView可以通過設定cacheExtent 來設定預先加載的内容大小。通過預先加載可以提升view渲染的速度。但是這個值需要合理設定,并非越大越好。因為預加載緩存越大,對頁面整體記憶體的壓力就越大。

2.4. 方案的不足

這裡需要客觀指出:如果是一個純Flutter APP,原生方案是完善,夠用的。但是如果從混合APP的角度來說,有如下兩個缺陷:

1. 無法複用Native圖檔加載能力

毫無疑問,原生的圖檔方案是完全獨立的圖檔加載方案。對于一個混合APP來說,原生方案和Native的圖檔架構互相獨立,能力無法複用。例如CDN裁剪&壓縮等能力需要重複建設。特别是Native一些獨特的圖檔解碼能力,Flutter就很難使用。這會造成APP範圍内的圖檔格式的支援不統一。

2. 記憶體性能不足

從整個APP的視角來說,采用原生圖檔方案的情況下,其實我們維護了兩個大的緩存池:一個是Native的圖檔緩存,一個是Flutter側的圖檔緩存。兩個緩存無法互通,這無疑是一個巨大的浪費。特别是對記憶體的峰值記憶體性能産生了非常大的壓力。

3. 打通Native

經過多輪優化,基于原生的方案已經獲得了非常大的性能提升。但是整個APP的記憶體水位線依然比較高(特别是Ios端)。現實的壓力迫使我們繼續對圖檔架構進行更深度的優化。基于上述原生方案缺點的分析,我們有了一個大膽的想法:能否完全複用Native的圖檔加載能力?

3.1. 外接紋理

怎樣打通Flutter和Native的圖檔能力?我們想到了外接紋理。外接紋理并非是Flutter自有的技術,他是音視訊領域常用的一種性能優化手段。

這個階段我們基于shared-Context的方案實作了Flutter和Native的紋理外接。通過該方案,Flutter可以通過共享紋理的方式,拿到Native圖檔庫加載好的圖檔并展示。為了實作這個紋理共享的通道,我們對engine層做了深度定制。細節過程如下:

閑魚Flutter圖檔架構架構演進(超詳細)

該方案不僅打通了Native和Flutter的圖檔架構,整個過程圖檔加載的性能也得到了優化。想要了解細節的同學可以繼續閱讀這篇文章:

萬萬沒想到——Flutter外接紋理

外接紋理是閑魚圖檔方案的一次大跨越。通過該技術,我們不僅實作圖檔方案的本地能力複用,而且還能實作視訊能力的紋理外接。這避免了大量重複的建設,提升了整個APP的性能。

3.2. 多頁面記憶體優化

這個優化政策是真真被逼出來的。在對線上資料分析以後,我們發現Flutter頁面棧有一個非常有意思的特點:

多頁面棧情況下,底層的頁面不會被釋放。即便是在記憶體非常緊張的情況下,也不會執行回收。這樣就會導緻一個問題:随着頁面的增多,記憶體消耗會線性增長。這裡占比最高的就是圖檔資源的占比了。

是不是可以在頁面處于頁面棧底層的時候直接回收掉該頁面内的圖檔呢?

在這個想法的驅動下,我們對圖檔架構進行了新一輪的優化。整個圖檔架構中的圖檔都會監聽頁面棧的變化。當方發現自己已經處于非棧頂的時候,就自動回收掉對應的圖檔紋理釋放資源。這種方案能使圖檔占用的記憶體大小不會随着頁面數的變多呈現持續線性增長。原理如下:

閑魚Flutter圖檔架構架構演進(超詳細)

需要注意的是:這個階段頁面判斷位置其實是需要頁面棧(具體來說就是混合棧)提供額外的接口來實作的。系統之間的耦合相對較高。

3.3. 意外收獲包大小

打通Native和Flutter側圖檔架構以後,我們發現了一個意外收獲: Native和Flutter可以共用本地圖檔資源了。也就是說,我們不再需要将相同的圖檔資源在Flutter和Native側各保留一份了。這樣能大幅提升本地資源的複用率,進而降低整體的包大小。基于這個方案,我們實作了一套資源管理的功能,腳本能自動同步不同端的本地圖檔資源。通過這樣提升本地資源使用率,降低包大小。

3.4. 其他優化

  1. PlaceHolder強化

原生的Image是沒有PlaceHolder功能的。如果想用原生方案的話,需要使用FadeInImage。針對閑魚的場景我們有很多定制,是以我們自己實作了一套PlaceHolder的機制。

從核心功能上來說,我們引入了加載狀态的概念分為: 1. 未初始化 2. 加載中 3. 加載完成 等。針對不同的狀态,可以細粒度的控制PlaceHolder的展示邏輯。

3.5. 整體架構

閑魚Flutter圖檔架構架構演進(超詳細)

3.6. 方案的不足

  1. 畢竟改了engine

随着閑魚業務的不斷推進,engine的更新的成本是我們必須要考慮的事情。能否不改engine實作同樣的功能是我們核心的述求。(PS: 我承認我們是貪心的)

  1. 通道性能還有優化空間

外接紋理的方案需要通過橋的方式跟native的能力做通信。這裡包括圖檔請求的傳遞和圖檔加載各種狀态的同步。特别是在listview快速滑動的時候,通過橋發送的資料量還是可觀的。目前方案每個圖檔加載時都會單獨進行橋的調用。在圖檔數量比較多的情況下,這顯然會是一個瓶頸。

  1. 耦合過多

在實作圖檔回收方案的時候,目前方案需要棧提供是否在棧底層的接口。這裡就産生方案耦合,很難抽象出一個獨立幹淨的圖檔加載方案。

4. Clean&Efficient

時間來到了2020年,随着對Flutter基礎能力了解的逐漸深入,我們實作了一個整體方案更優的圖檔架構。

4.1. 無侵入外接紋理

外接紋理可以不用修改engine麼?答案是肯定的。

其實Flutter是提供了官方的外接紋理方案的。

閑魚Flutter圖檔架構架構演進(超詳細)

而且Native操作的texture和Flutter側顯示的texture在底層是同一對象,并沒有産生額外的資料copy。這樣就保證了紋理共享的足夠高效。那為什麼閑魚之前會單獨基于shared-Context自己實作一套呢?1.12版本之前,官方Ios的外接紋理方案有性能問題。每次渲染的過程中(不管紋理是否有更新)都會頻繁擷取CVPixelBuffer,造成不必要的性能損耗(過程有加鎖損耗)。該問題已經在1.12版本中修複(

官方commit位址

),這樣官方方案也足夠滿足需求。在這樣的背景下,我們重新啟用官方方案來實作外接紋理功能。

4.2. 獨立的記憶體優化

之前提到過,老版本的基于頁面棧的圖檔資源回收需要強依賴棧功能的接口。一方面産生了不必要的依賴,更重要的是,整體方案無法獨立成通用方案。為了解決這個問題,我們對Flutter底層進行了深入的研究。我們發現Flutter的layer層可以穩定感覺到頁面棧的變化。

閑魚Flutter圖檔架構架構演進(超詳細)

然後每個頁面通過context擷取的router對象作為辨別對一個頁面中的所有的圖檔對象進行重新組織。所有擷取到同一個router對象的辨別成同一個頁面。這樣就能以頁面為機關對所有的圖檔進行管理。整體上通過LRU的算法來模拟虛拟頁面棧結構。這樣就能對棧底頁面的圖檔資源實作回收了。

4.3. 其他優化

1. 通道的高度複用

首先我們以一幀為機關對這一幀中的圖檔請求進行聚合,然後在一次通道請求中傳遞給Native的圖檔加載架構。這樣能避免頻繁的橋調用。特别在快速滾動等場景下優化效果尤為明顯。

閑魚Flutter圖檔架構架構演進(超詳細)

2. 高效的紋理複用

使用外接紋理進行圖檔加載以後,我們發現複用紋理可以進一步提升性能。舉一個簡單的場景。我們知道電商場景中,商品展示經常會有标簽,打底圖這樣的圖檔。這類圖檔往往在不同的商品上會出現大量重複。這時候,可以将已經渲染好的紋理,直接複用給不同的顯示元件。這樣能進一步優化GPU記憶體的占用,避免重複建立。為了精确對紋理進行管理,我們引入了引用計數的算法來管理紋理的複用。通過這些方案,我們實作了紋理跨頁面高效複用。

閑魚Flutter圖檔架構架構演進(超詳細)

此外,我們将紋理和請求的映射關系移動到了Flutter側。這樣能在最短路徑上完成紋理的複用,進一步減少了橋的通信的壓力。

4.5. 整體架構

閑魚Flutter圖檔架構架構演進(超詳細)

5. 優化效果

由于最新的版本目前還在灰階,具體資料後續會寫文跟大家詳細介紹。下屬資料主要以方案二為主。

  • 記憶體優化
    • 通過打通Native,相比于首次上線版本,在顯示效果不變的情況下,Ios的abort率降低25%,使用者體驗明顯提升。
    • 多頁面棧記憶體優化
    多頁面棧的記憶體優化,在多頁面場景下對記憶體優化作用明顯。我們做了一個極限試驗效果如下:(測試環境,非閑魚APP)
閑魚Flutter圖檔架構架構演進(超詳細)

​ 可見多頁面棧的優化,可以将多Flutter頁面的記憶體占用控制得更好。

  • 包大小減少

通過接入外接紋理,本地資源得到了更好的複用,包大小降低1M。早期閑魚接入Flutter,會以改造現有頁面為切入點。資源重複情況比較嚴重,但是随着閑魚Flutter 新業務越來越多。Flutter和Native的重複資源越來越少。外接紋理對包大小的影響已經逐漸變弱。

6. 總結

本文介紹了閑魚在Flutter圖檔架構方向上所做的持續優化。介紹了閑魚不同時期,典型的圖檔技術方案的細節。希望可以給到讀者一些啟發。這是一場沒有盡頭的旅行,我們對閑魚圖檔的優化還會持續。特别是我們最新的方案,受限篇幅,本文隻是做了初步介紹。更多技術細節,包括測試資料,我們随後還會專門寫文繼續給大家做介紹。方案完善以後,我們也會逐漸開源。

敬請期待!

7. 引用:

https://yuque.antfin-inc.com/xytec/xqgqgq/px23vn flutter.dev Flutter

[Flutter engine](

https://github.com/flutter/engine

繼續閱讀