前言
Flutter自誕生之時就以輕鬆構建美觀、高效能元件著稱,目標是提供逼近“原生效能”的60幀每秒(fps)的效能,或者是在可以達到120Hz的裝置上提供120fps的效能。這裡的幀率fps是指的畫面每秒傳輸幀數,是衡量效能優化中螢幕是否卡頓的一個重要指標,如何測量一個應用的幀率,就要用到工具Timeline。
注:關於Performance效能指標的描述有多個方面,本文側重點為Timeline
Flutter 效能分析效能
flutter 支援三種模式編譯的app,處於開發的不同階段,使用不同模式下的app
除錯模式
在命令列下輸入flutter run
,預設會啟動debug模式,該模式下app使用JIT的編譯方式,執行時去解析執行程式,意味著應用有著較慢的效能體驗,比如冷啟動或者第一次初始化Flutter Engine會有較長時間的黑屏、使用過程中掉幀和卡頓這都屬於正常現象。
該模式下一個突出的特點是可以熱過載,所以更像開發一個前端應用
Release模式
命令列輸入flutter run --release
會使用 Release 模式來進行編譯,該模式下應用具有最大的優化和效能體驗,採用AOT的編譯技術,由dart本地虛擬機器將程式碼編譯成對應平臺例如Android、IOS對應的機器碼,相當於原生開發,編譯耗時,失去了熱過載,但具有良好效能。一般用於最後應用市場發包,失去了debug模式下的各種除錯應用功能
Profile模式
命令列輸入flutter run --profile
會使用Profile模式編譯,一個專門的除錯應用效能模式,該模式是在保留一部分除錯能力的基礎上,又較大程度還原app真實效能,所以該模式不建議在虛擬機器或模擬器上執行,因為無法真實代表真機效能。
輸入該命令,執行完以後控制檯會列印除錯的地址
點選即可跳轉到除錯頁
這裡選擇timeline,點選Flutter Developer按鈕,即可進入timeline的除錯頁面,在手機上操作幾秒鐘,點選右上角Refresh按鈕,即可載入出影像,看頁面程式碼應該也是臨時借用的systemtrace的,操作都類似
這個其實是以前舊版的除錯方式,現在雖然也能使用,但是實際測試中發現不太準確,使用也不方便,現在基本不使用這種方式了,新版本的Performance頁面很美觀使用也方便,可以在Android studio中使用Flutter Performance外掛中頁面粗略判斷timeline是否卡頓,也可以開啟Flutter Performance右下角Open DevTools按鈕在網頁上具體分析。
Flutter Inspector
這裡順便說下Android Studio中其他兩個Flutter 外掛,一個是Flutter Outline
,即顯示頁面佈局的大綱,可以快速檢視頁面佈局的樹形結構,選單欄提供了包裹、刪除、上下移動元件的快捷功能,很簡單這裡不詳細介紹。另一個外掛是Flutter Inspector
,安裝Flutter外掛後,AS右側邊欄會出現這個標籤,主要作用是開發過程中佈局除錯用的,類似Android開發裡的Xml佈局檢視工具,只不過需要debug模式執行時才可以檢視佈局,這點相對於原生開發xml快速定位佈局檔案的體驗還是差一些,相信後期還會有好的優化。
點選標籤後頁面如下
頁面主要分上邊功能按鈕、左邊View樹、右邊佈局預覽。
每個按鈕的作用:
Select Widget Mode
選擇元件模式,選中後點選下方View Tree中某個Widget自動定位到程式碼位置,可在開發中快速定位程式碼
Refresh Tree
重新整理View Tree,在App上跳轉其他頁面後,View Tree不自動更新,所以有了此按鈕
Slow Animations
放慢動畫
Debug Paint
顯示佈局測量,可以快速確定元件邊界,效果如下
Show Paint Baselines
顯示Text元件的Baseline,方便文字對齊
Show Repaint Rainbow
顯示重繪時顏色變化
Invert Oversized Images
輕鬆檢視解析度比顯示解析度高的圖片
Flutter Performance
點選右邊欄Flutter Performance,出現如下頁面:
左上角第一個按鈕是在手機上顯示performance Overlay,效果如下,其他按鈕和上邊Inspector中一樣
上邊可以粗略判斷App是否掉幀,白色正常,紅色就卡頓了,中間記憶體佔用,下邊是每個元件的重繪狀態,點選右下角的Open DevTools可以使用更多功能
Track widget rebuilds
核取方塊勾上可以方便的檢視頁面中元件的重繪狀態,對於不應該重繪的元件應該調整程式碼層級結構或者抽離元件的方式避免重繪造成效能的損失,這裡分享個人在開發中總結的幾點經驗:
- 儘量少使用StatefulWidget編寫大的頁面,儘量避免在StatefulWidget中使用setState
- 不需要重繪元件新增const關鍵字
- Provider重新整理機制時使用Consumer下沉重新整理範圍
- 小部件需要重新整理抽取成StatefulWidget,縮小重新整理範圍
Timeline
時間線事件圖表顯示了應用程式中的所有事件跟蹤。 Flutter框架在構建框架,繪製場景以及跟蹤其他活動(例如HTTP流量)時會發出時間軸事件。這些事件顯示在時間軸上。您還可以通過dart傳送自己的時間線事件:developer Timeline和 TimelineTask API
Timeline 事件軌跡的格式和檢視器並被許多其他專案使用,此類專案包括 Chromium & Android (via systrace).
軌跡記錄的形式是JSON檔案格式儲存的,點選右上角的Export按鈕可以匯出檔案。
開啟DevTools以後,在App上操作一段,點選左上角Refresh按鈕即可載入出如下圖所示時間線。
圖中藍色條是正常幀,紅色條是卡頓幀,滑鼠移動到紅條上可以檢視當前卡頓幀的耗時,右上角有不同顏色條的對應關係,分別有UI、Raster、Jank。
UI
UI執行緒在Dart VM中執行Dart程式碼。這包括您的應用程式以及Flutter框架中的程式碼。當您的應用建立並顯示場景時,UI執行緒將建立一個層樹(包含與裝置無關的繪畫命令的輕量級物件),並將該層樹傳送到要在裝置上呈現的柵格執行緒。不要阻塞該執行緒。
Raster
光柵執行緒(以前稱為GPU執行緒)執行Flutter Engine中的圖形程式碼。該執行緒獲取層樹並通過與GPU(圖形處理單元)對話來顯示它。您無法直接訪問柵格執行緒或其資料,但是如果該執行緒速度很慢,則是由於您在Dart程式碼中所做的操作所致。圖形庫Skia在此執行緒上執行。
有時,場景會產生易於構造的圖層樹,但是在柵格執行緒上渲染的樹代價很高。在這種情況下,您需要弄清楚程式碼正在做什麼,這會導致渲染程式碼變慢。對於GPU而言,特定種類的工作負載更加困難。它們可能涉及對saveLayer()的不必要呼叫,與多個物件相交的不透明性以及在特定情況下的剪輯或陰影
Jank
幀渲染圖顯示帶有紅色疊加層的垃圾幀。如果一個幀完成的時間超過約16毫秒(對於60 FPS裝置),則該幀被認為是過時的。為了達到60 FPS(每秒幀)的幀渲染速率,每個幀必須在約16 ms或更短的時間內渲染。錯過此目標時,您可能會遇到UI混亂或掉幀的情況
Render Frames
當一個Flutter應用或者Flutter Engine啟動時,它會啟動(或者從池中選擇)另外三個執行緒,這些執行緒有些時候會有重合的工作點,但是通常,它們被稱為UI執行緒
,GPU執行緒
,IO執行緒
。UI、GPU之間的工作流程如下:
為了生成一幀,Flutter engine首先裝備了vsync
鎖存器,一個vsync的事件將會指示Flutter engine開始一些工作並最終繪製出新的幀呈現在螢幕上,vsync事件的生成頻率會根據硬體平臺的重新整理率決定。
vsync首先會喚醒UI執行緒,UI執行緒的工作是將你程式碼中編寫的Widget樹轉化為要渲染的RenderTree,Flutter中有三顆樹的概念WidgetTree
,ElementTree
,RenderTree
,dart檔案中的Widget樹並不是最終參與繪製的,而只是方便開發者編寫頁面的一個配置。比如,我們指定這裡有一個縱向列表Column,列表裡有三個並列Text,然後Flutter會根據相應語義在對應位置生成對應Element,這才是真正意義上的Flutter UI元件,也是顯示到螢幕上的元素。
元件樹對應到螢幕上還要經過一層渲染樹(RenderObject)的轉化,RenderObject是實際的渲染物件它負責佈局測量以及繪製操作,這樣做的目的是為了更好的應對上層UI的頻繁變化,儘可能地去比較更新,修改配置而不是直接建立下層樹,因為RenderObject樹的建立開銷比較大,所以Widget重新建立,ElementTree和RenderTree並不會完全重新建立,而是會複用一些節點,提升效能。UI執行緒工作到生成RenderTree的過程叫做渲染樹
一旦建立了渲染樹,GPU執行緒就會被喚醒,這個執行緒的工作是將渲染樹的資訊轉換到GPU的命令緩衝區,然後在同一執行緒將資料提交給GPU執行
示例
模擬一個元件耗時操作
class PageOne extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.tealAccent,
child: Center(
child: ListView(
children: [
for(var i=0;i<100000;i++) _buildItemWidget(i),
],
),
),
);
}
Widget _buildItemWidget(int i) {
var line = lines[i % lines.length];
return Padding(
padding: EdgeInsets.symmetric(vertical: 12,horizontal: 18),
child: Row(
children: [
Container(
color: Colors.black,
child: SizedBox(
width: 30,
height: 30,
child: Center(
child: Text(
line.substring(0,1),
style: TextStyle(color: Colors.white),
),
),
),
),
SizedBox(width: 10,),
Expanded(child: Text(
line,
softWrap: false,
))
],
),
);
}
}
複製程式碼
可以看到,在ListView的children填充時,沒有複用佈局,模擬了一個重複建立十萬條子child的情況,頁面第一次載入時會看到明顯示卡頓,timeline顯示如下,一條明顯的紅線,就是掉幀發生的位置。
點選Jank發生的位置,可以看到Timeline Events對應的事件被選中,下方有各個方法執行的耗時時間,可以看到_buildItemWidget
方法耗時26.81ms發生掉幀
模擬一個方法耗時
class PageTwo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.greenAccent,
child: Center(
child: Text(
'第2頁 ' + _fibonacci(30).toString(),
style: TextStyle(color: Colors.black, fontSize: 20.0),
),
),
);
}
static int _fibonacci(int i) {
if(i <= 1) return i;
return _fibonacci(i - 1) + _fibonacci(i - 2);
}
}
複製程式碼
組建初始化時,執行一個斐波那契函式遞迴呼叫,時間複雜度為O(2^n),傳入引數30,即函式執行2^30次運算,初始化頁面可看到明顯示卡頓,定位耗時方法同上。
示例程式碼已上傳至github
擴充
這裡給大家推薦一個小工具fps_monitor,貝殼同學開源的檢測頁面流暢度的小工具,可以更直觀和量化的評估頁面流暢度,頁面大概長這樣
最大耗時和平均耗時可以直觀的觀測頁面優化前後對比效果。
頁面流暢度劃為了四個級別:流暢(藍色)、良好(黃色)、輕微卡頓(粉色)、卡頓(紅色),將 FPS 折算成一幀所消耗的時間,不同級別採用不一樣的顏色,統計不同級別出現的次數
具體可以跳轉連結: juejin.cn/post/694791…
總結
效能優化在任何平臺任何語言上都是永恆不變的話題,理解效能優化原理,提升觀察的敏銳性對一個開發者至關重要。利用Flutter提供的外掛和效能分析工具,能夠幫助我們快速的定位到問題程式碼,提升開發效率,Flutter Inspector可以在寫程式碼階段提升頁面編碼質量,Timeline可以在執行階段發現哪個頁面掉幀嚴重,重點分析。