Flutter 應用效能檢測與優化

xiangzhihong發表於2020-03-29

概述

軟體專案的交付是一個複雜且漫長的過程,任何細小的失誤都有可能導致交付過程失敗。在軟體開發過程中,除了程式碼邏輯的 Bug 和視覺異常這些功能層面的問題之外,移動應用另一類常見的問題是效能問題,比如滑動操作不流暢、頁面出現卡頓丟幀現象等。這些問題雖然不至於讓移動應用完全不可用,但也很容易引起使用者反感,從而對應用質量產生質疑,甚至失去耐心。

那麼,對於應用渲染並不流暢,出現了效能問題,我們該如何檢測,又該從哪裡著手處理呢?和移動開發類似, Flutter 的效能問題主要可以分為 GPU 執行緒問題和 UI 執行緒(CPU)問題兩類。對於這些問題,有一個通用的套路:首先,都需要先通過效能圖層進行初步分析,而一旦確認問題存在,接下來就是利用 Flutter 提供的各類分析工具來進行問題定位。

圖層分析

Flutter執行模式

1、Debug

Debug模式可以在真機和模擬器上同時執行,此模式會開啟所有的斷言,包括debugging資訊、debugger aids(比如observatory)和服務擴充套件。優化了快速develop/run迴圈,但是沒有優化執行速度、二進位制大小和部署。命令flutter run就是以這種模式執行的,通過sky/tools/gn --android或者sky/tools/gn --ios來構建應用的。

2、Release

Release模式只能在真機上執行,不能在模擬器上執行:會關閉所有斷言和debugging資訊,關閉所有debugger工具。優化了快速啟動、快速執行和減小包體積。禁用所有的debugging aids和服務擴充套件。這個模式是為了部署給最終的使用者使用。命令flutter run --release就是以這種模式執行的,通過sky/tools/gn --android --runtime-mode=release或者sky/tools/gn --ios --runtime-mode=release來構建應用。

3、Profile

Profile模式只能在真機上執行,不能在模擬器上執行,基本和Release模式一致,除了啟用了服務擴充套件和tracing,以及一些為了最低限度支援tracing執行的東西(比如可以連線observatory到程式)。命令flutter run --profile就是以這種模式執行的,通過sky/tools/gn --android --runtime-mode=profile或者sky/tools/gn --ios --runtime-mode=profile來構建應用。

4、test

headless test模式只能在桌面上執行,基本和Debug模式一致,除了是headless的而且你能在桌面執行。命令flutter test就是以這種模式執行的,通過sky/tools/gn來build。

在實際開發中,應該用到上面所說的四種模式又各自分為兩種:一種是未優化的模式,供開發人員除錯使用;一種是優化過的模式,供最終的開發人員使用。預設情況下是未優化模式,如果要開啟優化模式,build的時候在命令列後面新增--unoptimized引數。

不管是移動開發還是前端開發,對於效能問題分析的思路都是先分析並定位問題,Flutter也不例外,藉助Flutter 提供的度量效能工具,我們可以快速定位程式碼中的效能問題,而效能圖層就是幫助我們確認問題影響範圍的利器,它類似Android的圖層分析工具。

為了使用效能圖層,Flutter提供了分析(Profile)模式,與除錯程式碼可以通過模擬器在除錯模式下找到程式碼邏輯 Bug 不同,效能問題需要在釋出模式下使用真機進行檢測。相比釋出(Release)模式而言,除錯模式增加了很多額外的檢查(比如斷言),這些檢查可能會耗費很多資源;更重要的是,除錯模式使用 JIT (即時編譯)模式執行應用,程式碼執行效率較低。這就使得除錯模式執行的應用,無法真實反映出它的效能問題。

而另一方面,模擬器使用的指令集為 x86,而真機使用的指令集是 ARM,由於這兩種方式的二進位制程式碼執行行為完全不同,因此模擬器與真機的效能差異較大。一些 x86 指令集擅長的操作模擬器會比真機快,而另一些操作則會比真機慢,這也使得我們無法使用模擬器來評估真機才能出現的效能問題。

為了除錯效能問題,我們需要在釋出模式的基礎之上,為分析工具提供少量必要的應用追蹤資訊,這就是分析模式。除了一些除錯效能問題必須的追蹤方法之外,Flutter 應用的分析模式和釋出模式的編譯和執行是類似的,只是啟動引數變成了 profile 而已。我們可以在 Android Studio 中通過選單欄點選 【Run】-【Profile 】‘main.dart’ 選項啟動應用,也可以通過命令列引數 flutter run --profile 執行 Flutter 應用。

渲染問題分析

在完成了應用啟動之後,接下來我們就可以利用 Flutter 提供的渲染問題分析工具,即效能圖層(Performance Overlay)來分析渲染問題了。效能圖層會在當前應用的最上層,以 Flutter 引擎自繪的方式展示 GPU 與 UI 執行緒的執行圖表,而其中每一張圖表都代表當前執行緒最近 300 幀的表現,如果 UI 產生了卡頓(跳幀),這些圖表可以幫助我們分析並找到原因,如下圖所示。

在這裡插入圖片描述
上圖演示了效能圖層的展現樣式。其中,GPU 執行緒的效能情況在上面,UI 執行緒的情況顯示在下面,藍色垂直的線條表示已執行的正常幀,綠色的線條代表的是當前幀。

同時,為了保持 60Hz 的重新整理頻率,GPU 執行緒與 UI 執行緒中執行每一幀耗費的時間都應該小於 16ms(1/60 秒)。在這其中有一幀處理時間過長,就會導致介面卡頓,圖表中就會展示出一個紅色豎條,如下圖所示。

在這裡插入圖片描述
如果紅色豎條出現在 GPU 執行緒圖表,意味著渲染的圖形太複雜,導致無法快速渲染;而如果是出現在了 UI 執行緒圖表,則表示 Dart 程式碼消耗了大量資源,需要優化程式碼執行時間。

GPU問題定位

GPU渲染問題主要集中在底層渲染耗時上,有時候 Widget 樹雖然構造起來容易,但在 GPU 執行緒下的渲染卻很耗時。例如,涉及 Widget 裁剪、蒙層這類多檢視疊加渲染,或是由於缺少快取導致靜態影象的反覆繪製,都會明顯拖慢 GPU 的渲染速度。

接下來,使用效能圖層提供的兩項引數,即檢查多檢視疊加的檢視渲染開關 checkerboardOffscreenLayers和檢查快取的影象開關checkerboardRasterCacheImages來檢查這兩種情況。

checkerboardOffscreenLayers

多檢視疊加通常會用到 Canvas 裡的 savaLayer 方法,這個方法在實現一些特定的效果(比如半透明)時非常有用,但由於其底層實現會在 GPU 渲染上涉及多圖層的反覆繪製,因此會帶來較大的效能問題。

對於 saveLayer 方法使用情況的檢查,我們只需要在 MaterialApp 的初始化方法中,將 checkerboardOffscreenLayers 開關設定為 true,分析工具就會自動幫我們檢測多檢視疊加的情況。使用了 saveLayer 的 Widget 會自動顯示為棋盤格式,並隨著頁面重新整理而閃爍。不過,saveLayer 是一個較為底層的繪製方法,因此我們一般不會直接使用它,而是會通過一些功能性 Widget,在涉及需要剪下或半透明蒙層的場景中間接地使用。所以一旦遇到這種情況,我們需要思考一下是否一定要這麼做,能不能通過其他方式來實現呢?

比如下面的例子中,我們使用 CupertinoPageScaffold 與 CupertinoNavigationBar 實現了一個動態模糊的效果,程式碼如下:


CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(),//動態模糊導航欄
    child: ListView.builder(
      itemCount: 100,
      //為列表建立100個不同顏色的RowItem
      itemBuilder: (context, index)=>TabRowItem(
            index: index,
            lastItem: index == 100 - 1,
            color: colorItems[index],//設定不同的顏色
            colorName: colorNameItems[index],
          )
    )
);
複製程式碼

其中,動態模糊的NavigationBar效果如下圖所示。

在這裡插入圖片描述
當我們開啟checkerboardOffscreenLayers之後,可以看到檢視蒙層效果對GPU的渲染壓力導致效能檢視頻繁閃動。如果我們沒有對動態模糊效果有特殊需求,則可以使用不帶模糊效果的 Scaffold 和白色的 AppBar 實現同樣的產品功能,來解決這個效能問題。


Scaffold(
  //使用普通的白色AppBar
  appBar: AppBar(title: Text('Home', style: TextStyle(color:Colors.black),),backgroundColor: Colors.white),
  body: ListView.builder(
      itemCount: 100,
      //為列表建立100個不同顏色的RowItem
      itemBuilder: (context, index)=>TabRowItem(
        index: index,
        lastItem: index == 100 - 1,
        color: colorItems[index],//設定不同的顏色
        colorName: colorNameItems[index],
      )
  ),
);
複製程式碼

執行一下程式碼,可以看到,在去掉了動態模糊效果之後,GPU 的渲染壓力得到了緩解,checkerboardOffscreenLayers 檢測圖層也不再頻繁閃爍了。

在這裡插入圖片描述

checkerboardRasterCacheImages

從資源的角度看,另一類非常消耗效能的操作是渲染影象,因為影象渲染會涉及 I/O、GPU 儲存以及不同通道的資料格式轉換,因此渲染過程的構建需要消耗大量資源。為了緩解 GPU 的壓力,Flutter 提供了多層次的快取快照,這樣 Widget 重建時就無需重新繪製靜態影象了。

與檢查多檢視疊加渲染的 checkerboardOffscreenLayers 引數類似,Flutter 提供了檢查快取影象的開關 checkerboardRasterCacheImages,來檢測在介面重繪時頻繁閃爍的影象。

為了提高靜態影象顯示效能,我們可以把需要靜態快取的影象加到 RepaintBoundary 中,RepaintBoundary 可以確定 Widget 樹的重繪邊界,如果影象足夠複雜,Flutter 引擎會自動將其快取,從而避免重複重新整理。當然,因為快取資源有限,如果引擎認為影象不夠複雜,也可能會忽略 RepaintBoundary。下面的程式碼展示了通過 RepaintBoundary,將一個靜態複合 Widget 加入快取的具體用法,如下所示。


RepaintBoundary(//設定靜態快取影象
  child: Center(
    child: Container(
      color: Colors.black,
      height: 10.0,
      width: 10.0,
    ),
));
複製程式碼

UI 執行緒問題定位

如果說 GPU 執行緒問題定位的是渲染引擎底層渲染異常,那麼 UI 執行緒問題發現的則是應用的效能瓶頸。比如在檢視構建時,在 build 方法中使用了一些複雜的運算,或是在主 Isolate 中進行了同步的 I/O 操作。這些問題,都會明顯增加 CPU 的處理時間,拖慢應用的響應速度。

針對這類問題,我們可以使用 Flutter 提供的 Performance 工具,來記錄應用的執行軌跡。Performance 是一個強大的效能分析工具,能夠以時間軸的方式展示 CPU 的呼叫棧和執行時間,去檢查程式碼中可疑的方法呼叫。

開啟 Android Studio 底部工具欄中的“Open DevTools”按鈕之後,系統會自動開啟 Dart DevTools 的網頁,將頂部的 tab 切換到 Performance 後,我們就可以開始分析程式碼中的效能問題了。

在這裡插入圖片描述
在這裡插入圖片描述
接下來,我們通過一個在 ListView 中計算 MD5 的例子來演示 Performance 的具體分析過程。考慮到在 build 函式中進行渲染資訊的組裝是一個常見的操作,為了演示Performance的使用過程,我們故意放大計算 MD5 的耗時,如迴圈迭代計算了 1 萬次。


class MyHomePage extends StatelessWidget {
  MyHomePage({Key key}) : super(key: key);

  String generateMd5(String data) {
    //MD5固定演算法
    var content = new Utf8Encoder().convert(data);
    var digest = md5.convert(content);
    return hex.encode(digest.bytes);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('demo')),
      body: ListView.builder(
          itemCount: 30,// 列表元素個數
          itemBuilder: (context, index) {
            //反覆迭代計算MD5
            String str = '1234567890abcdefghijklmnopqrstuvwxyz';
            for(int i = 0;i<10000;i++) {
              str = generateMd5(str);
            }
            return ListTile(title: Text("Index : $index"), subtitle: Text(str));
          }// 列表項建立方法
      ),
    );
  }
}
複製程式碼

與效能圖層能夠自動記錄應用執行情況不同,使用 Performance 來分析程式碼執行軌跡,我們需要手動點選【Record】按鈕去主動觸發,在完成資訊的抽樣採集後再點選【Stop】按鈕結束錄製,然後就可以得到在這期間應用的執行情況了。

Performance 記錄的應用執行情況叫做 CPU 幀圖,又被稱為火焰圖。火焰圖是基於記錄程式碼執行結果所產生的圖片,用來展示 CPU 的呼叫棧,表示的是 CPU 的繁忙程度。所以,我們要檢測 CPU 耗時問題,皆可以檢視火焰圖底部的哪個函式佔據的寬度最大。只要有“平頂”,就表示該函式可能存在效能問題,如下圖所示。

在這裡插入圖片描述
可以看到,_MyHomePage.generateMd5 函式的執行時間最長,幾乎佔滿了整個火焰圖的寬,而這也與程式碼中存在的問題是一致的。在找到了問題之後,我們就可以使用 Isolate(或 compute)將這些耗時的操作挪到併發主 Isolate 之外去完成了。

總結

在 Flutter 中,效能分析過程可以分為 GPU 執行緒問題定位和 UI 執行緒(CPU)問題定位,而它們都需要在真機上以分析模式(Profile)啟動應用,並通過效能圖層分析大致的渲染問題範圍。 一旦確認問題存在,接下來就需要利用 Flutter 所提供的分析工具來定位問題原因了。關於 GPU 執行緒渲染問題,我們可以重點檢查應用中是否存在多檢視疊加渲染,或是靜態影象反覆重新整理的現象。而 UI 執行緒渲染問題,我們則是通過 Performance 工具記錄的火焰圖(CPU 幀圖),分析程式碼耗時來找出應用執行瓶頸。

總的來說,由於 Flutter 採用基於宣告式的 UI 設計理念,以資料驅動渲染,並採用 Widget->Element->RenderObject 三層結構,遮蔽了無謂的介面重新整理,能夠保證絕大多數情況下我們構建的應用都是高效能的,所以在使用分析工具檢測出效能問題之後,通常我們並不需要做太多的細節優化工作,只需要在改造過程中避開一些常見的坑,就可以獲得優異的效能。同時,為了避免造成效能問題,還應該從以下幾個方面著手:

  • 控制 build 方法耗時,將 Widget 拆小,避免直接返回一個巨大的 Widget,這樣 Widget 會享有更細粒度的重建和複用;
  • 儘量不要為 Widget 設定半透明效果,而是考慮用圖片的形式代替,這樣被遮擋的 Widget 部分割槽域就不需要繪製了;
  • 對列表採用懶載入而不是直接一次性建立所有的子 Widget,這樣檢視的初始化時間就減少了。

參考資料

1,Flutter 應用程式除錯
2,Flutter For Web入門實戰
3,Flutter開發之路由與導航
4,Flutter 必備開源專案
5,Flutter混合開發
6,Flutter的Hot Reload是如何做到的
7,《Flutter in action》開源
8,Flutter開發之JSON解析
9,Flutter開發之基礎Widgets
10,Flutter開發之導航與路由管理
11,Flutter開發之網路請求
12,Flutter基礎知識
13,Flutter開發之Dart語言基礎
14,Flutter入門與環境搭建
15,移動跨平臺方案對比:WEEX、React Native、Flutter和PWA
16,Flutter開發之非同步程式設計
17,構建屬於自己的Flutter混合開發框架
18,Flutter應用整合極光推送
19,Flutter 國際化適配實戰
20,Apple為什麼不封殺 Flutter,以後會封殺嗎

相關文章