作者:閑魚技術——非歌
背景
目前閑魚的主要業務(搜尋、詳情等)都由Flutter來承載,雖然Flutter是高性能的跨端技術方案,但是随着業務快速疊代和業務複雜度不斷提升,應用的性能表現卻不盡如人意,主要展現在啟動慢、頁面加載慢,頁面滑動操作不流暢、卡頓等,非常影響使用者使用體驗。
為此我們啟動了閑魚體驗更新的項目,從使用者使用閑魚App的操作路徑的關鍵節點出發,系統性的對應用做了優化工作,包括安裝包瘦身、加快啟動速度、頁面加載時長優化、流暢度優化,最終提升了使用者使用體驗。
本文主要介紹閑魚在Flutter側的性能體驗優化工作,主要分為三個部分:
- 流暢度優化
- 搜尋結果頁加載優化
- 詳情頁加載優化
優化方案如下圖:

先從最棘手的Flutter流暢度問題介紹。從線上資料和實際使用體感來看,搜尋頁、商品詳情頁的滑動流暢度都不盡如意,輿情中也有很多關于卡頓的回報。
接下來分3個方面來介紹流暢度優化:
- Flutter卡頓定位工具建設
- 長清單loadmore優化
- 滾動加載小圖
Flutter采用資料驅動的方式建構UI,官方的
Flutter性能最佳實踐指出
- 避免在 build() 方法中進行重複且耗時的工作,因為當父 Widget 重建時,子 Wdiget 的 build() 方法會被頻繁地調用
- 避免在一個超長的 build() 方法中傳回一個過于龐大的 Widget。把他們分拆成不同的 Widget,并進行封裝
但是随着業務快速疊代,開發者往往會疏忽大意,寫出低性能的代碼。理想情況是,通過性能分析工具自動定位到問題代碼,及時修改。
Flutter官方提供了Devtools性能分析工具,它的timeline界面可以逐幀分析應用的UI性能,cpu profiler界面來抓取卡頓過程中的耗時操作。
在研發階段,我們可以使用這個工具來分析本地可以複現的卡頓問題。
但是,Devtools的局限性在于:
- 隻能用于在出現卡頓問題後去分析,無法做到監控卡頓問題。
- 隻能用于線下排查,對于線上回報卡頓問題,因為沒有足夠的場景,也無能為力。
為此我們需要建設一個Flutter卡頓工具,用于線上上線下監控定位造成卡頓的耗時函數。
實作思路:我們知道release模式下的Dart代碼是基于AOT編譯的,業務代碼和sdk都會編譯成平台相關的機器碼,是以我們是可以通過信号機制抓取native的堆棧,再通過符号化還原的方式來擷取flutter堆棧。
我們通過卡頓工具定位排查出兩類造成流暢度低的問題:耗時函數和過度渲染問題。
定位耗時函數
對于耗時函數導緻的卡頓問題,可以通過檢視堆棧定位到耗時函數。
從調用棧可以看出channel的調用時資料序列化 和 反序列化比較耗時。
通過檢視FxImage的代碼,了解到resumeImage對應的native實作已經為空,flutter側其實可以省去這一次channel調用。
定位過度渲染
在自動化階段上報的資料有大量是如下所示渲染階段的堆棧,無法定位到業務代碼。
這是由于Flutter的重新整理機制是中心化、異步的渲染機制,
業務層需要重新整理界面元素,framework層會先把需要更新的元素标髒,然後等下一次引擎側的渲染回調,在這次渲染回調中,對之前收集到的髒元素統一做更新。
這樣的機制導緻在抓取渲染階段的堆棧有大量的Element.rebuild、update方法,都是flutter framework的函數調用棧,而缺失了業務側代碼,隻看這些堆棧并不能幫我們定位到卡頓問題。
我們的思路是:通過增加渲染過程中的髒element資訊,
根據标髒的element的複雜程度(我們根據元素的深度、長度、子節點數來計算複雜度)
可以幫助我們檢測出是否有代碼不規範導緻重新整理範圍過大的問題。
通過這個方案,我們定位到詳情頁快速提問元件過度渲染問題:
檢視髒element資訊可以看到,打髒的元素複雜度較高。
優化方案:
提高build效率,setState重新整理資料下沉到葉子節點,将每個标簽抽離成LabelStatefulWidget,隻做局部重新整理。
通過卡頓工具,我們發現搜尋結果頁清單滑動加載更多過程中會對整個清單容器打髒重建,造成比較嚴重的流暢度問題。
經過分析發現,因為加載更多資料傳回後,追加清單資料後會調用setState,打髒容器widget。
并且由于我們有做預加載,在滑到底部前幾屏時就去觸發加載更多的邏輯,導緻打髒重建widget的頻率更高。
一期我們從業務層入手,預加載更多的資料回來并不去調用setState,而先是儲存到記憶體,等滑到底部再去調用setState,重建清單容器。後面我們從清單容器底層去優化,loadmore時不會打髒重建整個清單容器,容器加載和滑動過程有較大的優化,提升體驗比較明顯。
優化前如下圖,(紅色區域表示打髒的widget)可以看到重新整理了整個容器。
優化後,僅對新增的卡片做重新整理
feeds流卡片包含大量的圖檔,在快速滑動過程中,加載大量圖檔對于記憶體和IO都是比較大的考驗,影響流暢度,在低端機上尤其明顯。
優化思路:在快速滾動過程中,隻加載尺寸較小的模糊圖,等到滾動停止後再漸進式的展示原圖,并且在超出螢幕區域不加載原圖,優化上屏體驗。
效果如圖:(為了示範效果,這裡的用的是縮小5倍的小圖)
小結
在經過優化之後,閑魚詳情頁和搜尋頁流暢度 FPS 提升了 3 個點,低端機大卡頓次數降低一半,中高端機型上流暢度提升到 57 或以上,大卡頓次數接近 0。
詳情頁線上高可用 fps 資料如下:
線上低端機 fps 曲線,橫軸表示幀率區間,綠色為優化版本。曲線分布越靠右,流暢度越好。
線上高端機 fps 曲線。綠色為優化版本
搜尋頁線上高可用 fps 資料如下:
線上低端機 fps 曲線。綠色為優化版本
在優化流暢度問題之後,再來看下對于頁面加載需要做哪些優化。
在優化之前,從搜尋關鍵詞到搜尋結果展示過程中有較長loading。對于頁面的加載速度優化,我們更多的從業務流程開始去找突破口,搜尋結果頁的打開過程如下:
搜尋結果頁由Flutter實作,但它是從Native頁面點選打開,在混合棧的背景下導緻路由攔截到打開容器這一步有一定耗時。
我們可以通過 URL 攜帶預取資訊,在 Native 進行跳轉導航時同時進行異步并行的資料預取,可以減少頁面打開的耗時。
同時因為搜尋頁面的請求RT相對比較高,一般頁面進來了,還仍然在等待網絡請求回來,是以如果在網絡請求回來的時候再去做模闆的預加載,大機率會命中。
優化之後的流程如下:
通過一定的并行手段,采用資料預取、模闆預加載的方案,我們在Android低端機上将搜尋結果頁加載時長優化300ms。
同時在資料請求時展示骨架屏動畫(lottie實作)代替小黃魚loading,帶給使用者更好的使用體驗。
優化前:
優化後:
對于詳情頁的加載優化,我們主要通過下面3個手段做優化:
- FlutterBoost優化
- 資料透傳
- 轉場動畫
閑魚目前還是Native+Flutter的混合開發模式,通過FlutterBoost來處理混合棧頁面的映射和跳轉。
在FlutterBoost的open進行中,會通過startActivity()打開一個新的容器,而詳情頁的跳轉場景中,大部分都是從Flutter頁面跳轉過來的,其實可以複用之前打開過的容器。
針對這樣的應用場景,我們在FlutterBoost增加了一個新的特性,在Flutter頁面打開一個新的Flutter頁面時,可以選擇兩個Flutter頁面共用一個Flutter容器(Activity、ViewController), 以加快頁面打開速度、減少記憶體消耗,并且支援了Flutter實作頁面切換的動畫。
在搜尋結果頁跳詳情和猜你喜歡跳詳情的場景中,詳情的部分資料已經可以通過上一個接口拿到,我們可以把這部分資料透傳帶到詳情頁,在請求詳細資料的過程中,先展示簡單的内容,比如主圖、标題、價格,等詳細資料回來後再更新詳情頁,帶來直出的體驗效果。
在前面資料透傳的基礎上,我們又通過轉場動畫,實作沉浸式頁面的切換的效果,進一步地提升使用者使用體驗。
實作思路:在路由導航過程中通過繼承ModalRoute接管buildPage過程,對簡化版詳情頁AnimateDetailPage做動畫處理,監聽動畫狀态,當動畫完成後,再把詳情頁DetailPage挂到widget樹上。
優化前:
總結展望
總結下來,閑魚Flutter的性能體驗更新中,在技術細節優化方面,我們對主要頁面加載時長、流暢度做了優化,除了針對業務流程上的優化之外,我們也沉澱了一些通用優化能力,比如資料預取、widget分幀上屏、長清單加載更多優化、長清單局部重新整理能力、長清單滾動內插補點器算法優化。在視覺優化方面,突出年輕化的主題,設計使用了大量的微動畫元素,Flutter Lottie動畫使用在各個頁面随處可見,例如:釋出頁、搜尋結果加載頁、以及各個加載重新整理動畫。在優化之後,閑魚App操作變得更加流暢如絲般順滑。
未來,我們還需要對Flutter引擎啟動有更多的探索和思考,尤其是在首頁切換到Flutter之後,對Flutter啟動優化的工作是必須的。
另外,由于業務的快速疊代,前期的優化工作到後面很容易惡化。如何在業務快速變化的同時,既滿足效率又保證性能,是接下來需要着手解決的問題,比如在代碼內建合入前通過性能卡口,将性能不達标的代碼打回并給出優化建議;将性能優化的手段内置到容器架構這一層。