原生長(zhǎng)列表內(nèi)嵌 Flutter 卡片性能調(diào)研

作者:易旭昕
原文鏈接:https://zhuanlan.zhihu.com/p/354631257
本文由作者授權(quán)發(fā)布。
寫作費(fèi)時(shí),敬請(qǐng)點(diǎn)贊,關(guān)注,收藏三連。
這篇文章主要是對(duì)在原生長(zhǎng)列表中嵌入多個(gè) Flutter 卡片,每個(gè)卡片都對(duì)應(yīng)一個(gè)獨(dú)立的 FlutterView/Engine 這種使用場(chǎng)景進(jìn)行調(diào)研,分析該場(chǎng)景下的性能和內(nèi)存使用等指標(biāo)。通過調(diào)研,我們希望了解這種使用場(chǎng)景下 Flutter 的性能表現(xiàn)如何,在實(shí)際的業(yè)務(wù)中是否可行。
主要調(diào)研的指標(biāo)包括三方面:
原生長(zhǎng)列表的滾動(dòng)流暢度,是否存在一些 Flutter 相關(guān)的調(diào)用會(huì)長(zhǎng)時(shí)間阻塞主線程,也就是 Flutter.platform 線程,導(dǎo)致掉幀; Flutter 卡片的空白延遲幀數(shù),我們知道 Flutter 的布局是在 Flutter.ui 線程,光柵化是在 Flutter.raster 線程,它們跟原生 UI 的繪制是異步的,如果在 FlutterView 可見之后才觸發(fā)卡片的布局和光柵化,卡片必然存在一定時(shí)間的空白,我們希望知道這個(gè)空白持續(xù)的幀數(shù)和對(duì)視覺的影響; 內(nèi)存占用,F(xiàn)lutter 本身會(huì)帶來(lái)一定的內(nèi)存增量,那多個(gè) FlutterView/Engine 同時(shí)共存并顯示是不是會(huì)進(jìn)一步增大內(nèi)存的壓力,圖片紋理緩存管理在該場(chǎng)景下表現(xiàn)如何,是否還有進(jìn)一步優(yōu)化的空間;
心急的同學(xué)可以直接跳到最后結(jié)論的部分。
Flutter Card Demo 說(shuō)明

為了進(jìn)行調(diào)研,我們編寫了一個(gè) Android Demo,Demo 在 Android Native 端使用了 androidx 提供的 RecyclerView 實(shí)現(xiàn)長(zhǎng)列表。RecyclerView 會(huì)自動(dòng)創(chuàng)建多個(gè)卡片并循環(huán)使用,在 Demo 中,每個(gè)卡片都是一個(gè) FlutterCard 對(duì)象,其中包含一個(gè)獨(dú)立 FlutterView 和 FlutterEngine,卡片的內(nèi)容由 Flutter 呈現(xiàn)。
在上圖 "#5 at 11" 的文本中,5 代表這個(gè)卡片的 ID,對(duì)應(yīng)創(chuàng)建的 FlutterView/FlutterEngine 的序號(hào),11 代表這個(gè)卡片在 RecyclerView 顯示的位置,從這段文本我們可以很清楚地看到創(chuàng)建的 FlutterCard 卡片對(duì)象是不斷被 RecyclerView 循環(huán)使用的; 長(zhǎng)列表包含了 200 張卡片,在實(shí)際的運(yùn)行中 RecyclerView 創(chuàng)建了約 9 個(gè) FlutterCard 對(duì)象,也就是 9 對(duì) FlutterView/FlutterEngine(實(shí)際個(gè)數(shù)跟 RecyclerView 的高度和卡片的高度有關(guān)); 為了模擬真實(shí)的場(chǎng)景,我們會(huì)在 RecyclerView 重用 FlutterCard 對(duì)象時(shí),會(huì)重新隨機(jī)產(chǎn)生一個(gè)新的卡片高度,并通過 MessageChannel 通知 FlutterEngine 更新內(nèi)容,觸發(fā)該卡片的 Widget 樹的更新和重布局,每個(gè)卡片顯示一張圖片和兩段文本; FlutterView 使用 TextureView 作為輸出的 Surface,當(dāng) FlutterView 被 RecyclerView 回收時(shí),TextureView 會(huì)觸發(fā) Surface Destroy,當(dāng) FlutterView 被 RecyclerView 重用并重新參與繪制時(shí),TextureView 會(huì)觸發(fā) Surface Available(Create);
性能表現(xiàn)分析
測(cè)試手機(jī)使用了 Google Pixel,在現(xiàn)在來(lái)說(shuō)算是性能比較差了,可以更好地反映實(shí)際的狀況。
滾動(dòng)流暢度
FlutterCard
可能是因?yàn)閴嚎s的原因,視頻顯示不如實(shí)際表現(xiàn)流暢
除了初始滾動(dòng)時(shí),可能因?yàn)榧袆?chuàng)建和初始化 FlutterEngine 導(dǎo)致主線略微阻塞,會(huì)有輕微掉幀的現(xiàn)象外,整個(gè)滾動(dòng)過程都非常流暢。在慣性滾動(dòng)中,卡片會(huì)不斷地被回收和重用,所以 Surface 的 Destroy 和 Create 會(huì)頻繁地被觸發(fā),在應(yīng)用主線程,也就是 Flutter.platform 線程觸發(fā) Surface Destroy 和 Create,主線程需要阻塞等待 Flutter 完成清理或者初始化的操作,如果它造成明顯阻塞就很容易導(dǎo)致掉幀。
在 Android 平臺(tái)上,PlatformViewAndroid::NotifyDestroyed 主要工作:
通知 Flutter.ui 線程停止 Animator; 通知 Flutter.raster 線程的光柵化器釋放資源,如 RasterCache,GrResourceCache,LayerTree,GrContext 等; 通知 http://Flutter.io 線程釋放已經(jīng)處于等待釋放狀態(tài)的 GPU 資源; 通知 Flutter.raster 線程釋放 Window Surface;
PlatformViewAndroid::NotifyCreated 主要工作:
通知 Flutter.raster 線程設(shè)置 Window Surface; 通知 Flutter.raster 線程創(chuàng)建 GrContext; 通知 http://Flutter.io 線程設(shè)置紋理上傳使用的 GrContext; 通知 Flutter.ui 線程啟動(dòng) Animator,開始調(diào)度渲染 ScheduleFrame; 通知 Flutter.raster 設(shè)置光柵化器;
通過分析發(fā)現(xiàn),在對(duì)比開啟和關(guān)閉我們的引擎優(yōu)化的情況下:
Surface Destroy 過程耗時(shí) 1 ~ 2ms,開啟和關(guān)閉引擎優(yōu)化無(wú)明顯影響; Surface Create 過程沒有開啟引擎優(yōu)化耗時(shí) 4 ~ 7ms,開啟引擎優(yōu)化后降到了 2 ~ 4ms,引擎優(yōu)化降低了 3ms 左右的耗時(shí);
可以看到,在開啟引擎優(yōu)化后,Surface Destroy 和 Create 的耗時(shí)都很少,絕大部分情況下都不會(huì)導(dǎo)致掉幀。
卡片空白幀數(shù)
在 Demo 的場(chǎng)景中,RecyclerView 在慣性滾動(dòng)時(shí),將新的卡片從不可見區(qū)域移進(jìn)可見區(qū)域,觸發(fā)了 TextureView 的繪制,而 TextureView 的 Surface Available(Create)是在它第一次被繪制的時(shí)候觸發(fā)。
RecyclerView 會(huì)提前一些將卡片加入 View 樹參與布局
按照原生的邏輯,F(xiàn)lutter 需要在 Surface Create 時(shí)才觸發(fā) ScheduleFrame。如果當(dāng)前幀是第 N 幀,在第 N 幀的 Draw 的過程中觸發(fā)了 TextureView 的 Surface Available(Create),同時(shí)觸發(fā)了 Flutter 的 ScheduleFrame,F(xiàn)lutter 要等到 N + 1 幀的 VSync 回調(diào)時(shí)才觸發(fā) BeginFrame 開始繪制,如果 Flutter 首幀的布局 + 光柵化耗時(shí)少于一個(gè) VSync 周期,那 Flutter 的首幀可以在 Native UI 第 N + 2 幀輸出。
也就是說(shuō)即使卡片的 Widget 樹很簡(jiǎn)單,或者設(shè)備的性能非常高,F(xiàn)lutter 卡片最少也有兩幀的空白時(shí)間,實(shí)際空白持續(xù)的幀數(shù)跟設(shè)備的性能,Widget 樹的復(fù)雜程度都有關(guān)系。從 Demo 在 Pixel 上運(yùn)行的情況來(lái)看,因?yàn)榭ㄆ容^簡(jiǎn)單,大部分情況下都是兩幀空白。
如果僅僅只是兩幀的空白,考慮到卡片本身只是一部分可見,設(shè)置卡片的 Flutter Widget 背景色跟原生 View 保持一致,或者干脆 Flutter Widget 不繪制背景,完全透明(需要使用 TextureView),這樣一般情況下也不太容易察覺。
另外,因?yàn)?Flutter 的圖片是異步加載和解碼,所以圖片如果太大,圖片的繪制相比其它 Widget 可能會(huì)有更明顯的延遲。
相關(guān)的 Android 渲染流水線幀調(diào)度的分析,可以參考我的文章TextureView 的血與淚
內(nèi)存占用分析
為了排除圖片解碼緩存內(nèi)存管理的干擾,我們專門測(cè)試了無(wú)圖和有圖兩種情況,并且增加了開啟引擎優(yōu)化和關(guān)閉引擎優(yōu)化的對(duì)比。我們加入了只有一個(gè) FlutterView/Engine 的無(wú)圖簡(jiǎn)單 Demo 作為對(duì)比參考(使用 SurfaceView,大小只有窗口的一半),另外也加入了一個(gè)純?cè)鸁o(wú)圖的長(zhǎng)列表 Demo 作為對(duì)比參考(卡片內(nèi)容不完全一致,僅供參考)。
內(nèi)存占用通過 meminfo 查看,主要看 PSS,PSS 雖然不能完全代表真實(shí)的物理內(nèi)存占用,不過用于對(duì)比增量還是有一定參考價(jià)值的。實(shí)際操作中會(huì)滾動(dòng)到底部之后再滾動(dòng)回頭部,長(zhǎng)列表設(shè)置顯示 200 張卡片,在這個(gè)過程中 RecyclerView 一共創(chuàng)建了 9 個(gè) FlutterCard 對(duì)象,也就是 9 對(duì) FlutterView/Engine 循環(huán)使用。

我們首先對(duì)比單引擎的簡(jiǎn)單 Demo 和完全原生的應(yīng)用,主要增加的部分在:
.so mmap:額外的 so 庫(kù); EGL mtrack:額外的 Surface buffer,考慮到 Demo 的 FlutterView 只有一半窗口大小,如果是整個(gè)窗口大小,應(yīng)該增加 24m 左右(Android 的 Surface 是 triple buffer); Unknown:主要是 Flutter Engine 分配的內(nèi)存,包括 Skia 的內(nèi)存分配,Dart VM 的內(nèi)存分配;
所以一個(gè)單引擎全屏簡(jiǎn)單的 Flutter App 對(duì)比純?cè)矔?huì)帶來(lái) 40 ~ 50m 左右的額外開銷。
再對(duì)比多引擎同時(shí)運(yùn)行多個(gè) Flutter App 的情況:
Native Heap 小幅增加,猜測(cè)主要是額外線程的堆棧; EGL mtrack 因?yàn)槎嘁?Demo 使用的是 TextureView,TextureView 分配的 buffer 在 meminfo 中存在重復(fù)計(jì)數(shù)的問題,改成 SurfaceView 之后兩者應(yīng)該是差不多的,括號(hào)里面的 46 是改成使用 SurfaceView 時(shí)的占用,實(shí)際上這一項(xiàng)的增量只取決于當(dāng)前可見的 FlutterView 的總面積,在我們的卡片場(chǎng)景,全部可見的卡片總面積也只是略大于當(dāng)前窗口的面積,在 1080p 的手機(jī)上,20 ~ 30m 的增量是一個(gè)典型值; Unknown 增加的比較多,猜測(cè)主要來(lái)源至多個(gè) Flutter App 運(yùn)行在多個(gè) Dart Isolate,Dart VM 分配的內(nèi)存;
從上面的對(duì)比,如果在可見的 FlutterView 面積一樣的情況下,并且開啟引擎優(yōu)化,9 個(gè)引擎運(yùn)行 9 個(gè)比較簡(jiǎn)單的 Flutter App 對(duì)比只有一個(gè)引擎運(yùn)行一個(gè) Flutter App 大約增加了 40 ~ 50m 左右的額外開銷。如果沒有開啟引擎優(yōu)化,我們會(huì)看到大量額外的線程和 GL 上下文會(huì)導(dǎo)致 Native Heap 和 GL mtrack 大幅增加,總共增加了 68m。
開啟有圖之后,我們可以看到 Gfx Dev 大幅增加 348m,主要來(lái)自于圖片解碼后上傳的紋理。Unknown 部分也有一定幅度增加,猜測(cè)主要來(lái)自于圖片原始數(shù)據(jù)的內(nèi)存緩存。這里面最主要的問題是 Engine 在循環(huán)使用的過程中,會(huì)一直累積圖片紋理緩存不會(huì)主動(dòng)釋放,并且每個(gè) Engine 獨(dú)立管理紋理緩存,缺少全局管控。
結(jié)論
慣性滾動(dòng)十分流暢,Surface Destroy 和 Create 在開啟引擎優(yōu)化后基本不會(huì)導(dǎo)致掉幀; 原生的邏輯導(dǎo)致最少兩幀的卡片空白,實(shí)際的空白幀數(shù)取決于設(shè)備的性能和 Widget 樹的復(fù)雜程度,測(cè)試 Demo 在 Pixel 上大部分情況都是兩幀; 內(nèi)存占用的問題比較明顯,雖然我們的引擎優(yōu)化已經(jīng)大幅減少了額外的內(nèi)存占用,但是每個(gè)獨(dú)立的 Flutter App 運(yùn)行在獨(dú)立的 Dart Isolate 仍然有一定的內(nèi)存增量(簡(jiǎn)單的卡片大概 4m 左右),我們?nèi)匀恍枰拗埔欢〝?shù)量的引擎分配,不過最嚴(yán)重的還是圖片的紋理內(nèi)存占用,這是我們需要進(jìn)一步優(yōu)化的;

