新聞中心
圖片加載是 APP 最常見(jiàn)也最基本的功能,也是影響用戶體驗(yàn)的因素之一。在看似簡(jiǎn)單的圖片加載背后卻隱藏著很多技術(shù)難題。本文介紹閑魚(yú)技術(shù)團(tuán)隊(duì)在 Flutter 圖片優(yōu)化上所做的嘗試,分享閑魚(yú)在典型的圖片處理方案上的技術(shù)細(xì)節(jié),希望給大家?guī)?lái)一些啟發(fā)。

那些年
早在閑魚(yú)使用 Flutter 之初,圖片就是我們核心關(guān)注和重點(diǎn)優(yōu)化的功能。圖片展示體驗(yàn)的好壞會(huì)對(duì)閑魚(yú)用戶的使用體驗(yàn)產(chǎn)生巨大影響。你們是否也曾遇到過(guò):
- 圖片加載內(nèi)存占用過(guò)多?
- 使用 Flutter 以后本地資源重復(fù),利用率不高?
- 混合方案下 Flutter 原生圖片加載效率不高?
針對(duì)上述問(wèn)題,從第一版 Flutter 業(yè)務(wù)上線開(kāi)始,閑魚(yú)對(duì)圖片框架的優(yōu)化就從未停止。從開(kāi)始的原生優(yōu)化,到后面黑科技的外接紋理;從內(nèi)存占用,到包大小;文本會(huì)逐一介紹。希望其中的優(yōu)化思路和手段,能給大家?guī)ヒ恍﹩l(fā)。
原生模式
從技術(shù)層面看圖片加載,其實(shí)簡(jiǎn)單來(lái)說(shuō),追求的是無(wú)非是加載的效率的最大化——用盡可能小的資源成本,盡可能快地加載盡可能多的圖片。
閑魚(yú)圖片的第一個(gè)版本其實(shí)基本上是純?cè)姆桨?。如果你不想魔改很多底層的邏輯,原生方案肯定是最?jiǎn)單和經(jīng)濟(jì)的方案。原生方案的功能模塊如下:
??
??
如果你啥都沒(méi)做直接上了,那么你可能會(huì)發(fā)現(xiàn)效果并沒(méi)有達(dá)到你預(yù)期的那么美好。那么如果從原生的方案入手,我們有哪些具體的優(yōu)化手段呢?
設(shè)置圖片緩存
沒(méi)錯(cuò)猜對(duì)了,是緩存。對(duì)于圖片加載,最能想到的方案就是使用緩存。首先原生 Image 的組件是支持自定義圖片緩存的,具體的實(shí)現(xiàn)類是 ImageCache。ImageCache 的設(shè)置維度是兩個(gè)方向:
- 緩存圖片的張數(shù)。通過(guò) maximumSize 設(shè)置。默認(rèn)是 1000 張。
- 緩存空間的大小。通過(guò) maximumSizeBytes 來(lái)設(shè)置。默認(rèn)值 100M。相比張數(shù)的限制,其實(shí)大小的設(shè)置方式更加符合我們的最終的預(yù)期。
通過(guò)合理設(shè)置 ImageCache 的大小,能充分利用緩存機(jī)制加速圖片加載。不僅如此,閑魚(yú)在這個(gè)點(diǎn)上還做了額外兩個(gè)重要優(yōu)化:
低端手機(jī)適配
在上線以后,我們陸續(xù)收到線上輿情的反饋,發(fā)現(xiàn)全部機(jī)型設(shè)置同一個(gè)緩存大小的做法并非最優(yōu)。特別是大緩存設(shè)置在低端機(jī)器上面,不僅會(huì)出現(xiàn)體驗(yàn)變差,甚至還會(huì)影響穩(wěn)定性?;趯?shí)際情況,我們實(shí)現(xiàn)了一個(gè)能從 Native 側(cè)獲取機(jī)器基礎(chǔ)信息的 Flutter 插件。通過(guò)獲取的信息,我們根據(jù)不同手機(jī)的配置設(shè)置不同的緩存策略。在低端機(jī)器上面適當(dāng)降低圖片緩存的大小,同時(shí)在高端手機(jī)上將其適當(dāng)放大。這樣能在不同配置的手機(jī)上獲取最優(yōu)的緩存性能。
磁盤緩存
熟悉 APP 開(kāi)發(fā)的同學(xué)都知道,成熟的圖片加載框架一般都有多級(jí)緩存。除了常見(jiàn)的內(nèi)存緩存,一般都會(huì)配置一個(gè)文件緩存。從加載效率上來(lái)說(shuō),是通過(guò)空間換時(shí)間,提升加載速度。從穩(wěn)定性來(lái)說(shuō),這又不會(huì)過(guò)分占用寶貴的內(nèi)存資源,出現(xiàn) OOM。但是可惜的是,F(xiàn)lutter 自帶的圖片加載框架并沒(méi)有獨(dú)立的磁盤緩存。所以我們?cè)谠桨傅幕A(chǔ)上擴(kuò)展了磁盤緩存能力。
在具體的架構(gòu)實(shí)現(xiàn)上,我們并沒(méi)有完全自己擼一個(gè)磁盤緩存。我們的策略還是復(fù)用現(xiàn)有能力。首先我們將 Native 圖片加載框架的磁盤緩存的功能通過(guò)接口暴露出來(lái)。然后通過(guò)橋接的方式,將 Native 磁盤緩存能力嫁接到 Flutter 層。Flutter 側(cè)進(jìn)行圖片加載的時(shí)候,如果內(nèi)存沒(méi)有命中,就去磁盤緩存中進(jìn)行二次搜索。如果都沒(méi)有命中才會(huì)走網(wǎng)絡(luò)請(qǐng)求。
通過(guò)增加磁盤緩存,F(xiàn)lutter 圖片加載效率進(jìn)一步提升。
??
??
設(shè)置 CDN 優(yōu)化
CDN 優(yōu)化是另一個(gè)非常重要圖片優(yōu)化手段。CDN 優(yōu)化的效率提升主要是:最小化傳輸圖片的大小。常見(jiàn)策略包括:
根據(jù)顯示大小裁剪
簡(jiǎn)單來(lái)說(shuō),你要加載圖片的真實(shí)尺寸,可能會(huì)大于你實(shí)際展示窗口的大小。那么你就沒(méi)必要加載完整大圖,你只需要加載一個(gè)能覆蓋窗口大小的圖片即可。通過(guò)這種方式,裁剪掉不需要的部分,就能最小化傳輸圖片的大小。從端側(cè)角度來(lái)說(shuō),一來(lái)可以提升加載速度,二來(lái)可以降低內(nèi)存占用。
適當(dāng)壓縮圖片大小
這里主要是根據(jù)實(shí)際情況增加圖片壓縮的比例。在不影響顯示效果的情況下,通過(guò)壓縮進(jìn)一步降低圖片的大小。
圖片格式
建議優(yōu)先使用 webp 這樣格式,圖片資源相對(duì)小。Flutter 原生支持 webp(包括動(dòng)圖)。這里特別強(qiáng)調(diào)一下 webp 動(dòng)圖不僅大小要比 gif 小很多,而且還對(duì)透明效果有更好的支持。webp 動(dòng)圖是 gif 方案比較理想的一種替代方案。
??
??
基于上述原因,閑魚(yú)圖片框架在 Flutter 側(cè)實(shí)現(xiàn)了一套 CDN 尺寸匹配的算法。通過(guò)該算法,請(qǐng)求圖片會(huì)根據(jù)實(shí)際顯示的大小,自動(dòng)匹配到最合適的尺寸上并適當(dāng)壓縮。如果圖片格式允許,圖片盡可能轉(zhuǎn)化成 webp 格式下發(fā)。這樣 CDN 圖片的傳輸就能盡可能高效。
其他優(yōu)化
除了上面的策略,F(xiàn)lutter 還有一些其他的手段可以優(yōu)化圖片的性能。
圖片預(yù)加載
如果你想在展示的圖片的盡可能的快,官方也提供了一套預(yù)加載的機(jī)制:precacheImage。precacheImage 能預(yù)先將圖片加載到內(nèi)存,真正使用的時(shí)候就能秒出了。
Element 復(fù)用優(yōu)化
其實(shí)這個(gè)算是一個(gè) Flutter 通用的優(yōu)化方案。復(fù)寫(xiě) didWidgetUpdate 方案,通過(guò)比較前后兩次 widget 中針對(duì)圖片的描述是否一致,來(lái)決定是否重新渲染 Element。這樣能避免同一個(gè)圖片,不必要的反復(fù)渲染。
長(zhǎng)列表優(yōu)化
一般情況下,Listview 是 flutter 最為常見(jiàn)的滾動(dòng)容器。在 Listview 中的性能好壞,直接影響最終的用戶體驗(yàn)。
Flutter 的 Listview 跟 Native 的實(shí)現(xiàn)思路并不相同。其最大的特點(diǎn)是有一個(gè) viewPort 的概念。超出 viewPort 的部分會(huì)被強(qiáng)制回收掉。
基于上述的原理,我們有兩點(diǎn)建議:
1)cell 拆分
盡量避免大型的 cell 出現(xiàn),這樣能大幅降低 cell 頻繁創(chuàng)建過(guò)程中的性能損耗。其實(shí)這里影響的不僅僅是圖片加載過(guò)程。文字,視頻等其他組件也都應(yīng)該避免 cell 過(guò)于復(fù)雜導(dǎo)致的性能問(wèn)題。
2)合理使用緩沖區(qū)
ListView 可以通過(guò)設(shè)置 cacheExtent 來(lái)設(shè)置預(yù)先加載的內(nèi)容大小。通過(guò)預(yù)先加載可以提升 view 渲染的速度。但是這個(gè)值需要合理設(shè)置,并非越大越好。因?yàn)轭A(yù)加載緩存越大,對(duì)頁(yè)面整體內(nèi)存的壓力就越大。
該方案的不足
這里需要客觀指出:如果是一個(gè)純 Flutter APP,原生方案是完善,夠用的。但是如果從混合 APP 的角度來(lái)說(shuō),有如下兩個(gè)缺陷:
1)無(wú)法復(fù)用 Native 圖片加載能力
毫無(wú)疑問(wèn),原生的圖片方案是完全獨(dú)立的圖片加載方案。對(duì)于一個(gè)混合 APP 來(lái)說(shuō),原生方案和 Native 的圖片框架相互獨(dú)立,能力無(wú)法復(fù)用。例如 CDN 裁剪 & 壓縮等能力需要重復(fù)建設(shè)。特別是 Native一些獨(dú)特的圖片解碼能力,F(xiàn)lutter 就很難使用。這會(huì)造成 APP 范圍內(nèi)的圖片格式的支持不統(tǒng)一。
2)內(nèi)存性能不足
從整個(gè) APP 的視角來(lái)說(shuō),采用原生圖片方案的情況下,其實(shí)我們維護(hù)了兩個(gè)大的緩存池:一個(gè)是 Native 的圖片緩存,一個(gè)是 Flutter 側(cè)的圖片緩存。兩個(gè)緩存無(wú)法互通,這無(wú)疑是一個(gè)巨大的浪費(fèi)。特別是對(duì)內(nèi)存的峰值內(nèi)存性能產(chǎn)生了非常大的壓力。
打通 Native
經(jīng)過(guò)多輪優(yōu)化,基于原生的方案已經(jīng)獲得了非常大的性能提升。但是整個(gè) APP 的內(nèi)存水位線依然比較高(特別是 Ios 端)?,F(xiàn)實(shí)的壓力迫使我們繼續(xù)對(duì)圖片框架進(jìn)行更深度的優(yōu)化。基于上述原生方案缺點(diǎn)的分析,我們有了一個(gè)大膽的想法:能否完全復(fù)用 Native 的圖片加載能力?
外接紋理
怎樣打通 Flutter 和 Native 的圖片能力?我們想到了外接紋理。外接紋理并非是 Flutter 自有的技術(shù),它是音視頻領(lǐng)域常用的一種性能優(yōu)化手段。
這個(gè)階段我們基于 shared-Context 的方案實(shí)現(xiàn)了 Flutter 和 Native 的紋理外接。通過(guò)該方案,F(xiàn)lutter 可以通過(guò)共享紋理的方式,拿到 Native 圖片庫(kù)加載好的圖片并展示。為了實(shí)現(xiàn)這個(gè)紋理共享的通道,我們對(duì) engine 層做了深度定制。細(xì)節(jié)過(guò)程如下:
??
??
該方案不僅打通了 Native 和 Flutter 的圖片架構(gòu),整個(gè)過(guò)程圖片加載的性能也得到了優(yōu)化。
外接紋理是閑魚(yú)圖片方案的一次大跨越。通過(guò)該技術(shù),我們不僅實(shí)現(xiàn)圖片方案的本地能力復(fù)用,而且還能實(shí)現(xiàn)視頻能力的紋理外接。這避免了大量重復(fù)的建設(shè),提升了整個(gè) APP 的性能。
多頁(yè)面內(nèi)存優(yōu)化
這個(gè)優(yōu)化策略真真是被逼出來(lái)的。在對(duì)線上數(shù)據(jù)分析以后,我們發(fā)現(xiàn) Flutter 頁(yè)面棧有一個(gè)非常有意思的特點(diǎn):多頁(yè)面棧情況下,底層的頁(yè)面不會(huì)被釋放。即便是在內(nèi)存非常緊張的情況下,也不會(huì)執(zhí)行回收。這樣就會(huì)導(dǎo)致一個(gè)問(wèn)題:隨著頁(yè)面的增多,內(nèi)存消耗會(huì)線性增長(zhǎng)。這里占比最高的就是圖片資源的占比了。
是不是可以在頁(yè)面處于頁(yè)面棧底層的時(shí)候直接回收掉該頁(yè)面內(nèi)的圖片呢?
在這個(gè)想法的驅(qū)動(dòng)下,我們對(duì)圖片架構(gòu)進(jìn)行了新一輪的優(yōu)化。整個(gè)圖片框架中的圖片都會(huì)監(jiān)聽(tīng)頁(yè)面棧的變化。當(dāng)方發(fā)現(xiàn)自己已經(jīng)處于非棧頂?shù)臅r(shí)候,就自動(dòng)回收掉對(duì)應(yīng)的圖片紋理釋放資源。這種方案能使圖片占用的內(nèi)存大小不會(huì)隨著頁(yè)面數(shù)的變多呈現(xiàn)持續(xù)線性增長(zhǎng)。原理如下:
??
??
需要注意的是:這個(gè)階段頁(yè)面判斷位置其實(shí)是需要頁(yè)面棧(具體來(lái)說(shuō)就是混合棧)提供額外的接口來(lái)實(shí)現(xiàn)的。系統(tǒng)之間的耦合相對(duì)較高。
意外收獲:包大小
打通 Native 和 Flutter 側(cè)圖片框架以后,我們發(fā)現(xiàn)了一個(gè)意外收獲:Native 和 Flutter 可以共用本地圖片資源了。也就是說(shuō),我們不再需要將相同的圖片資源在 Flutter 和 Native 側(cè)各保留一份了。這樣能大幅提升本地資源的復(fù)用率,從而降低整體的包大小。基于這個(gè)方案,我們實(shí)現(xiàn)了一套資源管理的功能,腳本能自動(dòng)同步不同端的本地圖片資源。通過(guò)這樣提升本地資源利用率,降低包大小。
其他優(yōu)化——PlaceHolder 強(qiáng)化
原生的 Image 是沒(méi)有 PlaceHolder 功能的。如果想用原生方案的話,需要使用 FadeInImage。針對(duì)閑魚(yú)的場(chǎng)景我們有很多定制,所以我們自己實(shí)現(xiàn)了一套 PlaceHolder 的機(jī)制。
從核心功能上來(lái)說(shuō),我們引入了加載狀態(tài)的概念分為:
- 未初始化
- 加載中
- 加載完成
針對(duì)不同的狀態(tài),可以細(xì)粒度的控制 PlaceHolder 的展示邏輯。
整體架構(gòu)
??
??
該方案的不足
畢竟改了 engine
隨著閑魚(yú)業(yè)務(wù)的不斷推進(jìn),engine 的升級(jí)的成本是我們必須要考慮的事情。能否不改 engine 實(shí)現(xiàn)同樣的功能是我們核心的述求(PS:我承認(rèn)我們是貪心的)。
通道性能還有優(yōu)化空間
外接紋理的方案需要通過(guò)橋的方式跟 Native 的能力做通信。這里包括圖片請(qǐng)求的傳遞和圖片加載各種狀態(tài)的同步。特別是在 listview 快速滑動(dòng)的時(shí)候,通過(guò)橋發(fā)送的數(shù)據(jù)量還是可觀的。當(dāng)前方案每個(gè)圖片加載時(shí)都會(huì)單獨(dú)進(jìn)行橋的調(diào)用。在圖片數(shù)量比較多的情況下,這顯然會(huì)是一個(gè)瓶頸。
耦合過(guò)多
在實(shí)現(xiàn)圖片回收方案的時(shí)候,目前方案需要棧提供是否在棧底層的接口。這里就產(chǎn)生方案耦合,很難抽象出一個(gè)獨(dú)立干凈的圖片加載方案。
Clean & Efficient
時(shí)間來(lái)到了 2020 年,隨著對(duì) Flutter 基礎(chǔ)能力理解的逐步深入,我們實(shí)現(xiàn)了一個(gè)整體方案更優(yōu)的圖片框架。
無(wú)侵入外接紋理
外接紋理可以不用修改 engine 么?答案是肯定的。
其實(shí) Flutter 是提供了官方的外接紋理方案的。
??
??
而且 Native 操作的 texture 和 Flutter 側(cè)顯示的 texture 在底層是同一對(duì)象,并沒(méi)有產(chǎn)生額外的數(shù)據(jù) copy。這樣就保證了紋理共享的足夠高效。那為什么閑魚(yú)之前會(huì)單獨(dú)基于 shared-Context 自己實(shí)現(xiàn)一套呢?1.12 版本之前,官方 Ios 的外接紋理方案有性能問(wèn)題。每次渲染的過(guò)程中(不管紋理是否有更新)都會(huì)頻繁獲取 CVPixelBuffer,造成不必要的性能損耗(過(guò)程有加鎖損耗)。該問(wèn)題已經(jīng)在 1.12 版本中修復(fù)(官方 commit 地址),這樣官方方案也足夠滿足需求。在這樣的背景下,我們重新啟用官方方案來(lái)實(shí)現(xiàn)外接紋理功能。
獨(dú)立的內(nèi)存優(yōu)化
之前提到過(guò),老版本的基于頁(yè)面棧的圖片資源回收需要強(qiáng)依賴棧功能的接口。一方面產(chǎn)生了不必要的依賴,更重要的是,整體方案無(wú)法獨(dú)立成通用方案。為了解決這個(gè)問(wèn)題,我們對(duì) Flutter 底層進(jìn)行了深入的研究。我們發(fā)現(xiàn) Flutter 的 layer 層可以穩(wěn)定感知到頁(yè)面棧的變化。
??
??
然后每個(gè)頁(yè)面通過(guò) context 獲取的 router 對(duì)象作為標(biāo)識(shí)對(duì)一個(gè)頁(yè)面中的所有的圖片對(duì)象進(jìn)行重新組織。所有獲取到同一個(gè) router 對(duì)象的標(biāo)識(shí)成同一個(gè)頁(yè)面。這樣就能以頁(yè)面為單位對(duì)所有的圖片進(jìn)行管理。整體上通過(guò) LRU 的算法來(lái)模擬虛擬頁(yè)面棧結(jié)構(gòu)。這樣就能對(duì)棧底頁(yè)面的圖片資源實(shí)現(xiàn)回收了。
其他優(yōu)化
通道的高度復(fù)用
首先我們以一幀為單位對(duì)這一幀中的圖片請(qǐng)求進(jìn)行聚合,然后在一次通道請(qǐng)求中傳遞給 Native 的圖片加載框架。這樣能避免頻繁的橋調(diào)用。特別在快速滾動(dòng)等場(chǎng)景下優(yōu)化效果尤為明顯。
??
??
高效的紋理復(fù)用
使用外接紋理進(jìn)行圖片加載以后,我們發(fā)現(xiàn)復(fù)用紋理可以進(jìn)一步提升性能。舉一個(gè)簡(jiǎn)單的場(chǎng)景。我們知道電商場(chǎng)景中,商品展示經(jīng)常會(huì)有標(biāo)簽,打底圖這樣的圖片。這類圖片往往在不同的商品上會(huì)出現(xiàn)大量重復(fù)。這時(shí)候,可以將已經(jīng)渲染好的紋理,直接復(fù)用給不同的顯示組件。這樣能進(jìn)一步優(yōu)化 GPU 內(nèi)存的占用,避免重復(fù)創(chuàng)建。為了精確對(duì)紋理進(jìn)行管理,我們引入了引用計(jì)數(shù)的算法來(lái)管理紋理的復(fù)用。通過(guò)這些方案,我們實(shí)現(xiàn)了紋理跨頁(yè)面高效復(fù)用。
??
??
此外,我們將紋理和請(qǐng)求的映射關(guān)系移動(dòng)到了 Flutter 側(cè)。這樣能在最短路徑上完成紋理的復(fù)用,進(jìn)一步減少了橋的通信的壓力。
整體架構(gòu)
??
??
優(yōu)化效果
由于最新的版本目前還在灰度,具體數(shù)據(jù)后續(xù)會(huì)寫(xiě)文跟大家詳細(xì)介紹。下屬數(shù)據(jù)主要以方案二為主。
內(nèi)存優(yōu)化
通過(guò)打通 Native,相比于首次上線版本,在顯示效果不變的情況下,Ios 的 abort 率降低 25%,用戶體驗(yàn)明顯提升。
多頁(yè)面棧內(nèi)存優(yōu)化
多頁(yè)面棧的內(nèi)存優(yōu)化,在多頁(yè)面場(chǎng)景下對(duì)內(nèi)存優(yōu)化作用明顯。我們做了一個(gè)極限試驗(yàn)效果如下(測(cè)試環(huán)境,非閑魚(yú) APP):
??
??
可見(jiàn)多頁(yè)面棧的優(yōu)化,可以將多 Flutter 頁(yè)面的內(nèi)存占用控制得更好。
包大小減少
通過(guò)接入外接紋理,本地資源得到了更好的復(fù)用,包大小降低 1M。早期閑魚(yú)接入 Flutter,會(huì)以改造現(xiàn)有頁(yè)面為切入點(diǎn)。資源重復(fù)情況比較嚴(yán)重,但是隨著閑魚(yú) Flutter 新業(yè)務(wù)越來(lái)越多。Flutter 和 Native 的重復(fù)資源越來(lái)越少。外接紋理對(duì)包大小的影響已經(jīng)逐步變?nèi)酢?/p>
后續(xù)計(jì)劃
這是一場(chǎng)沒(méi)有盡頭的旅行,我們對(duì)閑魚(yú)圖片的優(yōu)化還會(huì)持續(xù)。特別是我們最新的方案,受限篇幅,本文只是做了初步介紹。更多技術(shù)細(xì)節(jié),包括測(cè)試數(shù)據(jù),我們隨后還會(huì)專門寫(xiě)文繼續(xù)給大家做介紹。方案完善以后,我們也會(huì)逐步開(kāi)源。
【本文為專欄作者“阿里巴巴官方技術(shù)”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】
??戳這里,看該作者更多好文??
當(dāng)前名稱:從原生到黑科技:閑魚(yú)Flutter圖片優(yōu)化經(jīng)歷了什么?
文章轉(zhuǎn)載:http://m.fisionsoft.com.cn/article/cooipos.html


咨詢
建站咨詢
