深刻分析 Flutter 渲染性能

簡介: Flutter 有不少優勢,特別是對於開發者來講,跨平臺多端支持,豐富的 UI 組件庫和交互效果,聲明式 UI,React 的更新方式,Hot-reload 提升開發效率等等。雖然它在渲染性能上有很多缺陷,可是某種程度上,某些缺陷也是爲了實現更高層次的設計目標而不得不承受的結果。算法

image.png

做者 | 蕭逸
來源 | 阿里技術公衆號編程

我在《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 頁面的測試數據比較直觀地展現了咱們優化的結果。

image.png

這裏電影幀是指 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 在上述兩方面都存在比較明顯的劣勢:

image.png

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 的技巧,經過使用間接光柵化來減小光柵化耗時。

從目前來看,部分優化嘗試的效果仍是十分明顯,有些優化的覆蓋面很廣,適用於幾乎全部的場景,而有些優化對特定場景效果比較好。總的來講,測試的業務頁面運行在咱們優化事後的引擎,總體流暢度可以明顯提高一個臺階,也基本實現了咱們對標原生流暢度的目標。在後續的文章中,我會逐步介紹咱們所作的一些優化,同時咱們也會爭取將一些優化的代碼提交回社區。

原文連接本文爲阿里雲原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索