扣丁書屋

深入分析 Flutter 渲染性能

我在《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 的技巧,通過使用間接光柵化來減少光柵化耗時;

從目前來看,部分優化嘗試的效果還是十分明顯,有些優化的覆蓋面很廣,適用于幾乎所有的場景,而有些優化對特定場景效果比較好??偟膩碚f,測試的業務頁面運行在我們優化過后的引擎,整體流暢度能夠明顯提升一個臺階,也基本實現了我們對標原生流暢度的目標。在后續的文章中,我會逐步介紹我們所做的一些優化,同時我們也會爭取將一些優化的代碼提交回社區。

Stay tuned, my friends, stay tuned...



https://mp.weixin.qq.com/s/cTnWx5I9sOC51hWZVDOH4w

最多閱讀

如何有效定位Flutter內存問題? 1年以前  |  11676次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  7605次閱讀
Flutter插件詳解及其發布插件 2年以前  |  6293次閱讀
在Flutter中添加資源和圖片 3年以前  |  5144次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  4341次閱讀
發布Flutter開發的iOS程序 3年以前  |  4317次閱讀
Flutter for Web詳細介紹 3年以前  |  4190次閱讀
在Flutter中發起HTTP網絡請求 3年以前  |  3953次閱讀
使用Inspector檢查用戶界面 3年以前  |  3885次閱讀
Flutter Widget框架概述 3年以前  |  3261次閱讀
Flutter路由詳解 3年以前  |  3103次閱讀
JSON和序列化 3年以前  |  3026次閱讀
Flutter框架概覽 3年以前  |  2990次閱讀
推薦5個Flutter重磅開源項目! 1年以前  |  2951次閱讀
為Flutter應用程序添加交互 3年以前  |  2941次閱讀
處理文本輸入 3年以前  |  2844次閱讀
使用自定義字體 3年以前  |  2843次閱讀
編寫國際化Flutter App 3年以前  |  2831次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>