本文目的
- 介紹應用流暢性的檢測和優化策略
- 介紹記憶體的檢測和優化策略
- 介紹效能優化證明的意義和流程
- 介紹效能檢測工具 Observatory 的基礎使用
目錄結構
- 流暢性
- 記憶體優化
- 優化證明
- 效能檢測利器 Observatory 基礎使用
- 總結
流暢性
App 流暢性的關鍵指標有 UI幀率,GPU幀率,我們期望它能達到 60fps,也就是16ms每幀。
以 profile / release 模式執行
為了獲取最接近生產環境的資料,我們應該選擇一臺儘可能低端的真機,並且以 profile 模式或者 release 模式下執行app。
- 因為 debug 模式會有一些額外的檢查工作,比如
assert()
等- 為了加速開發效率,debug 模式是以 JIT(Just in time)模式編譯 dart 程式碼的,而 profile 和 release 是提前編譯為機器碼 AOT(Ahead Of Time),所以 debug 會慢很多
-
在 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滑動的效能測試(更多參考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
知道哪些程式碼執行了,哪些沒有執行
- 綠色:已執行的程式碼
- 紅色:未執行的程式碼
- 沒有顏色:不可執行的程式碼
應用場景:寫某個類的單元測試,跑完測試後,可以檢視哪些程式碼沒有覆蓋到,進而補全
2. Class/Instance 資訊
檢視某個例項的狀態,比如我們的專案中使用了Flutter_redux,頁面的展示來源與狀態樹,當頁面出現了非預期的效果,我們可以通過Observatory檢視狀態樹
舉個例子
總結
效能優化涉及了應用的方法面面,很難一言以蔽之。本文我們主要討論了效能優化的兩大主題 —— 流暢性和記憶體優化,並分別介紹了他們的檢測方法和優化策略。另外,我們在優化的同時也要加強優化的證明,用資料說話。最後,我強烈推薦大家嘗試一下 Observatory 這個工具,開發中如果遇到了奇怪的問題,沒準它能幫你找到答案。
參考
本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。@akindone