Flutter 效能優化 Tips

升級之路發表於2018-12-13

本文目的

  • 介紹應用流暢性的檢測和優化策略
  • 介紹記憶體的檢測和優化策略
  • 介紹效能優化證明的意義和流程
  • 介紹效能檢測工具 Observatory 的基礎使用

目錄結構

  • 流暢性
  • 記憶體優化
  • 優化證明
  • 效能檢測利器 Observatory 基礎使用
  • 總結

流暢性

App 流暢性的關鍵指標有 UI幀率,GPU幀率,我們期望它能達到 60fps,也就是16ms每幀。

以 profile / release 模式執行

為了獲取最接近生產環境的資料,我們應該選擇一臺儘可能低端的真機,並且以 profile 模式或者 release 模式下執行app。

  1. 因為 debug 模式會有一些額外的檢查工作,比如assert()
  2. 為了加速開發效率,debug 模式是以 JIT(Just in time)模式編譯 dart 程式碼的,而 profile 和 release 是提前編譯為機器碼 AOT(Ahead Of Time),所以 debug 會慢很多
  1. 在 Android Studio and IntelliJ 中, 在選單欄中點選 Run > Flutter Run main.dart in Profile Mode

  2. VS Code:開啟 launch.json 檔案並設定flutterMode 為 profile:

"configurations": [
	{
		"name": "Flutter",
		"request": "launch",
		"type": "dart",
		"flutterMode": "profile" # 測試完後記得把它改回去!
	}
]
複製程式碼
  1. 用命令列啟動:
$ flutter run --profile
複製程式碼

檢測幀率

那麼檢測幀率有哪些方法呢?Flutter 給我們提供了 Performance Overlay,如下圖,綠色代表當前渲染幀。

performance-overlay-green

我們有三種開啟方式

  1. 在Android Studio 和 IntelliJ IDEA中: 選中 View > Tool Windows > Flutter Inspector. 點選下面這個按鈕。

Flutter 效能優化 Tips

  1. 在 VS Code中 選中 View > Command Palette… 會顯示一個 command 皮膚. 在命令皮膚中輸入 performance 並選擇 Toggle Performance Overlay 如果命令顯示為不可用,需要檢查 app 是否正在執行.

  2. 從命令列中執行 鍵盤輸入P

  3. 程式碼中開啟 在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。如果大多數幀都在第一格,說明達到了期望的幀率。

performance overlay jank

圖表分別體現了 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, // 做了快取的靜態圖片在重新整理頁面時不會改變棋盤格的顏色;如果棋盤格顏色變了說明被重新快取了,這是我們要避免的
        ...
    );
    複製程式碼

提高流暢性的策略

  • 程式碼呼叫時機是否可以延後?如底部導航欄式的頁面,沒有必要第一次進入就把每個子Page都建立出來
  • 儘量做到區域性重新整理
  • 把耗時的計算放到獨立的isolate去執行
  • 檢查不必要的 saveLayer
  • 檢查靜態圖片是否新增快取
  • relayout boundary:參考
  • repaint boundary:參考

記憶體優化

在記憶體優化方面,我們的目標是希望減少應用記憶體佔用,減少被系統殺死的概率,同時儘可能的避免記憶體洩露,減少記憶體碎片化。

記憶體優化策略

  • 載入物件過大?如圖片質量和尺寸不做限制就載入
  • 載入物件過多?如載入長列表;在呼叫頻率很高的方法中建立物件
    • 合理設定快取大小/長度
    • 在記憶體不足時或離開頁面時清空快取資料
    • 使用ListView.build()來複用子控制元件
    • 自定義繪圖中避免在onDraw中做建立物件操作,或者相同的引數設定
    • 複用系統提供的資源,比如字串、圖片、動畫、樣式、顏色、簡單佈局,在應用中直接引用
  • 記憶體洩露的問題?比如dispose需要銷燬的listener等
  • 不可見的檢視是否也在build?
  • 頁面離開後的網路請求是否取消?

如何獲取記憶體狀態

Dart 提供了一個效能檢測工具Observatory,我在最後一部分會進行詳細介紹

優化證明

優化證明的意義

效能優化不像其它的開發需求只要完成功能即可,它需要通過統計和資料來證明優化的效果。比如幀率有了多少提高?CPU佔用率降低了多少?記憶體佔用減少了多少?對比其它優化策略,哪個優化效果好?

優化證明的流程

profile prove

舉個例子

以檢查流暢性為例

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.jsonhome_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種方式:

  1. 在 androidStudio 中開啟Flutter Inspector皮膚,點選小鬧鐘圖示,如下圖
    open observatory from AS
  2. 再命令列中執行flutter run,應用啟動成功後,命令列中會輸出一個 url,把 url copy 到瀏覽器即可。
    open observatory from command line

開啟Observatory皮膚,要先選擇isolate,表示當前應用。

entey screen

主要頁面

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

main screen

1. CPU Profile

app的時間都花在哪了?

進入這個頁面後要一般需載入個幾秒鐘,so be patient。圖表的下部按cpu佔用比例做了一個列表,反映的是函式的呼叫次數和執行時間(劃重點)。一般排在前面的函式(這些函式是?有待學習)都不是我們寫的dart程式碼。如果你發現自己的某個函式呼叫佔比反常,那麼可能存在問題。

注:flutter程式的cpu profile和官方文件上的資料展示不太一樣,沒有VM tags,所以對於百分比的具體含義有待研究。

cpu profile

取樣過程:它每隔一定時間對isolate做取樣,取樣的資料儲存在一個環形緩衝區(叫做profile),它能存放約2分鐘的資料,一旦緩衝區滿了,它會用最新的sample替換掉最舊的。

  • Profile contains:取樣時長和對應的取樣數
  • Sampling:取樣頻率,預設1000Hz,即每毫秒取樣一次

2. Allocation Profile

記憶體都被誰吃了?

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

知道哪些程式碼執行了,哪些沒有執行

code coverage

  • 綠色:已執行的程式碼
  • 紅色:未執行的程式碼
  • 沒有顏色:不可執行的程式碼

應用場景:寫某個類的單元測試,跑完測試後,可以檢視哪些程式碼沒有覆蓋到,進而補全

2. Class/Instance 資訊

檢視某個例項的狀態,比如我們的專案中使用了Flutter_redux,頁面的展示來源與狀態樹,當頁面出現了非預期的效果,我們可以通過Observatory檢視狀態樹

watch state

舉個例子

Observatory 幫我找到迴圈呼叫的真凶

總結

效能優化涉及了應用的方法面面,很難一言以蔽之。本文我們主要討論了效能優化的兩大主題 —— 流暢性和記憶體優化,並分別介紹了他們的檢測方法和優化策略。另外,我們在優化的同時也要加強優化的證明,用資料說話。最後,我強烈推薦大家嘗試一下 Observatory 這個工具,開發中如果遇到了奇怪的問題,沒準它能幫你找到答案。

參考

本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。@akindone

相關文章