簡介:阿里妹導讀:圖片加載是 APP 最多見也最基本的功能,也是影響用戶體驗的因素之一。在看似簡單的圖片加載背後卻隱藏着不少技術難題。本文介紹閒魚技術團隊在 Flutter 圖片優化上所作的嘗試,分享閒魚在典型的圖片處理方案上的技術細節,但願給你們帶來一些啓發。web
早在閒魚使用 Flutter 之初,圖片就是咱們核心關注和重點優化的功能。圖片展現體驗的好壞會對閒魚用戶的使用體驗產生巨大影響。大家是否也曾遇到過:算法
針對上述問題,從初版 Flutter 業務上線開始,閒魚對圖片框架的優化就從未中止。從開始的原生優化,到後面黑科技的外接紋理;從內存佔用,到包大小;文本會逐一介紹。但願其中的優化思路和手段,能給你們帶去一些啓發。緩存
從技術層面看圖片加載,其實簡單來講,追求的是無非是加載的效率的最大化——用盡量小的資源成本,儘量快地加載儘量多的圖片。性能優化
閒魚圖片的第一個版本其實基本上是純原生的方案。若是你不想魔改不少底層的邏輯,原生方案確定是最簡單和經濟的方案。原生方案的功能模塊以下:網絡
若是你啥都沒作直接上了,那麼你可能會發現效果並無達到你預期的那麼美好。那麼若是從原生的方案入手,咱們有哪些具體的優化手段呢?架構
沒錯猜對了,是緩存。對於圖片加載,最能想到的方案就是使用緩存。首先原生 Image 的組件是支持自定義圖片緩存的,具體的實現類是 ImageCache。ImageCache 的設置維度是兩個方向:框架
經過合理設置 ImageCache 的大小,能充分利用緩存機制加速圖片加載。不只如此,閒魚在這個點上還作了額外兩個重要優化:性能
低端手機適配測試
在上線之後,咱們陸續收到線上輿情的反饋,發現所有機型設置同一個緩存大小的作法並不是最優。特別是大緩存設置在低端機器上面,不只會出現體驗變差,甚至還會影響穩定性。基於實際狀況,咱們實現了一個能從 Native 側獲取機器基礎信息的 Flutter 插件。經過獲取的信息,咱們根據不一樣手機的配置設置不一樣的緩存策略。在低端機器上面適當下降圖片緩存的大小,同時在高端手機上將其適當放大。這樣能在不一樣配置的手機上獲取最優的緩存性能。優化
磁盤緩存
熟悉 APP 開發的同窗都知道,成熟的圖片加載框架通常都有多級緩存。除了常見的內存緩存,通常都會配置一個文件緩存。從加載效率上來講,是經過空間換時間,提高加載速度。從穩定性來講,這又不會過度佔用寶貴的內存資源,出現 OOM。可是惋惜的是,Flutter 自帶的圖片加載框架並無獨立的磁盤緩存。因此咱們在原生方案的基礎上擴展了磁盤緩存能力。
在具體的架構實現上,咱們並無徹底本身擼一個磁盤緩存。咱們的策略仍是複用現有能力。首先咱們將 Native 圖片加載框架的磁盤緩存的功能經過接口暴露出來。而後經過橋接的方式,將 Native 磁盤緩存能力嫁接到 Flutter 層。Flutter 側進行圖片加載的時候,若是內存沒有命中,就去磁盤緩存中進行二次搜索。若是都沒有命中才會走網絡請求。
經過增長磁盤緩存,Flutter 圖片加載效率進一步提高。
CDN 優化是另外一個很是重要圖片優化手段。CDN 優化的效率提高主要是:最小化傳輸圖片的大小。常見策略包括:
根據顯示大小裁剪
簡單來講,你要加載圖片的真實尺寸,可能會大於你實際展現窗口的大小。那麼你就不必加載完整大圖,你只須要加載一個能覆蓋窗口大小的圖片便可。經過這種方式,裁剪掉不須要的部分,就能最小化傳輸圖片的大小。從端側角度來講,一來能夠提高加載速度,二來能夠下降內存佔用。
適當壓縮圖片大小
這裏主要是根據實際狀況增長圖片壓縮的比例。在不影響顯示效果的狀況下,經過壓縮進一步下降圖片的大小。
圖片格式
建議優先使用 webp 這樣格式,圖片資源相對小。Flutter 原生支持 webp(包括動圖)。這裏特別強調一下 webp 動圖不只大小要比 gif 小不少,並且還對透明效果有更好的支持。webp 動圖是 gif 方案比較理想的一種替代方案。
基於上述緣由,閒魚圖片框架在 Flutter 側實現了一套 CDN 尺寸匹配的算法。經過該算法,請求圖片會根據實際顯示的大小,自動匹配到最合適的尺寸上並適當壓縮。若是圖片格式容許,圖片儘量轉化成 webp 格式下發。這樣 CDN 圖片的傳輸就能儘量高效。
除了上面的策略,Flutter 還有一些其餘的手段能夠優化圖片的性能。
圖片預加載
若是你想在展現的圖片的儘量的快,官方也提供了一套預加載的機制:precacheImage。precacheImage 能預先將圖片加載到內存,真正使用的時候就能秒出了。
Element 複用優化
其實這個算是一個 Flutter 通用的優化方案。複寫 didWidgetUpdate 方案,經過比較先後兩次 widget 中針對圖片的描述是否一致,來決定是否從新渲染 Element。這樣能避免同一個圖片,沒必要要的反覆渲染。
長列表優化
通常狀況下,Listview 是 flutter 最爲常見的滾動容器。在 Listview 中的性能好壞,直接影響最終的用戶體驗。
Flutter 的 Listview 跟 Native 的實現思路並不相同。其最大的特色是有一個 viewPort 的概念。超出 viewPort 的部分會被強制回收掉。
基於上述的原理,咱們有兩點建議:
1)cell 拆分
儘可能避免大型的 cell 出現,這樣能大幅下降 cell 頻繁建立過程當中的性能損耗。其實這裏影響的不只僅是圖片加載過程。文字,視頻等其餘組件也都應該避免 cell 過於複雜致使的性能問題。
2)合理使用緩衝區
ListView 能夠經過設置 cacheExtent 來設置預先加載的內容大小。經過預先加載能夠提高 view 渲染的速度。可是這個值須要合理設置,並不是越大越好。由於預加載緩存越大,對頁面總體內存的壓力就越大。
這裏須要客觀指出:若是是一個純 Flutter APP,原生方案是完善,夠用的。可是若是從混合 APP 的角度來講,有以下兩個缺陷:
1)沒法複用 Native 圖片加載能力
毫無疑問,原生的圖片方案是徹底獨立的圖片加載方案。對於一個混合 APP 來講,原生方案和 Native 的圖片框架相互獨立,能力沒法複用。例如 CDN 裁剪 & 壓縮等能力須要重複建設。特別是 Native一些獨特的圖片解碼能力,Flutter 就很難使用。這會形成 APP 範圍內的圖片格式的支持不統一。
2)內存性能不足
從整個 APP 的視角來講,採用原生圖片方案的狀況下,其實咱們維護了兩個大的緩存池:一個是 Native 的圖片緩存,一個是 Flutter 側的圖片緩存。兩個緩存沒法互通,這無疑是一個巨大的浪費。特別是對內存的峯值內存性能產生了很是大的壓力。
通過多輪優化,基於原生的方案已經得到了很是大的性能提高。可是整個 APP 的內存水位線依然比較高(特別是 Ios 端)。現實的壓力迫使咱們繼續對圖片框架進行更深度的優化。基於上述原生方案缺點的分析,咱們有了一個大膽的想法:可否徹底複用 Native 的圖片加載能力?
怎樣打通 Flutter 和 Native 的圖片能力?咱們想到了外接紋理。外接紋理並不是是 Flutter 自有的技術,它是音視頻領域經常使用的一種性能優化手段。
這個階段咱們基於 shared-Context 的方案實現了 Flutter 和 Native 的紋理外接。經過該方案,Flutter 能夠經過共享紋理的方式,拿到 Native 圖片庫加載好的圖片並展現。爲了實現這個紋理共享的通道,咱們對 engine 層作了深度定製。細節過程以下:
該方案不只打通了 Native 和 Flutter 的圖片架構,整個過程圖片加載的性能也獲得了優化。
外接紋理是閒魚圖片方案的一次大跨越。經過該技術,咱們不只實現圖片方案的本地能力複用,並且還能實現視頻能力的紋理外接。這避免了大量重複的建設,提高了整個 APP 的性能。
這個優化策略真真是被逼出來的。在對線上數據分析之後,咱們發現 Flutter 頁面棧有一個很是有意思的特色:多頁面棧狀況下,底層的頁面不會被釋放。即使是在內存很是緊張的狀況下,也不會執行回收。這樣就會致使一個問題:隨着頁面的增多,內存消耗會線性增加。這裏佔比最高的就是圖片資源的佔比了。
是否是能夠在頁面處於頁面棧底層的時候直接回收掉該頁面內的圖片呢?
在這個想法的驅動下,咱們對圖片架構進行了新一輪的優化。整個圖片框架中的圖片都會監聽頁面棧的變化。當方發現本身已經處於非棧頂的時候,就自動回收掉對應的圖片紋理釋放資源。這種方案能使圖片佔用的內存大小不會隨着頁面數的變多呈現持續線性增加。原理以下:
須要注意的是:這個階段頁面判斷位置實際上是須要頁面棧(具體來講就是混合棧)提供額外的接口來實現的。系統之間的耦合相對較高。
打通 Native 和 Flutter 側圖片框架之後,咱們發現了一個意外收穫:Native 和 Flutter 能夠共用本地圖片資源了。也就是說,咱們再也不須要將相同的圖片資源在 Flutter 和 Native 側各保留一份了。這樣能大幅提高本地資源的複用率,從而下降總體的包大小。基於這個方案,咱們實現了一套資源管理的功能,腳本能自動同步不一樣端的本地圖片資源。經過這樣提高本地資源利用率,下降包大小。
原生的 Image 是沒有 PlaceHolder 功能的。若是想用原生方案的話,須要使用 FadeInImage。針對閒魚的場景咱們有不少定製,因此咱們本身實現了一套 PlaceHolder 的機制。
從核心功能上來講,咱們引入了加載狀態的概念分爲:
針對不一樣的狀態,能夠細粒度的控制 PlaceHolder 的展現邏輯。
畢竟改了 engine
隨着閒魚業務的不斷推動,engine 的升級的成本是咱們必需要考慮的事情。可否不改 engine 實現一樣的功能是咱們核心的述求(PS:我認可咱們是貪心的)。
通道性能還有優化空間
外接紋理的方案須要經過橋的方式跟 Native 的能力作通訊。這裏包括圖片請求的傳遞和圖片加載各類狀態的同步。特別是在 listview 快速滑動的時候,經過橋發送的數據量仍是可觀的。當前方案每一個圖片加載時都會單獨進行橋的調用。在圖片數量比較多的狀況下,這顯然會是一個瓶頸。
耦合過多
在實現圖片回收方案的時候,目前方案須要棧提供是否在棧底層的接口。這裏就產生方案耦合,很難抽象出一個獨立乾淨的圖片加載方案。
時間來到了 2020 年,隨着對 Flutter 基礎能力理解的逐步深刻,咱們實現了一個總體方案更優的圖片框架。
外接紋理能夠不用修改 engine 麼?答案是確定的。
其實 Flutter 是提供了官方的外接紋理方案的。
並且 Native 操做的 texture 和 Flutter 側顯示的 texture 在底層是同一對象,並無產生額外的數據 copy。這樣就保證了紋理共享的足夠高效。那爲何閒魚以前會單獨基於 shared-Context 本身實現一套呢?1.12 版本以前,官方 Ios 的外接紋理方案有性能問題。每次渲染的過程當中(無論紋理是否有更新)都會頻繁獲取 CVPixelBuffer,形成沒必要要的性能損耗(過程有加鎖損耗)。該問題已經在 1.12 版本中修復(官方 commit 地址),這樣官方方案也足夠知足需求。在這樣的背景下,咱們從新啓用官方方案來實現外接紋理功能。
以前提到過,老版本的基於頁面棧的圖片資源回收須要強依賴棧功能的接口。一方面產生了沒必要要的依賴,更重要的是,總體方案沒法獨立成通用方案。爲了解決這個問題,咱們對 Flutter 底層進行了深刻的研究。咱們發現 Flutter 的 layer 層能夠穩定感知到頁面棧的變化。
而後每一個頁面經過 context 獲取的 router 對象做爲標識對一個頁面中的全部的圖片對象進行從新組織。全部獲取到同一個 router 對象的標識成同一個頁面。這樣就能以頁面爲單位對全部的圖片進行管理。總體上經過 LRU 的算法來模擬虛擬頁面棧結構。這樣就能對棧底頁面的圖片資源實現回收了。
通道的高度複用
首先咱們以一幀爲單位對這一幀中的圖片請求進行聚合,而後在一次通道請求中傳遞給 Native 的圖片加載框架。這樣能避免頻繁的橋調用。特別在快速滾動等場景下優化效果尤其明顯。
高效的紋理複用
使用外接紋理進行圖片加載之後,咱們發現複用紋理能夠進一步提高性能。舉一個簡單的場景。咱們知道電商場景中,商品展現常常會有標籤,打底圖這樣的圖片。這類圖片每每在不一樣的商品上會出現大量重複。這時候,能夠將已經渲染好的紋理,直接複用給不一樣的顯示組件。這樣能進一步優化 GPU 內存的佔用,避免重複建立。爲了精確對紋理進行管理,咱們引入了引用計數的算法來管理紋理的複用。經過這些方案,咱們實現了紋理跨頁面高效複用。
此外,咱們將紋理和請求的映射關係移動到了 Flutter 側。這樣能在最短路徑上完成紋理的複用,進一步減小了橋的通訊的壓力。
因爲最新的版本目前還在灰度,具體數據後續會寫文跟你們詳細介紹。下屬數據主要以方案二爲主。
內存優化
經過打通 Native,相比於首次上線版本,在顯示效果不變的狀況下,Ios 的 abort 率下降 25%,用戶體驗明顯提高。
多頁面棧內存優化**
多頁面棧的內存優化,在多頁面場景下對內存優化做用明顯。咱們作了一個極限試驗效果以下(測試環境,非閒魚 APP):
可見多頁面棧的優化,能夠將多 Flutter 頁面的內存佔用控制得更好。
包大小減小
經過接入外接紋理,本地資源獲得了更好的複用,包大小下降 1M。早期閒魚接入 Flutter,會以改造現有頁面爲切入點。資源重複狀況比較嚴重,可是隨着閒魚 Flutter 新業務愈來愈多。Flutter 和 Native 的重複資源愈來愈少。外接紋理對包大小的影響已經逐步變弱。
這是一場沒有盡頭的旅行,咱們對閒魚圖片的優化還會持續。特別是咱們最新的方案,受限篇幅,本文只是作了初步介紹。更多技術細節,包括測試數據,咱們隨後還會專門寫文繼續給你們作介紹。方案完善之後,咱們也會逐步開源。