「本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」
列表流暢度優化
這是一個通用的流暢度優化方案,通過分幀渲染優化由構建導致的卡頓,例如頁面切換或者複雜列表快速滾動的場景。
程式碼中 example 執行在 VIVO X23(驍龍 660),在相同的滾動操作下優化前後 200 幀採集資料指標對比(錄屏在文章最後):
優化前 | 優化後 |
---|---|
監控工具來自:fps_monitor,指標詳細資訊:頁面流暢度不再是謎!除錯神器開箱即用,Flutter FPS檢測工具
- 流暢:一幀耗時低於 18ms
- 良好:一幀耗時在 18ms-33ms 之間
- 輕微卡頓:一幀耗時在 33ms-67ms 之間
- 卡頓:一幀耗時大於 66.7ms
採用分幀優化後,卡頓次數從 平均 33.3 幀出現了一幀,降低到 200 幀中僅出現了一幀,峰值也從 188ms 降低到 90ms。卡頓現象大幅減輕,流暢幀佔比顯著提升,整體表現更流暢。下方是詳細資料。
優化前 | 優化後 | |
---|---|---|
平均多少幀出現一幀卡頓 | 33.3 | 200 |
平均多少幀出現一幀輕微卡頓 | 8.6 | 66.7 |
最大耗時 | 188.0ms | 90.0ms |
平均耗時 | 27.0ms | 19.4ms |
流暢幀佔比 | 40% | 64.5% |
頁面切換流暢度提升
在開啟一個頁面或者 Tab 切換時,系統會渲染整個頁面並結合動畫完成頁面切換。對於複雜頁面,同樣會出現卡頓掉幀。藉助分幀元件,將頁面的構建逐幀拆解,通過 DevTools 中的效能工具檢視。切換過程的峰值由 112.5ms 降低到 30.2 ms,整體切換過程更加流暢。
實際優化合集: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 ~
快速上手:
如下圖所示
假如現在頁面由 A、B、C、D 四部分組成,每部分耗時 10ms,在頁面時構建為 40ms。使用分幀元件 FrameSeparateWidget
巢狀每一個部分。頁面構建時會在第一幀渲染簡單的佔位,在後續四幀內分別渲染 A、B、C、D。
對於列表,在每一個 item 中巢狀 FrameSeparateWidget
,並將 ListView
巢狀在 SizeCacheWidget
內即可。
建構函式說明
FrameSeparateWidget :分幀元件,將巢狀的 widget 單獨一幀渲染
型別 | 引數名 | 是否必填 | 含義 |
---|---|---|---|
Key | key | 否 | |
int | index | 否 | 分幀元件 id,使用 SizeCacheWidget 的場景必傳,SizeCacheWidget 中維護了 index 對應的 Size 資訊 |
Widget | child | 是 | 實際需要渲染的 widget |
Widget | placeHolder | 否 | 佔位 widget,儘量設定簡單的佔位,不傳預設是 Container() |
SizeCacheWidget:快取子節點中,分幀元件巢狀的實際 widget 的尺寸資訊
型別 | 引數名 | 是否必填 | 含義 |
---|---|---|---|
Key | key | 否 | |
Widget | child | 是 | 子節點中如果包含分幀元件,則快取實際的 widget 尺寸 |
int | estimateCount | 否 | 預估螢幕上子節點的數量,提高快速滾動時的響應速度 |
方案設計與分析:
卡頓的本質,就是 單幀的繪製時間過長。基於此自然衍生出兩種思路解決:
1、減少一幀的繪製耗時,因為導致耗時過長的原因有很多,比如不合理的重新整理,或者繪製時間過長,都有可能,需要具體問題具體分析,後面我會分享一些我的優化經驗。
2、在不對耗時優化下,將一幀的任務拆分到多幀內,保證每一幀都不超時。這也是本元件的設計思路,分幀渲染。
如下圖所示:
原理並不複雜,問題在於如何在 Flutter 中實踐這一機制。
因為涉及到幀與系統的排程,自然聯想到看 SchedulerBinding
中有無現成的 API。
發現了 scheduleTask
方法,這是系統提供的一個執行任務的方法,但這個方法存在兩個問題:
-
1、其中的渲染任務是優先順序進行堆排序,而堆排序是不穩定排序,這會導致任務的執行順序並非 FIFO。從效果上來看,就是列表不會按照順序渲染,而是會出現跳動渲染的情況
-
2、這個方法本身存在排程問題,我已經提交 issue 與 pr,不過一直卡在單元測試上,如果感興趣可以以在這裡交流談論。
fix: Tasks scheduled through 'SchedulerBinding.instance.scheduleTask'… #82781
最終,參考這個設計結合 endOfFrame
方法的使用,完成了分幀佇列。整個渲染流程變為下圖所示:
對於列表構建場景來說,假設螢幕上能顯示五個 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),提升慢速滑動時候的體驗。
此外,也可以給 item 巢狀透明度/位移等動畫,優化視覺上的效果。
效果如下圖:
分幀的成本
當然分幀方案也非十全十美,在我看來主要有兩點成本:
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進階與優化指南,歡迎關注。