概述
軟體專案的交付是一個複雜且漫長的過程,任何細小的失誤都有可能導致交付過程失敗。在軟體開發過程中,除了程式碼邏輯的 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 產生了卡頓(跳幀),這些圖表可以幫助我們分析並找到原因,如下圖所示。

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

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效果如下圖所示。

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 後,我們就可以開始分析程式碼中的效能問題了。


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 耗時問題,皆可以檢視火焰圖底部的哪個函式佔據的寬度最大。只要有“平頂”,就表示該函式可能存在效能問題,如下圖所示。

總結
在 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,以後會封殺嗎