[Flutter 渲染優(yōu)化系列] Flutter 渲染性能問(wèn)題分析

作者:易旭昕
原文鏈接:https://zhuanlan.zhihu.com/p/354631257
本文由作者授權(quán)發(fā)布。
易老師寫(xiě)了很多篇關(guān)于 Flutter 渲染引擎的文章,講的非常深入,我從中學(xué)到了很多,昨天很有幸的加到易老師微信,表達(dá)了一番崇敬之情,易老師人非常好,也非常謙遜。
最后表達(dá)一點(diǎn)小小的心意,發(fā)了個(gè)紅包,不管怎么說(shuō),學(xué)到了很多知識(shí),知識(shí)是無(wú)價(jià)的,不過(guò)易老師并沒(méi)有收,大家也可以到易老師到博客中看看其他文章,點(diǎn)贊、轉(zhuǎn)發(fā)也是一種支持,后面我也會(huì)繼續(xù)分享易老師的文章。
正文
我在Flutter vs Chromium 動(dòng)畫(huà)渲染的對(duì)比分析一文中對(duì) Flutter 和 Web (Chromium) 的各種動(dòng)畫(huà)的理論性能優(yōu)劣進(jìn)行了分析,其中一個(gè)主要結(jié)論是,由于慣性滾動(dòng)處理機(jī)制和光柵化機(jī)制的不同,Web (Chromium) 的慣性滾動(dòng)動(dòng)畫(huà)性能理論上要遠(yuǎn)遠(yuǎn)優(yōu)于 Flutter。而在一些已經(jīng)上線的使用 Flutter 的業(yè)務(wù)中,業(yè)務(wù)方也持續(xù)給我們反饋了這些業(yè)務(wù)在中低端 Android 手機(jī)上存在比較嚴(yán)重的慣性滾動(dòng)性能問(wèn)題:
業(yè)務(wù) A 的頁(yè)面較為簡(jiǎn)單,但是在低端手機(jī)上平均幀率在 40 ~ 50 之間,中端手機(jī)在 50 ~ 55 之間,低端機(jī)存在較為明顯的卡頓問(wèn)題; 業(yè)務(wù) B 的頁(yè)面比較復(fù)雜,業(yè)務(wù)邏輯也較為復(fù)雜,在低端手機(jī)上平均幀率更是低到最低 30 多幀(35 ~ 45 之間),中端手機(jī)也是在 50 左右,并且存在較為頻繁的長(zhǎng)時(shí)間卡頓,低端機(jī)存在比較嚴(yán)重的卡頓問(wèn)題,中端機(jī)也不太流暢;
而以我們長(zhǎng)期的經(jīng)驗(yàn)數(shù)據(jù),對(duì)于 Web 來(lái)說(shuō),即使在低端手機(jī)上,較為復(fù)雜的頁(yè)面慣性滾動(dòng)幀率一般也在 50 以上,也較少長(zhǎng)時(shí)間的卡頓,達(dá)到基本流暢的水平。并且剛好業(yè)務(wù) B 有完全一樣的 Native 版本,它對(duì)比 Flutter 版本,幀率普遍高了 5 ~ 10 幀左右。
所以雖然我們沒(méi)有找到同一個(gè)頁(yè)面的三個(gè)不同版本進(jìn)行嚴(yán)格的比對(duì),但是基于上述的測(cè)試數(shù)據(jù)和我們長(zhǎng)期的經(jīng)驗(yàn),很容易得出結(jié)論是,在慣性滾動(dòng)的性能上:
Web (Chromium) > Native (Android) > Flutter (Android)
我們?cè)诓煌O(shè)備上對(duì)上述業(yè)務(wù)頁(yè)面在慣性滾動(dòng)過(guò)程中進(jìn)行 trace 的抓取,結(jié)合 Flutter 的代碼對(duì) trace 文件進(jìn)行分析,了解 Flutter 渲染流水線在慣性滾動(dòng)過(guò)程中各個(gè)環(huán)節(jié)的調(diào)度,了解各個(gè)環(huán)節(jié)的可能耗時(shí)和哪些環(huán)節(jié)可能成為性能瓶頸。在分析的過(guò)程中,我們對(duì) Flutter 的渲染機(jī)制有了更深入的了解,這篇文章就是對(duì)比 Web (Chromium) 和 Native (Android),對(duì) Flutter 的渲染性能問(wèn)題進(jìn)行深入分析,特別是分析慣性滾動(dòng)性能糟糕的原因。
\1. 這里的幀率數(shù)據(jù)給的是一個(gè)范圍是因?yàn)槲覀兪褂昧藥追N不同的滾動(dòng)速度進(jìn)行測(cè)試,一般來(lái)說(shuō)滾動(dòng)速度越快,平均幀率就越低 \2. iPhone 基本不存在所謂的低端機(jī),iOS 整體表現(xiàn)都還可以,不同實(shí)現(xiàn)的差異不大,所以我們目前主要的測(cè)試和優(yōu)化都是在 Android 上進(jìn)行
寫(xiě)在前面的結(jié)論
Flutter 有很多優(yōu)點(diǎn),特別是對(duì)于開(kāi)發(fā)者來(lái)說(shuō),跨平臺(tái)多端支持,豐富的 UI 組件庫(kù)和交互效果,聲明式 UI,React 的更新方式,Hot-reload 提高開(kāi)發(fā)效率等等。雖然它在渲染性能上有不少缺陷,但是某種程度上,某些缺陷也是為了實(shí)現(xiàn)更高層次的設(shè)計(jì)目標(biāo)而不得不承受的結(jié)果。
比如 Dart 語(yǔ)言原生對(duì)異步編程有良好的支持,應(yīng)用開(kāi)發(fā)者不需要去編寫(xiě)復(fù)雜和容易出問(wèn)題的多線程代碼,就可以有效地避免主線程長(zhǎng)時(shí)間阻塞,編寫(xiě)出性能良好的 UI。但是在慣性滾動(dòng)這樣對(duì)性能要求非常高場(chǎng)景下,可能幾毫秒的阻塞都會(huì)導(dǎo)致掉幀,缺少真正的多線程編程能力某種程度就變成了一種阻礙(Android 上你甚至可以在其它線程對(duì) View 做非 UI 直接相關(guān)的操作)。
又比如使用 Immutable Widget 作為 UI Configuration 的設(shè)計(jì)是聲明式 UI 和 Hot-reload 的基礎(chǔ),但還是會(huì)引入額外的開(kāi)銷(xiāo)和喪失足夠的靈活性,應(yīng)用無(wú)法直接控制 UI 組件的生命周期,無(wú)法直接控制 UI 組件的布局和繪制,這同樣妨礙了慣性滾動(dòng)的性能優(yōu)化。
而對(duì)我們內(nèi)核團(tuán)隊(duì)來(lái)說(shuō),要做的就是在理解 Flutter 這些缺陷的同時(shí),去研究是否存在有效地進(jìn)行局部改進(jìn),或者從其它設(shè)計(jì)層面上對(duì)某些缺陷進(jìn)行規(guī)避的方法,讓?xiě)?yīng)用開(kāi)發(fā)者既可以充分利用 Flutter 的優(yōu)勢(shì),又不用過(guò)于擔(dān)心它存在的問(wèn)題。
總的來(lái)說(shuō)下半年的工作目前看來(lái)還是取得了不錯(cuò)的成果,也基本實(shí)現(xiàn)了讓 Flutter 慣性滾動(dòng)性能對(duì)標(biāo)原生的目標(biāo),下圖比較直觀地展示了我們優(yōu)化的結(jié)果。

這里電影幀是指 1000 / 24 約 40毫秒,2個(gè)電影幀 / min 是指連續(xù)滾動(dòng)一分鐘內(nèi)出現(xiàn)超過(guò) 80 毫秒卡頓的次數(shù)
Web (Chromium) vs Flutter
Web (Chromium) 在慣性滾動(dòng)上是有非常明顯的機(jī)制優(yōu)勢(shì)的,這跟 Web 渲染引擎為了適應(yīng) Web 頁(yè)面的高復(fù)雜度,高不確定性有關(guān),甚至某種程度上犧牲了一些渲染效果和其它動(dòng)畫(huà)的渲染性能。Web (Chromium) 在慣性滾動(dòng)上的優(yōu)勢(shì)主要體現(xiàn)在以上兩方面:
Chromium 有完整獨(dú)立的合成器驅(qū)動(dòng)慣性滾動(dòng)動(dòng)畫(huà)的運(yùn)行,有獨(dú)立的合成線程,慣性滾動(dòng)動(dòng)畫(huà)的更新和主線程更新 DOM 樹(shù)是不同步的,主線程運(yùn)行 JS,Build & Layout 不會(huì)阻塞合成線程; Chromium 的分塊異步光柵化機(jī)制一方面減少了慣性滾動(dòng)動(dòng)畫(huà)過(guò)程中圖層的重復(fù)光柵化,另一方面光柵化不會(huì)阻塞合成線程的合成輸出;
對(duì)比 Web (Chromium),F(xiàn)lutter 在上述兩方面都存在比較明顯的劣勢(shì):

Flutter 需要依賴于 Relayout 來(lái)驅(qū)動(dòng)慣性滾動(dòng)動(dòng)畫(huà),滾動(dòng)容器內(nèi)的元素在滾動(dòng)過(guò)程中每一幀都需要 Relayout,不過(guò)這個(gè)一般耗時(shí)不高。Flutter 的無(wú)限長(zhǎng)列表一般都采用 Lazy Build 的方式生成列表單元,當(dāng)列表單元接近可見(jiàn)區(qū)域的時(shí)候,框架才調(diào)用應(yīng)用提供的 Builder 生成列表單元的 Widget 樹(shù)并進(jìn)行布局,新掛載的列表單元的 Build & Layout 通常耗時(shí)較長(zhǎng),在上述業(yè)務(wù)頁(yè)面中,可能耗費(fèi) 10 毫秒以上,甚至幾十毫秒,特別是單幀內(nèi)需要 Build 多個(gè)單元的情況,它們是導(dǎo)致掉幀的主要原因。從上圖 trace 中我們很容易發(fā)現(xiàn),正常速度滾動(dòng)下,在 Flutter UI 線程 Frame 的階段,大部分情況下耗時(shí)不高,但是每幾幀就會(huì)出現(xiàn)一次耗時(shí)較長(zhǎng)的 Frame,從上圖看耗時(shí)較長(zhǎng)的 Frame 已經(jīng)接近甚至超過(guò)一個(gè) vsync 周期,滾動(dòng)速度越快,出現(xiàn)耗時(shí)較長(zhǎng)的 Frame 的頻率就越高,耗時(shí)也可能越長(zhǎng),它的耗時(shí)主要就來(lái)自新掛載列表單元的 Build & Layout。
Flutter 采用的以直接光柵化為主,間接光柵化為輔的同步光柵化機(jī)制,在合成輸出過(guò)程中進(jìn)行光柵化,光柵化的耗時(shí)會(huì)直接影響動(dòng)畫(huà)的性能。以實(shí)際業(yè)務(wù)為例子:
業(yè)務(wù) A 的頁(yè)面較為簡(jiǎn)單,光柵化耗時(shí)大部分在 3 ~ 5 毫秒之間,除了偶爾波動(dòng)較高外,基本沒(méi)有造成阻塞,所以業(yè)務(wù) A 的大部分掉幀都是 Flutter UI 線程的 Frame 耗時(shí)較高導(dǎo)致; 業(yè)務(wù) B 的頁(yè)面比較復(fù)雜,光柵化耗時(shí)大部分在 7 ~ 10 毫秒之間,偶爾波動(dòng)超過(guò) 10 毫秒,所以部分掉幀主要是光柵化導(dǎo)致的; 實(shí)際上我們還碰到一個(gè)頁(yè)面因?yàn)榇蠓秶褂?Backdrop Filter 導(dǎo)致光柵化耗時(shí)非常高,在低端機(jī)上只有 10 ~ 20幀,不過(guò)這個(gè)可以在應(yīng)用層面做一些優(yōu)化來(lái)避免;
總的來(lái)說(shuō),F(xiàn)lutter 在慣性滾動(dòng)過(guò)程的掉幀大部分都來(lái)自 Flutter UI 線程的阻塞,新掛載列表單元的 Build & Layout 耗時(shí)過(guò)長(zhǎng)是主要原因。但是對(duì)于一些比較復(fù)雜的頁(yè)面,光柵化耗時(shí)較長(zhǎng)也是一個(gè)導(dǎo)致掉幀的原因。
我們?cè)?Chromium 光柵化改造 - 混合光柵化 對(duì)比了不同光柵化機(jī)制在合成輸出過(guò)程中的光柵化+合成輸出的耗時(shí),異步光柵化機(jī)制在這方面會(huì)有明顯的優(yōu)勢(shì),這也是我們?cè)?U4 4.0 上采用了混合光柵化的原因
Flutter 雖然提供了 KeepLive 機(jī)制用于避免列表單元滾出可見(jiàn)區(qū)域被回收,重新滾入可見(jiàn)區(qū)域又重新 Rebuild & Relayout,但是 KeepLive 機(jī)制并不適用于第一次顯示的列表單元,并且在無(wú)限長(zhǎng)列表場(chǎng)景很容易造成內(nèi)存爆炸,適用場(chǎng)景不多
Native (Android) vs Flutter
如果說(shuō) Web (Chromium) 因?yàn)闄C(jī)制的原因,慣性滾動(dòng)性能明顯優(yōu)于 Flutter,這個(gè)比較容易理解。那么 Native (Android) 在機(jī)制上其實(shí)跟 Flutter 是比較類似的,為什么它的性能也會(huì)優(yōu)于 Flutter 呢?
Android 無(wú)限長(zhǎng)列表一般使用 RecyclerView 實(shí)現(xiàn),而 RecyclerView 支持子 View 樹(shù)級(jí)別的復(fù)用,使得新掛載的列表單元在 RecyclerView 的支持下,只需要更新復(fù)用的子 View 樹(shù)的數(shù)據(jù)然后局部重排即可,耗時(shí)會(huì)大大少于 Flutter 整個(gè)列表單元的完整 Build & Layout,這是 Native (Android) 的無(wú)限長(zhǎng)列表滾動(dòng)更流暢的主要原因。不過(guò)除此以外,還有很多因素也會(huì)影響到 Flutter 的流暢度。
跟 Native 相比較,F(xiàn)lutter UI 線程會(huì)顯得更擁擠。Dart Isolate 的內(nèi)存堆是隔離的,這點(diǎn)比較像 JavaScript,Isolate 之間的關(guān)系更像是多進(jìn)程而不是多線程,導(dǎo)致了一些多線程優(yōu)化很難實(shí)現(xiàn)。應(yīng)用通常要注冊(cè)多個(gè)回調(diào)來(lái)處理外部傳入的數(shù)據(jù)或者事件,這些回調(diào)接收外部數(shù)據(jù)或者事件,進(jìn)行處理后更新內(nèi)部數(shù)據(jù)(Model),通常這些回調(diào)都需要在 UI 線程執(zhí)行。如果它們集中頻繁地發(fā)生,即使單次耗時(shí)不高,也很容易造成 Flutter UI 線程的阻塞,簡(jiǎn)單說(shuō)就是這些非 UI 任務(wù)的頻繁執(zhí)行可能會(huì)導(dǎo)致慣性滾動(dòng)過(guò)程中 UI 任務(wù)的延遲,最終導(dǎo)致掉幀,但是 Dart Isolate 的限制,對(duì)內(nèi)部數(shù)據(jù)的更新又必須在 UI 線程上進(jìn)行。
大部分應(yīng)用都是局部使用 Flutter 開(kāi)發(fā),需要跟 Native 進(jìn)行混用,這就導(dǎo)致了應(yīng)用很難使用 SurfaceView,而需要使用 TextureView。TextureView 會(huì)帶來(lái)一些額外的性能問(wèn)題,除了更高的 GPU 開(kāi)銷(xiāo)外,TextureView 的繪制機(jī)制也容易出現(xiàn)因?yàn)檎{(diào)度的不合理而導(dǎo)致掉幀。
最后雖然 Android 和 Flutter 都是以直接光柵化為主,間接光柵化為輔的同步光柵化機(jī)制。但是將 Skia 作為 UI 的光柵化引擎,比起為 UI 專門(mén)定制的光柵化引擎可能還是存在一些缺陷:
Skia GPU 光柵化的過(guò)程,涉及將通用的 2D 繪制指令轉(zhuǎn)換成一種接近 GPU 指令的內(nèi)部形式,然后經(jīng)過(guò)進(jìn)一步優(yōu)化后輸出最終的 GPU 指令,為 UI 專門(mén)定制的光柵化引擎理論上可以緩存第一步的結(jié)果,減少每一幀光柵化的耗時(shí); Skia 作為一個(gè)通用的光柵化引擎,內(nèi)部實(shí)現(xiàn)是線程無(wú)感的,而為 UI 專門(mén)定制的光柵化引擎可以更容易使用多線程來(lái)將光柵化過(guò)程中部分 CPU 工作并行化,比如生成字型或者路徑頂點(diǎn)等任務(wù);
不過(guò)我們沒(méi)有實(shí)際去比較兩者的光柵化性能差異,這里只是一些理論分析。
TextureView 的調(diào)度問(wèn)題更詳細(xì)的信息可以參考我的文章TextureView 的血與淚
應(yīng)用層面優(yōu)化和局限性
針對(duì) Flutter 的慣性滾動(dòng)性能問(wèn)題,不少應(yīng)用也嘗試了各種優(yōu)化方案,比如閑魚(yú)的方案就比較有代表性。針對(duì)新掛載列表單元的 Build & Layout 耗時(shí)過(guò)長(zhǎng),閑魚(yú)的優(yōu)化方案是 Element 復(fù)用和分幀渲染。
Element 復(fù)用其實(shí)就是參考 RecyclerView 的子 View 樹(shù)復(fù)用,理論上可以避免重新創(chuàng)建列表單元的 Element 樹(shù)和 RenderObject 樹(shù)的時(shí)間開(kāi)銷(xiāo)。但是對(duì)比 Native,仍然需要重新構(gòu)建 Widget 樹(shù),并把新的 Widget 樹(shù)跟舊的 Element 樹(shù)進(jìn)行綁定,并通過(guò) Element 樹(shù)去更新 RenderObject 樹(shù)。而 Native 則可以直接復(fù)用 View 樹(shù),然后更新若干子 View 的數(shù)據(jù)即可,這部分的開(kāi)銷(xiāo)仍然比優(yōu)化過(guò)后的 Flutter 要低。
分幀渲染的思路是每個(gè)列表單元提供兩個(gè)版本的 Widget 樹(shù),除了完整版,還有一個(gè)簡(jiǎn)化版作為占位符。如果單幀內(nèi)已經(jīng) Build 過(guò)一個(gè)完整版本的單元,在需要 Build 第二個(gè)單元時(shí)就只 Build 簡(jiǎn)化的版本,這樣可以避免單幀內(nèi)多個(gè)列表單元的 Build & Layout 疊加在一起造成更大的阻塞。它的局限性是主要適用于列表單元較小,慣性滾動(dòng)速度較快,一幀滾動(dòng)會(huì)出現(xiàn)多個(gè)列表單元需要 Build & Layout 的場(chǎng)景,對(duì)避免更長(zhǎng)時(shí)間的卡頓有一定作用。只是這個(gè)優(yōu)化 Android Native 看起來(lái)也完全能做,并且因?yàn)?Android 應(yīng)用可以直接控制 View 是否參與布局和繪制,理論上做起來(lái)也更簡(jiǎn)單,效果也更好。
總的來(lái)說(shuō),F(xiàn)lutter 應(yīng)用的一些優(yōu)化,要不是 Native 本來(lái)就已經(jīng)實(shí)現(xiàn),并且效果更好;就是 Native 同樣也可以實(shí)現(xiàn),而且實(shí)現(xiàn)起來(lái)更簡(jiǎn)單,效果也更好,并且其它一些影響 Flutter 性能的因素在應(yīng)用層面無(wú)法進(jìn)行優(yōu)化。
所以 Flutter 應(yīng)用優(yōu)化起來(lái)可能比 Native 更麻煩,最后的效果也還是比不上 Native。一個(gè)優(yōu)化后的 Flutter 應(yīng)用,比起一個(gè)優(yōu)化后的 Native 應(yīng)用,在慣性滾動(dòng)上還是會(huì)有一定性能差距。
我們的優(yōu)化嘗試
作為一個(gè)引擎團(tuán)隊(duì),我們期望實(shí)現(xiàn)的目標(biāo)是從框架和引擎層面對(duì) Flutter 渲染流水線的方方面面進(jìn)行優(yōu)化,使應(yīng)用在不需要改動(dòng)或者極少量改動(dòng)就能實(shí)現(xiàn)基本對(duì)標(biāo)原生的慣性滾動(dòng)流暢度,如果應(yīng)用本身再進(jìn)一步優(yōu)化,甚至有可能獲得優(yōu)于原生的效果。
我們嘗試了各式各樣的優(yōu)化,包括:
優(yōu)化線程的優(yōu)先級(jí)設(shè)置,更好地保障渲染流水線的前臺(tái)線程,UI 和 Raster 線程不會(huì)因?yàn)闊o(wú)法獲取到 CPU 調(diào)度而阻塞; 優(yōu)化渲染流水線的 vsync 調(diào)度,減少一些不必要的耗時(shí)和空等; 優(yōu)化渲染流水線針對(duì) TextureView 繪制的調(diào)度,規(guī)避 TextureView 繪制機(jī)制的副作用; 重構(gòu)渲染流水線的調(diào)度邏輯,通過(guò)更深的流水線深度來(lái)增加輸出的吞吐量,使得輸出更平穩(wěn)連續(xù); 優(yōu)化一些布局算法,減少布局耗時(shí); 優(yōu)化新掛載列表單元的 Build & Layout 的調(diào)度,減少其成為性能瓶頸的可能,比如說(shuō)將新掛載單元的 Build 和 Layout 拆分到不同幀去執(zhí)行; 優(yōu)化光柵化性能,比如更好地支持客戶端使用類似 Web 開(kāi)發(fā)的 Opacity Hack 的技巧,通過(guò)使用間接光柵化來(lái)減少光柵化耗時(shí);
從目前來(lái)看,部分優(yōu)化嘗試的效果還是十分明顯,有些優(yōu)化的覆蓋面很廣,適用于幾乎所有的場(chǎng)景,而有些優(yōu)化對(duì)特定場(chǎng)景效果比較好??偟膩?lái)說(shuō),測(cè)試的業(yè)務(wù)頁(yè)面運(yùn)行在我們優(yōu)化過(guò)后的引擎,整體流暢度能夠明顯提升一個(gè)臺(tái)階,也基本實(shí)現(xiàn)了我們對(duì)標(biāo)原生流暢度的目標(biāo)。在后續(xù)的文章中,我會(huì)逐步介紹我們所做的一些優(yōu)化,同時(shí)我們也會(huì)爭(zhēng)取將一些優(yōu)化的代碼提交回社區(qū)。
Stay tune, my friends, stay tune...

