Flutter 效能優化 Tips
App 流暢性的關鍵指標有 UI幀率,GPU幀率,我們期望它能達到 60fps,也就是16ms每幀。
以 profile / release 模式執行
為了獲取最接近生產環境的資料,我們應該選擇一臺儘可能低端的真機,並且以 profile 模式或者 release 模式下執行app。
assert()
-
在 Android+Studio/">Android Studio and IntelliJ 中, 在選單欄中點選
Run > Flutter Run main.dart in Profile Mode
-
VS Code:開啟 launch.json 檔案並設定flutterMode 為 profile:
"configurations": [ { "name": "Flutter", "request": "launch", "type": "dart", "flutterMode": "profile" # 測試完後記得把它改回去! } ] 複製程式碼
- 用命令列啟動:
$ flutter run --profile 複製程式碼
檢測幀率
那麼檢測幀率有哪些方法呢?Flutter 給我們提供了 Performance Overlay
,如下圖,綠色代表當前渲染幀。

我們有三種開啟方式
- 在Android Studio 和 IntelliJ IDEA中: 選中
View > Tool Windows > Flutter Inspector
. 點選下面這個按鈕。

-
在 VS Code中 選中
View > Command Palette…
會顯示一個 command 面板. 在命令面板中輸入performance
並選擇Toggle Performance Overlay
如果命令顯示為不可用,需要檢查 app 是否正在執行. -
從命令列中執行 鍵盤輸入
P
-
程式碼中開啟 在
MaterialApp
或者WidgetsApp
的建構函式中設定showPerformanceOverlay
屬性為true
:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( showPerformanceOverlay: true, // 開啟 title: 'My Awesome App', home: MyHomePage(title: 'My Awesome App'), ); } } 複製程式碼
然後就是動手操作 app,並觀察圖表上是否出現紅色線條。綠色代表當前幀,當頁面有變動,圖表會不斷繪製。蒙版上有2個圖表,每個圖表上有三橫格,每個橫格代表16ms。如果大多數幀都在第一格,說明達到了期望的幀率。

圖表分別體現了 UI幀率 和 GPU幀率。如果出現了紅色,說明對應的執行緒有太多work要做。那先來了解一下 Flutter 中的4個主要執行緒分別承擔了什麼職責。
- Platform執行緒:外掛程式碼執行的執行緒;即Android/iOS的主執行緒,
- UI執行緒:在Dart虛擬機器中執行Dart程式碼。作用是建立檢視樹,然後將它傳送給GPU。注意不要阻塞此執行緒!
- GPU執行緒:把上面提到的檢視樹渲染出來,雖然我們在flutter中不能直接訪問GPU執行緒和資料,但是Dart程式碼可能導致此執行緒變慢
- I/O執行緒:執行比較耗時的任務
在執行app的過程中,觀察爆紅的地方和觸發場景,進行分析。
分析思路
- 如果是UI報紅:那麼可能是執行了某個較耗時的函式?或者函式呼叫過多?演算法複雜度高?
- 如果只是 GPU 報紅:那麼可能是要繪製的圖形過於複雜?或者執行了過多GPU操作?
- 比如要實現一個混合圖層的半透明效果:如果把透明度設定在頂層控制元件上,CPU會把每個子控制元件圖層渲染出來,再執行
saveLayer
操作儲存為一個圖層,最後給這個圖層設定透明度。而saveLayer
開銷很大,這裡官方給出了一個建議:首先確認這些效果是否真的有必要;如果有必要,我們可以把透明度設定到每個子控制元件上,而不是父控制元件。裁剪操作也是類似。 - 還有一個拖慢GPU渲染速度的是沒有給靜態影象做快取,導致每次build都會重新繪製。我們可以把靜態圖形加到
RepaintBoundry
控制元件中,引擎會自動判斷影象是否複雜到需要用repaint boundary,不需要的話也會忽略。 - 開啟saveLayer和圖形快取的檢查
MaterialApp( showPerformanceOverlay: true, checkerboardOffscreenLayers: true, // 使用了saveLayer的圖形會顯示為棋盤格式並隨著頁面重新整理而閃爍 checkerboardRasterCacheImages: true, // 做了快取的靜態圖片在重新整理頁面時不會改變棋盤格的顏色;如果棋盤格顏色變了說明被重新快取了,這是我們要避免的 ... ); 複製程式碼
- 比如要實現一個混合圖層的半透明效果:如果把透明度設定在頂層控制元件上,CPU會把每個子控制元件圖層渲染出來,再執行
提高流暢性的策略
- 程式碼呼叫時機是否可以延後?如底部導航欄式的頁面,沒有必要第一次進入就把每個子Page都創建出來
- 儘量做到區域性重新整理
- 把耗時的計算放到獨立的isolate去執行
- 檢查不必要的 saveLayer
- 檢查靜態圖片是否新增快取
- relayout boundary:參考
- repaint boundary:參考
記憶體優化
在記憶體優化方面,我們的目標是希望減少應用記憶體佔用,減少被系統殺死的概率,同時儘可能的避免記憶體洩露,減少記憶體碎片化。
記憶體優化策略
- 載入物件過大?如圖片質量和尺寸不做限制就載入
- 載入物件過多?如載入長列表;在呼叫頻率很高的方法中建立物件
- 合理設定快取大小/長度
- 在記憶體不足時或離開頁面時清空快取資料
- 使用ListView.build()來複用子控制元件
- 自定義繪圖中避免在onDraw中做建立物件操作,或者相同的引數設定
- 複用系統提供的資源,比如字串、圖片、動畫、樣式、顏色、簡單佈局,在應用中直接引用
- 記憶體洩露的問題?比如dispose需要銷燬的listener等
- 不可見的檢視是否也在build?
- 頁面離開後的網路請求是否取消?
如何獲取記憶體狀態
Dart 提供了一個性能檢測工具Observatory,我在最後一部分會進行詳細介紹
優化證明
優化證明的意義
效能優化不像其它的開發需求只要完成功能即可,它需要通過統計和資料來證明優化的效果。比如幀率有了多少提高?CPU佔用率降低了多少?記憶體佔用減少了多少?對比其它優化策略,哪個優化效果好?
優化證明的流程

舉個例子
以檢查流暢性為例
1. 在profile模式下執行並開啟Performance Overlay,整體測試app
2. 找到幀率報紅色的模組
3. 把頁面孤立出來,並多次測量,並得到baseline(參照)幀率資料。比如長列表頁面出現了卡頓,我們可以用TestDriver寫一個ListView滑動的效能測試(更多參考 ofollow,noindex">Flutter gallery )
scroll_pref.dart
void main() { enableFlutterDriverExtension(); runApp(const GalleryApp(testMode: true)); } 複製程式碼
scroll_perf_test.dart
void main() { group('scrolling performance test', () { FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('measure', () async { final Timeline timeline = await driver.traceAction(() async { await driver.tap(find.text('Material')); final SerializableFinder demoList = find.byValueKey('GalleryDemoList'); for (int i = 0; i < 5; i++) { await driver.scroll(demoList, 0.0, -300.0, const Duration(milliseconds: 300)); await Future<void>.delayed(const Duration(milliseconds: 500)); } // Scroll up for (int i = 0; i < 5; i++) { await driver.scroll(demoList, 0.0, 300.0, const Duration(milliseconds: 300)); await Future<void>.delayed(const Duration(milliseconds: 500)); } }); TimelineSummary.summarize(timeline) ..writeSummaryToFile('home_scroll_perf', pretty: true) ..writeTimelineToFile('home_scroll_perf', pretty: true); }); }); } 複製程式碼
在命令列下執行以下命令
flutter driver --target=test_driver/scroll_perf.dart 複製程式碼
這個命令會:
- build 目標 app,並把它安裝到裝置上
- 執行位於
test_driver/
目錄下的scroll_perf_test.dart
的測試( flutter drive 能幫你找到帶_test
字尾的同名檔案)
Test Driver 將會安裝 app 到裝置上,再跳轉到 Material-GalleryDemoList 頁面,做5次滑動列表的操作。執行完成後會藉助 TimelineSummary
,在build目錄下生成兩個json檔案: home_scroll_perf.timeline.json
和 home_scroll_perf.timeline_summary.json
。這裡我們看一下 timeline_summary.json
檔案的內容
{ "average_frame_build_time_millis": 5.6319655172413805, # 平均每幀 build 時間 "90th_percentile_frame_build_time_millis": 10.216, "99th_percentile_frame_build_time_millis": 17.168, "worst_frame_build_time_millis": 20.415, # 最長幀 build 時間 "missed_frame_build_budget_count": 21, # build 期丟幀數 "average_frame_rasterizer_time_millis": 14.234294964028772, # 平均每幀光柵化時間 "90th_percentile_frame_rasterizer_time_millis": 22.338, "99th_percentile_frame_rasterizer_time_millis": 42.661, "worst_frame_rasterizer_time_millis": 43.161, "missed_frame_rasterizer_budget_count": 112, "frame_count": 116, "frame_build_times": [ ... ],# 所有幀的 build 時間 "frame_rasterizer_times": [ ... ] # 所有幀的光柵化時間 } 複製程式碼
4. 優化
5. 用步驟3的方法再次測量,對比baseline得出確切的優化效果
Flutter 提供的效能除錯 API
更多可以參考官方文件
效能檢測利器 Observatory
Observatory 是用於分析和除錯Dart應用程式的工具。Observatory允許您根據需要檢視正在執行的Dart虛擬機器(VM),並提供實時,即時的資料報告。您可以使用它來瀏覽應用程式的很多狀態。
開啟Observatory
有2種方式:
- 在 androidStudio 中開啟
Flutter Inspector
面板,點選小鬧鐘圖示,如下圖 - 再命令列中執行
flutter run
,應用啟動成功後,命令列中會輸出一個 url,把 url copy 到瀏覽器即可。
開啟Observatory面板,要先選擇isolate,表示當前應用。

主要頁面
下面是效能優化常關注的幾個頁面。

1.CPU Profile
app的時間都花在哪了?
進入這個頁面後要一般需載入個幾秒鐘,so be patient。圖表的下部按cpu佔用比例做了一個列表,反映的是函式的呼叫次數和執行時間(劃重點)。一般排在前面的函式(這些函式是?有待學習)都不是我們寫的dart程式碼。如果你發現自己的某個函式呼叫佔比反常,那麼可能存在問題。
注:flutter程式的cpu profile和官方文件上的資料展示不太一樣,沒有VM tags,所以對於百分比的具體含義有待研究。

取樣過程:它每隔一定時間對isolate做取樣,取樣的資料儲存在一個環形緩衝區(叫做profile),它能存放約2分鐘的資料,一旦緩衝區滿了,它會用最新的sample替換掉最舊的。
- Profile contains:取樣時長和對應的取樣數
- Sampling:取樣頻率,預設1000Hz,即每毫秒取樣一次
2.Allocation Profile
記憶體都被誰吃了?

Heap 堆,動態分配的Dart物件所在的記憶體空間
- New generation: 新建立的物件,一般來說物件比較小,生命週期短,如local 變數。在這裡GC活動頻繁
- Old generation:從GC中存活下來的New generation將會提拔到老生代Old generation,它比新生代空間大,更適合大的物件和生命週期長的物件
通過這個面板你能看到新生代/老生代的記憶體大小和佔比;每個型別所佔用的記憶體大小。
為了debug的方便,我們可以獲取到某段時間的記憶體分配情況:點選Reset Accumulator按鈕,把資料清零,執行一下要測試的程式,點選重新整理。
為了檢查記憶體洩露,我們可以點選GC按鈕,手動執行GC。
Accumulator Size:自點選Reset Accumulator以來,累加物件佔用記憶體大小 Accumulator Instances:自點選Reset Accumulator以來,累加例項個數 Current Size:當前物件佔用記憶體大小 Current Instances:當前物件數量
3.Heap Map
是否出現記憶體碎片化
heap map 面板能檢視old generation中的記憶體狀態
它以顏色顯示記憶體塊。 每個記憶體頁面(page of memory)為256 KB,每頁由水平黑線分隔。 畫素的顏色表示物件的類ID - 例如,藍色表示字串,綠色表示雙精度表。 可用空間為白色,指令(程式碼)為紫色。 如果啟動垃圾收集(使用“分配配置檔案”螢幕中的GC按鈕),堆對映中將顯示更多空白區域(可用空間)。 將游標懸停在上面時,頂部的狀態列顯示有關游標下畫素所代表的物件的資訊。 顯示的資訊包括該物件的型別,大小和地址。 當你看到白色區域中有很多分散的其它顏色,說明存在記憶體碎片化,可能是記憶體洩露導致的。
其它
1.Code Coverage
知道哪些程式碼執行了,哪些沒有執行

- 綠色:已執行的程式碼
- 紅色:未執行的程式碼
- 沒有顏色:不可執行的程式碼
應用場景:寫某個類的單元測試,跑完測試後,可以檢視哪些程式碼沒有覆蓋到,進而補全