重度使用Flutter研發(fā)模式下的頁面性能優(yōu)化實踐
一、Flutter頁面性能優(yōu)化的挑戰(zhàn)
淘寶特價版是集團(tuán)內(nèi)應(yīng)用Flutter技術(shù)場景比較多,且用戶量一億人以上的應(yīng)用了。目前我們首頁、詳情、店鋪、我的,看看短視頻,及評價,設(shè)置等二級頁面都在用Flutter技術(shù)搭建。
我們發(fā)現(xiàn)使用Flutter經(jīng)常會遇到性能問題。因為Flutter嚴(yán)格意義上僅是一種“UI渲染框架”,它通過異步來來實現(xiàn)子線程渲染UI,并且通過Skia保證兩端“渲染的一致性”。但子線程執(zhí)行并渲染,且動態(tài)庫打包這些策略并非“一片通吃”,會導(dǎo)致?lián)p耗頁面打開性能及可交互時長的增加。試想,app啟動時動態(tài)庫加載的dynamic binding(影響啟動時長),頁面啟動時主線程啟動了頁面,但ui渲染卻需要等待Flutter的子線程執(zhí)行并渲染,低端機(jī)上頁面會短暫白屏(頁面未渲染影響可交互時長,雖然fps欺騙性的提高了)。
Flutter有性能瓶頸,但重度使用Flutter研發(fā)的我們是如何做到性能優(yōu)化的?本篇會就基礎(chǔ)鏈路各Flutter頁面的優(yōu)化策略,分享我們的實踐!
二、模塊級混合——首頁的優(yōu)化實踐
首頁最開始是全部采用Flutter+DXFlutter(面向Flutter的UI動態(tài)化框架)實現(xiàn),業(yè)務(wù)實現(xiàn)一切ok,但發(fā)版的時候測試同學(xué)發(fā)現(xiàn)首頁的啟動性能突然比上個版本跌了1s。這個問題是必然的,因為Flutter是動態(tài)庫要延遲加載和綁定,同時DXFlutter大量的模板邏輯也會極大消耗性能。
問題很難解,當(dāng)時我們就是否繼續(xù)全部Flutter,但優(yōu)化引擎和DXFlutter,還是回退為Native實現(xiàn)產(chǎn)生了分歧。如果回退Native,則首頁及搜索的分類tab技術(shù)方案都要切回native,成本巨大。最終,我們根據(jù)特價版的現(xiàn)狀及經(jīng)驗,拍板采用:app啟動時首頁推薦等采用Native實現(xiàn),但搜索實現(xiàn)的其他tab分類繼續(xù)采用Flutter,如此不會對搜索業(yè)務(wù)研發(fā)模式產(chǎn)生影響,又能避免Flutter帶來啟動性能的損耗。
但是該方案會遇到一個技術(shù)挑戰(zhàn):我們使用的Flutter混合棧FlutterBoost僅支持頁面級混合,還不支持頁面內(nèi)模塊級混合。因為Flutter是單window的設(shè)計策略,如果模塊級混合必然會遇到Flutter頁面生命周期的管理及渲染窗口尺寸的一致性問題。
前一個問題是因Flutter是單引擎引起的。當(dāng)模塊切換的時候,新模塊顯示需要連接引擎重新觸發(fā)渲染,否則頁面會空白或不可交互。這個在FlutterBoost中早就做了。
后一個問題是單window,F(xiàn)lutter頁面上彈框Native頁面都會導(dǎo)致頁面布局問題。
但模塊級混合明顯是技術(shù)可行的,我們在AliFlutter正物、來一等同學(xué)的參與下,很快就開發(fā)出了可以容納Native、Flutter甚至其他類型如WebView的模塊級混合容器。如下圖:
我們通過一個FlutterWrapperVC,基于FlutterBoost解決單引擎渲染問題,根據(jù)模塊可見性切換FlutterEngine和虛擬機(jī),保證當(dāng)前可見的模塊能正常渲染,并執(zhí)行底層的Flutter代碼;然后通過Window大小強(qiáng)制修正解決了單window問題,解決布局問題。最后我們也考慮到了模塊復(fù)用性,將這個能力組件化,并封裝到這里:LTaoUIKit。
- pod LTaoUIKit '0.0.3.89'
基于這個方案之后,這么改造后,首頁的啟動實踐至少提升1s。更重要的是首頁的研發(fā)就如圍棋里建了兩眼,活出了一片。后面推薦頁基于native dxcontainer實現(xiàn)后,就直接可以復(fù)用之前淘寶等成熟app的優(yōu)化經(jīng)驗。
后面我們優(yōu)化RT,DX模板打底,圖標(biāo)本地化,圖片壓縮多管其下,首頁啟動性能穩(wěn)穩(wěn)提升。
同事后面對首頁的啟動鏈路做了分步治理,并做了較為體系化的治理建設(shè):
三、數(shù)據(jù)預(yù)取與FFI——純Flutter頁面的優(yōu)化實踐
上面探討模塊混合Flutter頁性能優(yōu)化,本節(jié)則講整個頁面均是Flutter實現(xiàn)的優(yōu)化策略,這應(yīng)該也是大部分Flutter開發(fā)者常遇到的。通過這種方式,以特價版詳情頁為例,我們在之前的優(yōu)化結(jié)果上又再優(yōu)化了100多ms,以下是具體數(shù)據(jù):
- Android(vivo y67): 80~100ms
- iOS(iPhone6): 120~200ms
首先,F(xiàn)lutter頁面會遇到哪些性能瓶頸?從Flutter機(jī)制看,他其實是個能較好解決“多端一致問題”的“UI渲染框架”,雖然提供了通過bridge訪問native,但Flutter bridge性能極差,涉及到了線程切換,字符編碼等問題(后面會講)。所以,我們使用Flutter應(yīng)該避免直接通過Flutter來解決IO等資源訪問的工作,而且這些工作應(yīng)盡量放在native側(cè)。
顯然app一張頁面的啟動,往往涉及到請求服務(wù)端準(zhǔn)備渲染數(shù)據(jù)。同時,從路由跳轉(zhuǎn),到通過Engine初始化Native的VC或者activity到Engine構(gòu)造Rasterrizer還涉及Engine層面的線程等待。這些時間其實可以做不少事情,我們可以將服務(wù)端數(shù)據(jù)請求放在這個階段,這就是我們希望做的“數(shù)據(jù)預(yù)取”。
其實“數(shù)據(jù)預(yù)取”在淘寶上已經(jīng)大規(guī)模用了,但在Flutter頁面上所顯示的優(yōu)越性則會更強(qiáng),因為Flutter頁的多線程切換太多了,很容易就掉入channel bridge的陷阱。如我們詳情頁最開始也用了數(shù)據(jù)預(yù)取,但數(shù)據(jù)預(yù)取調(diào)用是從Flutter發(fā)起的,性能似乎有提升,但并不那么明顯。為什么?以下是從Flutter發(fā)起一個mtop請求的流程圖,可以從其中一窺究竟:
上圖UI線程是指Flutter的ui線程,并非系統(tǒng)主線程。請求從“開始”處開始,兜兜轉(zhuǎn)轉(zhuǎn),要經(jīng)歷2次線程切換等待,多次的數(shù)據(jù)encode和decode,造成的性能損耗還是蠻多的,分析如下:
- 首先,一旦某個情形下設(shè)備cpu緊張,則Flutter的請求/數(shù)據(jù)返回會遲遲無法送達(dá)到native或者Flutter。
- 其次,當(dāng)數(shù)據(jù)量大的情形下,數(shù)據(jù)encode和decode也會耗費(fèi)更多時間。
- 最后,頁面打開經(jīng)常遇到Engine和Native之間誰先啟動的問題。比如VC啟動了,這個時候并不能馬上就給Flutter發(fā)送message,因為Flutter Engine可能還沒有準(zhǔn)備好,此時message丟失,雙方都不知道。這個問題在FlutterBoost中遇到不少。
我們最終通過以下策略來解決:
- 數(shù)據(jù)預(yù)取在Native側(cè)發(fā)起,在頁面路由構(gòu)造Native VC/Activity時,就馬上發(fā)起mtop等請求。
- mtop返回的數(shù)據(jù)優(yōu)先不通過channel bridge返回給Flutter層,而是通過ffi機(jī)制供Flutter直接讀取。
- 上面native側(cè)必須暫存數(shù)據(jù),但為避免長久引用造成資源泄漏,采用LRU策略緩存數(shù)據(jù)(iOS不能用NSCache,猜猜為什么)。
- 考慮到有些頁面數(shù)據(jù)可以持久化存儲,供下次使用,我們構(gòu)造了多級緩存策略。
- 將上面能力全部組件化,以供其他業(yè)務(wù)復(fù)用。
詳細(xì)的設(shè)計如下:
首先,基于ffi和native側(cè)數(shù)據(jù)預(yù)取,優(yōu)化后的數(shù)據(jù)請求鏈路如下:
右上角之所以還有channel bridge是為解決Native請求返回慢于Flutter頁面渲染的情形下的數(shù)據(jù)刷新。
其次,我們構(gòu)建了多級緩存策略和緩存失效及復(fù)用策略,以支持部分頁面數(shù)據(jù)的持久化復(fù)用提升首屏渲染性能:
以緩存復(fù)用策略為例,我們支持以下策略:
- 激進(jìn)型:第二次請求直接使用上一次緩存數(shù)據(jù),不再馬上刷新數(shù)據(jù),待緩存自然過期后刷新。該策略適用于頁面數(shù)據(jù)不常變的情形。
- 正常型:第二次請求可使用上一次緩存,但仍需請求并馬上刷新數(shù)據(jù)。該策略比較普適,適合數(shù)據(jù)變化不頻繁的情形。
- 保守型:第二次請求不可使用上一次緩存,需請求最新數(shù)據(jù)。適合強(qiáng)實時性的頁面數(shù)據(jù)渲染。
最后,我們將這些能力做了封裝,比如iOS側(cè),我們以單獨的SDK集成:
- pod LTPrefetch '1.0.1.19'
目前基礎(chǔ)鏈路如詳情,我的,店鋪,mini詳情等都采用了這個方案進(jìn)行優(yōu)化,啟動實踐均有了不錯的提升。
四、其他優(yōu)化實踐
其他還有很多優(yōu)化實踐。有些是淘系已經(jīng)實踐過的,有些是特價版根據(jù)Flutter的特點有所改動的。這里不詳細(xì)說了,僅就我們使用過的列個列表:
- 詳情從首頁借圖。
- 資源壓縮及本地預(yù)置:如首頁dx模板預(yù)置,Json壓縮及預(yù)置,圖片壓縮及預(yù)置。
- 數(shù)據(jù)提前異步加載。如我的頁面數(shù)據(jù),其實在用戶登陸的時候就會異步加載并緩存下來,然后通過上面的緩存更新策略來更新。
- 優(yōu)化服務(wù)端RT,精簡協(xié)議。
其他。
五、最后
其實我們的優(yōu)化策略更多在上層應(yīng)用上做了優(yōu)化,UC那邊在Flutter Engine層面做了優(yōu)化,后期可以考慮使用他們引擎,相信頁面打開性能會更上一層樓。
同時上面的ffi及數(shù)據(jù)預(yù)取也可以做的更激進(jìn)一些。如通過Dart2Native的方式,完全實現(xiàn)Json數(shù)據(jù)的encode和decode本地實現(xiàn),容器訪問的本地實現(xiàn),估計還能提升至少50ms的時間。