
作者 | 蕭逸
來源 |
阿裡技術公衆号我在《Flutter vs Chromium 動畫渲染的對比分析》一文中對 Flutter 和 Web (Chromium) 的各種動畫的理論性能優劣進行了分析,其中一個主要結論是,由于慣性滾動處理機制和光栅化機制的不同,Web (Chromium) 的慣性滾動動畫性能理論上要遠遠優于 Flutter。而在一些已經上線的使用 Flutter 的業務中,業務方也持續給我們回報了這些業務在中低端 Android 手機上存在比較嚴重的慣性滾動性能問題:
- 業務 A 的頁面較為簡單,但是在低端手機上平均幀率在 40 ~ 50 之間,中端手機在 50 ~ 55 之間,低端機存在較為明顯的卡頓問題。
- 業務 B 的頁面比較複雜,業務邏輯也較為複雜,在低端手機上平均幀率更是低到最低 30 多幀(35 ~ 45 之間),中端手機也是在 50 左右,并且存在較為頻繁的長時間卡頓,低端機存在比較嚴重的卡頓問題,中端機也不太流暢。
而以我們長期的經驗資料,對于 Web 來說,即使在低端手機上,較為複雜的頁面慣性滾動幀率一般也在 50 以上,也較少長時間的卡頓,達到基本流暢的水準。并且剛好業務 B 有完全一樣的 Native 版本,它對比 Flutter 版本,幀率普遍高了 5 ~ 10 幀左右。
是以雖然我們沒有找到同一個頁面的三個不同版本進行嚴格的比對,但是基于上述的測試資料和我們長期的經驗,很容易得出結論是,在慣性滾動的性能上:
Web (Chromium) > Native (Android) > Flutter (Android)
我們在不同裝置上對上述業務頁面在慣性滾動過程中進行 trace 的抓取,結合 Flutter 的代碼對 trace 檔案進行分析,了解 Flutter 渲染流水線在慣性滾動過程中各個環節的排程,了解各個環節的可能耗時和哪些環節可能成為性能瓶頸。在分析的過程中,我們對 Flutter 的渲染機制有了更深入的了解,這篇文章就是對比 Web (Chromium) 和 Native (Android),對 Flutter 的渲染性能問題進行深入分析,特别是分析慣性滾動性能糟糕的原因。
說明:
這裡的幀率資料給的是一個範圍是因為我們使用了幾種不同的滾動速度進行測試,一般來說滾動速度越快,平均幀率就越低。
iPhone 基本不存在所謂的低端機,iOS 整體表現都還可以,不同實作的差異不大,是以我們目前主要的測試和優化都是在 Android 上進行。
一 寫在前面的結論
Flutter 有很多優點,特别是對于開發者來說,跨平台多端支援,豐富的 UI 元件庫和互動效果,聲明式 UI,React 的更新方式,Hot-reload 提高開發效率等等。雖然它在渲染性能上有不少缺陷,但是某種程度上,某些缺陷也是為了實作更高層次的設計目标而不得不承受的結果。
比如 Dart 語言原生對異步程式設計有良好的支援,應用開發者不需要去編寫複雜和容易出問題的多線程代碼,就可以有效地避免主線程長時間阻塞,編寫出性能良好的 UI。但是在慣性滾動這樣對性能要求非常高場景下,可能幾毫秒的阻塞都會導緻掉幀,缺少真正的多線程程式設計能力某種程度就變成了一種阻礙(Android 上你甚至可以在其它線程對 View 做非 UI 直接相關的操作)。
又比如使用 Immutable Widget 作為 UI Configuration 的設計是聲明式 UI 和 Hot-reload 的基礎,但還是會引入額外的開銷和喪失足夠的靈活性,應用無法直接控制 UI 元件的生命周期,無法直接控制 UI 元件的布局和繪制,這同樣妨礙了慣性滾動的性能優化。
我們是 UC 浏覽器核心團隊,主要負責 Chromium 和 Flutter 定制引擎的開發,我們的 Flutter 定制引擎以 Hummer 為代号。而對我們核心團隊來說,要做的就是在了解 Flutter 這些缺陷的同時,去研究是否存在有效地進行局部改進,或者從其它設計層面上對某些缺陷進行規避的方法,讓應用開發者既可以充分利用 Flutter 的優勢,又不用過于擔心它存在的問題。
總的來說下半年的工作目前看來還是取得了不錯的成果,也基本實作了讓 Flutter 慣性滾動性能對标原生的目标,下圖對業務 B 頁面的測試資料比較直覺地展示了我們優化的結果。
這裡電影幀是指 1000 / 24 約 40毫秒,2個電影幀 / min 是指連續滾動一分鐘内出現超過 80 毫秒卡頓的次數。
二 Web (Chromium) vs Flutter
Web (Chromium) 在慣性滾動上是有非常明顯的機制優勢的,這跟 Web 渲染引擎為了适應 Web 頁面的高複雜度,高不确定性有關,甚至某種程度上犧牲了一些渲染效果和其它動畫的渲染性能。Web (Chromium) 在慣性滾動上的優勢主要展現在以上兩方面:
- Chromium 有完整獨立的合成器驅動慣性滾動動畫的運作,有獨立的合成線程,慣性滾動動畫的更新和主線程更新 DOM 樹是不同步的,主線程運作 JS,Build & Layout 不會阻塞合成線程。
- Chromium 的分塊異步光栅化機制一方面減少了慣性滾動動畫過程中圖層的重複光栅化,另一方面光栅化不會阻塞合成線程的合成輸出。
對比 Web (Chromium),Flutter 在上述兩方面都存在比較明顯的劣勢:
Flutter 需要依賴于 Relayout 來驅動慣性滾動動畫,滾動容器内的元素在滾動過程中每一幀都需要 Relayout,不過這個一般耗時不高。Flutter 的無限長清單一般都采用 Lazy Build 的方式生成清單單元,當清單單元接近可見區域的時候,架構才調用應用提供的 Builder 生成清單單元的 Widget 樹并進行布局,新挂載的清單單元的 Build & Layout 通常耗時較長,在上述業務頁面中,可能耗費 10 毫秒以上,甚至幾十毫秒,特别是單幀内需要 Build 多個單元的情況,它們是導緻掉幀的主要原因。從上圖 trace 中我們很容易發現,正常速度滾動下,在 Flutter UI 線程 Frame 的階段,大部分情況下耗時不高,但是每幾幀就會出現一次耗時較長的 Frame,從上圖看耗時較長的 Frame 已經接近甚至超過一個 vsync 周期,滾動速度越快,出現耗時較長的 Frame 的頻率就越高,耗時也可能越長,它的耗時主要就來自新挂載清單單元的 Build & Layout。
Flutter 采用的以直接光栅化為主,間接光栅化為輔的同步光栅化機制,在合成輸出過程中進行光栅化,光栅化的耗時會直接影響動畫的性能。以實際業務為例子:
- 業務 A 的頁面較為簡單,光栅化耗時大部分在 3 ~ 5 毫秒之間,除了偶爾波動較高外,基本沒有造成阻塞,是以業務 A 的大部分掉幀都是 Flutter UI 線程的 Frame 耗時較高導緻;
- 業務 B 的頁面比較複雜,光栅化耗時大部分在 7 ~ 10 毫秒之間,偶爾波動超過 10 毫秒,是以部分掉幀主要是光栅化導緻的;
- 實際上我們還碰到一個頁面因為大範圍使用 Backdrop Filter 導緻光栅化耗時非常高,在低端機上隻有 10 ~ 20幀,不過這個可以在應用層面做一些優化來避免;
總的來說,Flutter 在慣性滾動過程的掉幀大部分都來自 Flutter UI 線程的阻塞,新挂載清單單元的 Build & Layout 耗時過長是主要原因。但是對于一些比較複雜的頁面,光栅化耗時較長也是一個導緻掉幀的原因。
我們在 Chromium 光栅化改造 - 混合光栅化 對比了不同光栅化機制在合成輸出過程中的光栅化+合成輸出的耗時,異步光栅化機制在這方面會有明顯的優勢,這也是我們在 U4 4.0 上采用了混合光栅化的原因。
Flutter 雖然提供了 KeepLive 機制用于避免清單單元滾出可見區域被回收,重新滾入可見區域又重新 Rebuild & Relayout,但是 KeepLive 機制并不适用于第一次顯
示的清單單元,并且在無限長清單場景很容易造成記憶體爆炸,适用場景不多。
三 Native (Android) vs Flutter
如果說 Web (Chromium) 因為機制的原因,慣性滾動性能明顯優于 Flutter,這個比較容易了解。那麼 Native (Android) 在機制上其實跟 Flutter 是比較類似的,為什麼它的性能也會優于 Flutter 呢?
Android 無限長清單一般使用 RecyclerView 實作,而 RecyclerView 支援子 View 樹級别的複用,使得新挂載的清單單元在 RecyclerView 的支援下,隻需要更新複用的子 View 樹的資料然後局部重排即可,耗時會大大少于 Flutter 整個清單單元的完整 Build & Layout,這是 Native (Android) 的無限長清單滾動更流暢的主要原因。不過除此以外,還有很多因素也會影響到 Flutter 的流暢度。
跟 Native 相比較,Flutter UI 線程會顯得更擁擠。Dart Isolate 的記憶體堆是隔離的,這點比較像 JavaScript,Isolate 之間的關系更像是多程序而不是多線程,導緻了一些多線程優化很難實作。應用通常要注冊多個回調來處理外部傳入的資料或者事件,這些回調接收外部資料或者事件,進行處理後更新内部資料(Model),通常這些回調都需要在 UI 線程執行。如果它們集中頻繁地發生,即使單次耗時不高,也很容易造成 Flutter UI 線程的阻塞,簡單說就是這些非 UI 任務的頻繁執行可能會導緻慣性滾動過程中 UI 任務的延遲,最終導緻掉幀,但是 Dart Isolate 的限制,對内部資料的更新又必須在 UI 線程上進行。
大部分應用都是局部使用 Flutter 開發,需要跟 Native 進行混用,這就導緻了應用很難使用 SurfaceView,而需要使用 TextureView。TextureView 會帶來一些額外的性能問題,除了更高的 GPU 開銷外,TextureView 的繪制機制也容易出現因為排程的不合理而導緻掉幀。
最後雖然 Android 和 Flutter 都是以直接光栅化為主,間接光栅化為輔的同步光栅化機制。但是将 Skia 作為 UI 的光栅化引擎,比起為 UI 專門定制的光栅化引擎可能還是存在一些缺陷:
- Skia GPU 光栅化的過程,涉及将通用的 2D 繪制指令轉換成一種接近 GPU 指令的内部形式,然後經過進一步優化後輸出最終的 GPU 指令,為 UI 專門定制的光栅化引擎理論上可以緩存第一步的結果,減少每一幀光栅化的耗時;
- Skia 作為一個通用的光栅化引擎,内部實作是線程無感的,而為 UI 專門定制的光栅化引擎可以更容易使用多線程來将光栅化過程中部分 CPU 工作并行化,比如生成字型或者路徑頂點等任務;
不過我們沒有實際去比較兩者的光栅化性能差異,這裡隻是一些理論分析。
四 應用層面優化和局限性
針對 Flutter 的慣性滾動性能問題,不少應用也嘗試了各種優化方案,比如閑魚的方案就比較有代表性。針對新挂載清單單元的 Build & Layout 耗時過長,閑魚的優化方案是 Element 複用和分幀渲染。
Element 複用其實就是參考 RecyclerView 的子 View 樹複用,理論上可以避免重新建立清單單元的 Element 樹和 RenderObject 樹的時間開銷。但是對比 Native,仍然需要重新建構 Widget 樹,并把新的 Widget 樹跟舊的 Element 樹進行綁定,并通過 Element 樹去更新 RenderObject 樹。而 Native 則可以直接複用 View 樹,然後更新若幹子 View 的資料即可,這部分的開銷仍然比優化過後的 Flutter 要低。
分幀渲染的思路是每個清單單元提供兩個版本的 Widget 樹,除了完整版,還有一個簡化版作為占位符。如果單幀内已經 Build 過一個完整版本的單元,在需要 Build 第二個單元時就隻 Build 簡化的版本,這樣可以避免單幀内多個清單單元的 Build & Layout 疊加在一起造成更大的阻塞。它的局限性是主要适用于清單單元較小,慣性滾動速度較快,一幀滾動會出現多個清單單元需要 Build & Layout 的場景,對避免更長時間的卡頓有一定作用。隻是這個優化 Android Native 看起來也完全能做,并且因為 Android 應用可以直接控制 View 是否參與布局和繪制,理論上做起來也更簡單,效果也更好。
總的來說,Flutter 應用的一些優化,要不是 Native 本來就已經實作,并且效果更好;就是 Native 同樣也可以實作,而且實作起來更簡單,效果也更好,并且其它一些影響 Flutter 性能的因素在應用層面無法進行優化。
是以 Flutter 應用優化起來可能比 Native 更麻煩,最後的效果也還是比不上 Native。一個優化後的 Flutter 應用,比起一個優化後的 Native 應用,在慣性滾動上還是會有一定性能差距。
五 我們的優化嘗試
作為一個引擎團隊,我們期望實作的目标是從架構和引擎層面對 Flutter 渲染流水線的方方面面進行優化,使應用在不需要改動或者極少量改動就能實作基本對标原生的慣性滾動流暢度,如果應用本身再進一步優化,甚至有可能獲得優于原生的效果。
我們嘗試了各式各樣的優化,包括:
- 優化線程的優先級設定,更好地保障渲染流水線的前台線程,UI 和 Raster 線程不會因為無法擷取到 CPU 排程而阻塞;
- 優化渲染流水線的 vsync 排程,減少一些不必要的耗時和空等;
- 優化渲染流水線針對 TextureView 繪制的排程,規避 TextureView 繪制機制的副作用;
- 重構渲染流水線的排程邏輯,通過更深的流水線深度來增加輸出的吞吐量,使得輸出更平穩連續;
- 優化一些布局算法,減少布局耗時;
- 優化新挂載清單單元的 Build & Layout 的排程,減少其成為性能瓶頸的可能,比如說将新挂載單元的 Build 和 Layout 拆分到不同幀去執行;
- 優化光栅化性能,比如更好地支援用戶端使用類似 Web 開發的 Opacity Hack 的技巧,通過使用間接光栅化來減少光栅化耗時;
從目前來看,部分優化嘗試的效果還是十分明顯,有些優化的覆寫面很廣,适用于幾乎所有的場景,而有些優化對特定場景效果比較好。總的來說,測試的業務頁面運作在我們優化過後的引擎,整體流暢度能夠明顯提升一個台階,也基本實作了我們對标原生流暢度的目标。在後續的文章中,我會逐漸介紹我們所做的一些優化,同時我們也會争取将一些優化的代碼送出回社群。
Stay tune, my friends, stay tune...
**免費領取電子書
《實時計算 Flink 版獨家實戰秘籍》**
實時計算 Flink 版獨家實戰秘籍,由阿裡巴巴實時計算核心研發團隊出品,技術大佬手把手教學,從基礎場景解析到進階任務遷移、demo實操等,9天幫您輕松Get企業級實時計算能力!
掃碼加阿裡妹好友,回複“實時計算”擷取吧~(若掃碼無效,可直接添加alimei2020、alimei5、alimei6、alimei7)