已開源!Flutter 流暢度優化元件 Keframe

Nayuta發表於2021-07-01

「本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!

列表流暢度優化

這是一個通用的流暢度優化方案,通過分幀渲染優化由構建導致的卡頓,例如頁面切換或者複雜列表快速滾動的場景。

程式碼中 example 執行在 VIVO X23(驍龍 660),在相同的滾動操作下優化前後 200 幀採集資料指標對比(錄屏在文章最後):

優化前優化後
優化前優化後

監控工具來自:fps_monitor,指標詳細資訊:頁面流暢度不再是謎!除錯神器開箱即用,Flutter FPS檢測工具

  • 流暢:一幀耗時低於 18ms
  • 良好:一幀耗時在 18ms-33ms 之間
  • 輕微卡頓:一幀耗時在 33ms-67ms 之間
  • 卡頓:一幀耗時大於 66.7ms

採用分幀優化後,卡頓次數從 平均 33.3 幀出現了一幀,降低到 200 幀中僅出現了一幀,峰值也從 188ms 降低到 90ms。卡頓現象大幅減輕,流暢幀佔比顯著提升,整體表現更流暢。下方是詳細資料。

優化前優化後
平均多少幀出現一幀卡頓33.3200
平均多少幀出現一幀輕微卡頓8.666.7
最大耗時188.0ms90.0ms
平均耗時27.0ms19.4ms
流暢幀佔比40%64.5%

頁面切換流暢度提升

在開啟一個頁面或者 Tab 切換時,系統會渲染整個頁面並結合動畫完成頁面切換。對於複雜頁面,同樣會出現卡頓掉幀。藉助分幀元件,將頁面的構建逐幀拆解,通過 DevTools 中的效能工具檢視。切換過程的峰值由 112.5ms 降低到 30.2 ms,整體切換過程更加流暢。

image.pngimage.png

實際優化合集:Keframe 流暢度優化實踐合集

如何使用?

專案依賴:

pubspec.yaml 中新增 keframe 依賴

dependencies:
  keframe: version
複製程式碼

元件僅區分非空安全與空安全版本

非空安全使用: 1.0.2

空安全版本使用: 2.0.2

github 地址:github.com/LianjiaTech…

pub 檢視:pub.dev/packages/ke…

Dont forget star ~

快速上手:

如下圖所示

image.png

假如現在頁面由 A、B、C、D 四部分組成,每部分耗時 10ms,在頁面時構建為 40ms。使用分幀元件 FrameSeparateWidget 巢狀每一個部分。頁面構建時會在第一幀渲染簡單的佔位,在後續四幀內分別渲染 A、B、C、D。

對於列表,在每一個 item 中巢狀 FrameSeparateWidget,並將 ListView 巢狀在 SizeCacheWidget 內即可。

image.png


建構函式說明

FrameSeparateWidget :分幀元件,將巢狀的 widget 單獨一幀渲染

型別引數名是否必填含義
Keykey
intindex分幀元件 id,使用 SizeCacheWidget 的場景必傳,SizeCacheWidget 中維護了 index 對應的 Size 資訊
Widgetchild實際需要渲染的 widget
WidgetplaceHolder佔位 widget,儘量設定簡單的佔位,不傳預設是 Container()

SizeCacheWidget:快取子節點中,分幀元件巢狀的實際 widget 的尺寸資訊

型別引數名是否必填含義
Keykey
Widgetchild子節點中如果包含分幀元件,則快取實際的 widget 尺寸
intestimateCount預估螢幕上子節點的數量,提高快速滾動時的響應速度

方案設計與分析:

卡頓的本質,就是 單幀的繪製時間過長。基於此自然衍生出兩種思路解決:

1、減少一幀的繪製耗時,因為導致耗時過長的原因有很多,比如不合理的重新整理,或者繪製時間過長,都有可能,需要具體問題具體分析,後面我會分享一些我的優化經驗。

2、在不對耗時優化下,將一幀的任務拆分到多幀內,保證每一幀都不超時。這也是本元件的設計思路,分幀渲染。

如下圖所示:

image.png

原理並不複雜,問題在於如何在 Flutter 中實踐這一機制。

因為涉及到幀與系統的排程,自然聯想到看 SchedulerBinding 中有無現成的 API。

發現了 scheduleTask 方法,這是系統提供的一個執行任務的方法,但這個方法存在兩個問題:

  • 1、其中的渲染任務是優先順序進行堆排序,而堆排序是不穩定排序,這會導致任務的執行順序並非 FIFO。從效果上來看,就是列表不會按照順序渲染,而是會出現跳動渲染的情況

  • 2、這個方法本身存在排程問題,我已經提交 issue 與 pr,不過一直卡在單元測試上,如果感興趣可以以在這裡交流談論。

fix: Tasks scheduled through 'SchedulerBinding.instance.scheduleTask'… #82781

最終,參考這個設計結合 endOfFrame 方法的使用,完成了分幀佇列。整個渲染流程變為下圖所示:

image.png

對於列表構建場景來說,假設螢幕上能顯示五個 item。首先在第一幀的時候,列表會渲染 5 個佔位的 Widget,同時新增 5 個高優先順序任務到佇列中,這裡的任務可以是簡單的將佔位 Widget 和實際 item進行替換,也可通過漸變等動畫提升體驗。在後續的五幀中佔位 Widget 依次被替換成實際的列表 item。

ListView流暢度翻倍!!Flutter卡頓分析和通用優化方案 這篇文章中有更加詳細的分析。


一些展示效果(Example 說明請檢視 Github

卡頓的頁面往往都是由多個複雜 widget 同時渲染導致。通過為複雜的 widget 巢狀分幀元件 FrameSeparateWidget。渲染時,分幀元件會在第一幀同時渲染多個 palceHolder,之後連續的多幀內依次渲染複雜子項,以此提升頁面流暢度。

例如 example 中的優化前示例:

ListView.builder(
              itemCount: childCount,
              itemBuilder: (c, i) => CellWidget(
                color: i % 2 == 0 ? Colors.red : Colors.blue,
                index: i,
              ),
            )
複製程式碼

其中 CellWidget 高度為 60,內部巢狀了三個 TextField 的元件(整體構建耗時在 9ms 左右)。

優化僅需為每一個 item 巢狀分幀元件,併為其設定 placeHolder(placeHolder 儘量簡單,樣式與實際 item 接近即可)。

在列表情況下,給 ListView 巢狀 SizeCacheWidget,同時建議將預載入範圍 cacheExtent 設定大一點,例如 500(該屬性預設為 250),提升慢速滑動時候的體驗。

Screenrecording_20210611_194905.gif (佔位與實際列表項不一致時,首次渲染抖動,二次渲染正常)

此外,也可以給 item 巢狀透明度/位移等動畫,優化視覺上的效果。

效果如下圖:

Screenrecording_20210315_133310.gifScreenrecording_20210315_133848.gif

分幀的成本

當然分幀方案也非十全十美,在我看來主要有兩點成本:

1、額外的構建開銷:整個構建過程的構建消耗由「n * widget消耗 」變成了「n *( widget + 佔位)消耗 + 系統排程 n 幀消耗」。可以看出,額外的開銷主要由佔位的複雜度決定。如果佔位只是簡單的 Container,測試後發現整體構建耗時大概提升在 15 % 左右。這種額外開銷對於當下的移動裝置而言,成本幾乎可以不計。

2、視覺上的變化:如同上面的演示,元件會將 item 分幀渲染,頁面在視覺上出現佔位變成實際 widget 的過程。但其實由於列表存在快取區域(建議將快取區調大),在高階機或正常滑動情況下使用者並無感知。而在中低端裝置上快速滑動能感覺到切換的過程,但比嚴重頓挫要好。


優化前後對比演示

注:gif 幀率只有20

優化前優化後
優化前優化後

最後:一點點思考

列表優化篇到此告一段落,在整個開源實踐過程中,有兩點感觸較深:

「點」與「面」的關係

我們在思考技術方案的時候可以由「點」到「面」,站在一個較高視野去想問題的本質。

而在執行的時候則需要由「面」到「點」的進行逐級拆分,抓住問題的關鍵節點,並且擬定進度計劃,逐步破解。

很多時候,這種向上和向下的邏輯思維才是我們的核心競爭力

以不變應萬變

對於未知的東西,我們往往會過度的將它想複雜。在一開始分析列表構建原理的時候,我也苦於無從下手,走了很多彎路。但其實對於 Flutter 這套 「UI」 框架而言,核心仍然在於三棵樹的構建機制

在這套體系內,抓住不變的東西,無論是生命週期、路由等等問題都可以從裡面找到答案。我之前也有過總結:Flutter 核心渲染機制Flutter路由設計與原始碼解析

下一階段,我會聚焦於 Dart 中的 I/O 部分,結合計算機網路原理由淺入深地進行分析與實踐。從底層原理出發,與大家一起學習 「不變的原理」,一起進步。如果你有任何疑問可以通過公眾號與聯絡我,如果文章對你有所啟發,希望能得到你的點贊、關注和收藏,這是我持續寫作的最大動力。Thanks~

如果遇到了任何問題與建議,歡迎在評論區或者公眾號聯絡我,或者 issue 和 pr。

公眾號:進擊的Flutter或者 runflutter 裡面整理收集了最詳細的Flutter進階與優化指南,歡迎關注。

相關文章