重度使用 Flutter 研發模式下的頁面效能最佳化實踐

一 Flutter頁面效能最佳化的挑戰

淘寶特價版是集團內應用Flutter技術場景比較多,且使用者量一億人以上的應用了。目前我們首頁、詳情、店鋪、我的,看看短影片,及評價,設定等二級頁面都在用Flutter技術搭建。

我們發現使用Flutter經常會遇到效能問題。因為Flutter嚴格意義上僅是一種“UI渲染框架”,它透過非同步來來實現子執行緒渲染UI,並且透過Skia保證兩端“渲染的一致性”。但子執行緒執行並渲染,且動態庫打包這些策略並非“一片通吃”,會導致損耗頁面開啟效能及可互動時長的增加。試想,app啟動時動態庫載入的dynamic binding(影響啟動時長),頁面啟動時主執行緒啟動了頁面,但ui渲染卻需要等待Flutter的子執行緒執行並渲染,低端機上頁面會短暫白屏(頁面未渲染影響可互動時長,雖然fps欺騙性的提高了)。

Flutter有效能瓶頸,但重度使用Flutter研發的我們是如何做到效能最佳化的?本篇會就基礎鏈路各Flutter頁面的最佳化策略,分享我們的實踐!

二 模組級混合——首頁的最佳化實踐

首頁最開始是全部採用Flutter+DXFlutter(面向Flutter的UI動態化框架)實現,業務實現一切ok,但發版的時候測試同學發現首頁的啟動效能突然比上個版本跌了1s。這個問題是必然的,因為Flutter是動態庫要延遲載入和繫結,同時DXFlutter大量的模板邏輯也會極大消耗效能。

問題很難解,當時我們就是否繼續全部Flutter,但最佳化引擎和DXFlutter,還是回退為Native實現產生了分歧。如果回退Native,則首頁及搜尋的分類tab技術方案都要切回native,成本巨大。最終,我們根據特價版的現狀及經驗,拍板採用:app啟動時首頁推薦等採用Native實現,但搜尋實現的其他tab分類繼續採用Flutter,如此不會對搜尋業務研發模式產生影響,又能避免Flutter帶來啟動效能的損耗。

但是該方案會遇到一個技術挑戰:我們使用的Flutter混合棧FlutterBoost僅支援頁面級混合,還不支援頁面內模組級混合。因為Flutter是單window的設計策略,如果模組級混合必然會遇到Flutter頁面生命週期的管理及渲染視窗尺寸的一致性問題。

前一個問題是因Flutter是單引擎引起的。當模組切換的時候,新模組顯示需要連線引擎重新觸發渲染,否則頁面會空白或不可互動。這個在FlutterBoost中早就做了。

後一個問題是單window,Flutter頁面上彈框Native頁面都會導致頁面佈局問題。

但模組級混合明顯是技術可行的,我們在AliFlutter正物、來一等同學的參與下,很快就開發出了可以容納Native、Flutter甚至其他型別如WebView的模組級混合容器。如下圖:

我們透過一個FlutterWrapperVC,基於FlutterBoost解決單引擎渲染問題,根據模組可見性切換FlutterEngine和虛擬機器,保證當前可見的模組能正常渲染,並執行底層的Flutter程式碼;然後透過Window大小強制修正解決了單window問題,解決佈局問題。最後我們也考慮到了模組複用性,將這個能力元件化,並封裝到這裡:LTaoUIKit。

pod LTaoUIKit ‘0。0。3。89’

基於這個方案之後,這麼改造後,首頁的啟動實踐至少提升1s。更重要的是首頁的研發就如圍棋裡建了兩眼,活出了一片。後面推薦頁基於native dxcontainer實現後,就直接可以複用之前淘寶等成熟app的最佳化經驗。

後面我們最佳化RT,DX模板打底,圖示本地化,圖片壓縮多管齊下,首頁啟動效能穩穩提升。

同時後面對首頁的啟動鏈路做了分步治理,並做了較為體系化的治理建設:

三 資料預取與FFI——純Flutter頁面的最佳化實踐

上面探討模組混合Flutter頁效能最佳化,本節則講整個頁面均是Flutter實現的最佳化策略,這應該也是大部分Flutter開發者常遇到的。透過這種方式,以特價版詳情頁為例,我們在之前的最佳化結果上又再優化了100多ms,以下是具體資料:

Android(vivo y67): 80~100ms

iOS(iPhone6): 120~200ms

首先,Flutter頁面會遇到哪些效能瓶頸?從Flutter機制看,它其實是個能較好解決“多端一致問題”的“UI渲染框架”,雖然提供了透過bridge訪問native,但Flutter bridge效能極差,涉及到了執行緒切換,字元編碼等問題(後面會講)。所以,我們使用Flutter應該避免直接透過Flutter來解決IO等資源訪問的工作,而且這些工作應儘量放在native側。

顯然app一張頁面的啟動,往往涉及到請求服務端準備渲染資料。同時,從路由跳轉,到透過Engine初始化Native的VC或者activity到Engine構造Rasterrizer還涉及Engine層面的執行緒等待。這些時間其實可以做不少事情,我們可以將服務端資料請求放在這個階段,這就是我們希望做的“資料預取”。

其實“資料預取”在淘寶上已經大規模用了,但在Flutter頁面上所顯示的優越性則會更強,因為Flutter頁的多執行緒切換太多了,很容易就掉入channel bridge的陷阱。如我們詳情頁最開始也用了資料預取,但資料預取呼叫是從Flutter發起的,效能似乎有提升,但並不那麼明顯。為什麼?以下是從Flutter發起一個mtop請求的流程圖,可以從其中一窺究竟:

上圖UI執行緒是指Flutter的ui執行緒,並非系統主執行緒。請求從“開始”處開始,兜兜轉轉,要經歷2次執行緒切換等待,多次的資料encode和decode,造成的效能損耗還是蠻多的,分析如下:

首先,一旦某個情形下裝置cpu緊張,則Flutter的請求/資料返回會遲遲無法送達到native或者Flutter。

其次,當資料量大的情形下,資料encode和decode也會耗費更多時間。

最後,頁面開啟經常遇到Engine和Native之間誰先啟動的問題。比如VC啟動了,這個時候並不能馬上就給Flutter傳送message,因為Flutter Engine可能還沒有準備好,此時message丟失,雙方都不知道。這個問題在FlutterBoost中遇到不少。

我們最終透過以下策略來解決:

資料預取在Native側發起,在頁面路由構造Native VC/Activity時,就馬上發起mtop等請求。

mtop返回的資料優先不透過channel bridge返回給Flutter層,而是透過ffi機制供Flutter直接讀取。

上面native側必須暫存資料,但為避免長久引用造成資源洩漏,採用LRU策略快取資料(iOS不能用NSCache,猜猜為什麼)。

考慮到有些頁面資料可以持久化儲存,供下次使用,我們構造了多級快取策略。

將上面能力全部元件化,以供其他業務複用。

詳細的設計如下:

首先,基於ffi和native側資料預取,最佳化後的資料請求鏈路如下:

右上角之所以還有channel bridge是為解決Native請求返回慢於Flutter頁面渲染的情形下的資料重新整理。

其次,我們構建了多級快取策略和快取失效及複用策略,以支援部分頁面資料的持久化複用提升首屏渲染效能:

以快取複用策略為例,我們支援以下策略:

激進型:第二次請求直接使用上一次快取資料,不再馬上重新整理資料,待快取自然過期後重新整理。該策略適用於頁面資料不常變的情形。

正常型:第二次請求可使用上一次快取,但仍需請求並馬上重新整理資料。該策略比較普適,適合資料變化不頻繁的情形。

保守型:第二次請求不可使用上一次快取,需請求最新資料。適合強實時性的頁面資料渲染。

最後,我們將這些能力做了封裝,比如iOS側,我們以單獨的SDK整合:

pod LTPrefetch ‘1。0。1。19’

目前基礎鏈路如詳情,我的,店鋪,mini詳情等都採用了這個方案進行最佳化,啟動實踐均有了不錯的提升。

四 其他最佳化實踐

其他還有很多最佳化實踐。有些是淘系已經實踐過的,有些是特價版根據Flutter的特點有所改動的。這裡不詳細說了,僅就我們使用過的列個列表:

詳情從首頁截圖。

資源壓縮及本地預置:如首頁dx模板預置,Json壓縮及預置,圖片壓縮及預置。

資料提前非同步載入。如我的頁面資料,其實在使用者登陸的時候就會非同步載入並快取下來,然後透過上面的快取更新策略來更新。

最佳化服務端RT,精簡協議。

其他。

五 最後

其實我們的最佳化策略更多在上層應用上做了最佳化,UC那邊在Flutter Engine層面做了最佳化,後期可以考慮使用他們引擎,相信頁面開啟效能會更上一層樓。

同時上面的ffi及資料預取也可以做得更激進一些。如透過Dart2Native的方式,完全實現Json資料的encode和decode本地實現,容器訪問的本地實現,估計還能提升至少50ms的時間。

作者 | 餘玠

相關文章