在上一篇文章中,我與你分享了調試 Flutter 代碼的 3 種基本方式,即輸出日志、斷點調試與布局調試。
通過可定制打印行為的 debugPrint 函數,我們可以實現生產環境與開發環境不同的日志輸出行為,從而保證在開發期打印的調試信息不會被發布至線上;借助于 IDE(Android Studio)所提供的斷點調試選項,我們可以不斷調整代碼執行步長和代碼暫停條件,收斂問題發生范圍,直至找到問題根源;而如果我們想找出代碼中的布局渲染類 Bug,則可以通過 Debug Painting 和 Flutter Inspector 提供的輔助線和視圖可視化信息,來更為精準地定位視覺問題。
除了代碼邏輯 Bug 和視覺異常這些功能層面的問題之外,移動應用另一類常見的問題是性能問題,比如滑動操作不流暢、頁面出現卡頓丟幀現象等。這些問題雖然不至于讓移動應用完全不可用,但也很容易引起用戶反感,從而對應用質量產生質疑,甚至失去耐心。
那么,如果應用渲染并不流暢,出現了性能問題,我們該如何檢測,又該從哪里著手處理呢?
在 Flutter 中,性能問題可以分為 GPU 線程問題和 UI 線程(CPU)問題兩類。這些問題的確認都需要先通過性能圖層進行初步分析,而一旦確認問題存在,接下來就需要利用 Flutter 提供的各類分析工具來定位問題了。
所以在今天這篇文章中,我會與你一起學習分析 Flutter 應用性能問題的基本思路和工具,以及常見的優化辦法。
## 如何使用性能圖層?
要解決問題,我們首先得了解如何去度量問題,性能分析也不例外。Flutter 提供了度量性能問題的工具和手段,來幫助我們快速定位代碼中的性能問題,而性能圖層就是幫助我們確認問題影響范圍的利器。
**為了使用性能圖層,我們首先需要以分析(Profile)模式啟動應用。**與調試代碼可以通過模擬器在調試模式下找到代碼邏輯 Bug 不同,性能問題需要在發布模式下使用真機進行檢測。
這是因為,相比發布模式而言,調試模式增加了很多額外的檢查(比如斷言),這些檢查可能會耗費很多資源;更重要的是,調試模式使用 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 線程的情況顯示在下面,藍色垂直的線條表示已執行的正常幀,綠色的線條代表的是當前幀:
:-: 
圖 1 性能圖層
為了保持 60Hz 的刷新頻率,GPU 線程與 UI 線程中執行每一幀耗費的時間都應該小于 16ms(1/60 秒)。在這其中有一幀處理時間過長,就會導致界面卡頓,圖表中就會展示出一個紅色豎條。下圖演示了應用出現渲染和繪制耗時的情況下,性能圖層的展示樣式:
:-: 
圖 2 渲染和繪制耗時異常
如果紅色豎條出現在 GPU 線程圖表,意味著渲染的圖形太復雜,導致無法快速渲染;而如果是出現在了 UI 線程圖表,則表示 Dart 代碼消耗了大量資源,需要優化代碼執行時間。
接下來,我們就先看看 GPU 問題定位吧。
## 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],
)
)
);
~~~
:-: 
圖 3 動態模糊效果
由于視圖滾動過程中頻繁涉及視圖蒙層效果的更新,因此 checkerboardOffscreenLayers 檢測圖層也感受到了對 GPU 的渲染壓力,頻繁的刷新閃爍。
:-: 
圖 4 檢測 saveLayer 使用
如果我們沒有對動態模糊效果的特殊需求,則可以使用不帶模糊效果的 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 檢測圖層也不再頻繁閃爍了。
:-: 
圖 5 去掉動態模糊效果
### checkerboardRasterCacheImages
從資源的角度看,另一類非常消耗性能的操作是,渲染圖像。這是因為圖像的渲染涉及 I/O、GPU 存儲,以及不同通道的數據格式轉換,因此渲染過程的構建需要消耗大量資源。為了緩解 GPU 的壓力,Flutter 提供了多層次的緩存快照,這樣 Widget 重建時就無需重新繪制靜態圖像了。
與檢查多視圖疊加渲染的 checkerboardOffscreenLayers 參數類似,Flutter 也提供了檢查緩存圖像的開關 checkerboardRasterCacheImages,來檢測在界面重繪時頻繁閃爍的圖像(即沒有靜態緩存)。
我們可以把需要靜態緩存的圖像加到 RepaintBoundary 中,RepaintBoundary 可以確定 Widget 樹的重繪邊界,如果圖像足夠復雜,Flutter 引擎會自動將其緩存,避免重復刷新。當然,因為緩存資源有限,如果引擎認為圖像不夠復雜,也可能會忽略 RepaintBoundary。
如下代碼展示了通過 RepaintBoundary,將一個靜態復合 Widget 加入緩存的具體用法。可以看到,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 后,我們就可以開始分析代碼中的性能問題了。
:-: 
圖 6 打開 Performance 工具
:-: 
圖 7 Performance 主界面
接下來,我們通過一個 ListView 中計算 MD5 的例子,來演示 Performance 的具體分析過程。
考慮到在 build 函數中進行渲染信息的組裝是一個常見的操作,為了演示這個知識點,我們故意放大了計算 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 的繁忙程度。
其中,y 軸表示調用棧,其每一層都是一個函數。調用棧越深,火焰就越高,底部就是正在執行的函數,上方都是它的父函數;x 軸表示單位時間,一個函數在 x 軸占據的寬度越寬,就表示它被采樣到的次數越多,即執行時間越長。
所以,我們要檢測 CPU 耗時問題,皆可以查看火焰圖底部的哪個函數占據的寬度最大。只要有“平頂”,就表示該函數可能存在性能問題。比如,我們這個案例的火焰圖如下所示:
:-: 
圖 8 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,這樣視圖的初始化時間就減少了。
## 思考題
最后,我給你留下一道思考題吧。
請你改造 ListView 計算 MD5 的示例,在保證原有功能的情況下,使用并發 Isolate(或 compute)完成 MD5 的計算。提示:計算過程可以使用 CircularProgressIndicator 來展示加載動畫。
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略