前言
??有幸負責的模塊使用Flutter編寫,在三個月的開發過程中,在原有Demo自學基礎上又學到了很多,謹以此篇文章做一個Flutter階段性的學習和總結,以便于往后的學習過程中溫故而知新,那么我們正篇開始。
前世今生
??新事物的誕生往往是有一定原因存在的,移動端在這條路上有幾個階段,從Android Native 到 WebView 階段,為了獲得不發版本就可以獲得實時動態化的效果,雙端使用JSBridge實現了與原生Native底層能力的對接。
??Native容器階段,主要有React Native為代表,雖然RN依賴于原生渲染,性能好與H5,但是還存在一些問題,比如:JavaScript和原生通信,部分場景存在通訊瓶頸,容易導致卡頓、不同移動平臺,空間需要單獨維護,原生版本更新后,社區控件更新較慢等問題。
??Flutter階段,Flutter拋棄了原生系統控件和 Webview,使用自研高性能渲染引擎來繪制Widget,預先(AOT)編譯,運行時直接執行Native(arm)代碼,Dart代碼執行(在UI TaskRunner),圖片下載(IO TaskRunner),真正的渲染(GPU TaskRunner),同平臺的通信等(Platform TaskRunner即Native概念下的主線程)是互相隔離的。針對布局等的優化:布局計算時單次樹走動即可完成;Relayout Boundary機制:如果Child 的size是固定的,那么不會因為Child的Relayout導致Parent ReLayout等布局優化,都讓Flutter脫穎而出。
??所以,App引入Flutter,可以有效提高開發效率,避免雙端不一致的現象,縮小開發成本。但是Flutter畢竟不是原生,在代碼寫法上有很多需要注意的地方,文章第三章會主要總結本人在開發和工作中遇到的問題,相比于H5,flutter在動態化方面,支持的并不好。
1 基礎知識
1.1 運行模式
??Debug:Debug模式可以在真機和模擬器上同時運行:會進入所有斷點,包括debugging信息、debugger aids(比如observatory)和服務擴展。優化了快速develop/run循環,但是沒有優化執行速度、二進制大小和部署。命令flutter run就是以這種模式運行的。
??Release : Release模式只能在真機上運行,不能在模擬器上運行:會關閉所有斷言和debugging信息,關閉所有debugger工具。優化了快速啟動、快速執行和減小包體積。禁用所有的debugging aids和服務擴展。這個模式是為了部署給最終的用戶使用。
??Profile:Profile模式只能在真機上運行,不能在模擬器上運行:和Release模式類似,除了啟用了服務擴展和tracing,以及一些為了最低限度支持tracing運行的東西(比如可以使用DevTools)。flutter run --profile 可進入該模式。
??Test :headless test模式只能在桌面上運行:和Debug模式不同的是headless的而且你能在桌面運行。命令flutter test就是以這種模式運行的。
1.2 架構設計
??Framework : 使用dart實現,包括Material Design風格的Widget,Cupertino(針對iOS)風格的Widgets,文本/圖片/按鈕等基礎Widgets,渲染,動畫,手勢等。
??Engine : C++實現,主要包括:Skia,Dart和Text。 Skia是開源的二維圖形庫,提供了適用于多種軟硬件平臺的通用API。
??Embedder : 嵌入層,即把Flutter嵌入到各個平臺上去,這里做的主要工作包括渲染Surface設置,線程設置,以及插件等。 從這里可以看出,Flutter的平臺相關層很低,平臺(如iOS)只是提供一個畫布,剩余的所有渲染相關的邏輯都在Flutter內部,這就使得它具有了很好的跨端一致性。
1.3 渲染流程
??Widget:Widget樹實際上是一個配置樹,而真正的UI渲染樹是由Element構成;不過,由于Element是通過Widget生成,所以它們之間有對應關系,我們可以寬泛地認為Widget樹就是指UI控件樹或UI渲染樹。
??Element : 一個Widget對象可以對應多個Element對象。這很好理解,根據同一份配置(Widget),可以創建多個實例(Element)。
??從創建到渲染的大體流程是:根據Widget生成Element,然后創建相應的RenderObject并關聯到Element.renderObject屬性上,最后再通過RenderObject來完成布局排列和繪制。
舉個栗子?
??以下例子取自 閑魚技術博客 閑魚Flutter復雜業務優化
Container(
color: Colors.blue,
child: Row(
children: [
Image.asset('image'),
Text('text'),
],
),
);
??依據上圖 來說就是 UI 刷新的時候,Framework 通知 Engine,Engine 會等到下個 Vsync 信號到達的時候,會通知 Framework 進行 animate, build,layout,paint,最后生成 layer 提交給 Engine。Engine 會把 layer 進行組合,生成紋理,最后通過 Open Gl 接口提交數據給 GPU, GPU 經過處理后在顯示器上面顯示
??在基礎能力和業務開發完成后,性能優化和代碼檢查也是非常重要的,所以后邊的文章主要圍繞 性能檢測和優化 等細節的總結。
3 性能檢測工具
結合 Flutter 性能分析 鏈接 和Flutter 性能視圖 等官方文檔,整合調試工具說明,并添加自我理解
3.1 Performance Overlay
??開啟方式比較多,通過 DevTools TimeLines 或者 run Flutter Inspector 都可以進入
??點擊show CPU GPU 圖后
● 豎軸表示耗時,沿豎軸的黑線是時間線 (間隔單位為 16ms)
● 橫軸則表示幀,垂直的綠色條代表的是當前幀 (如果為紅色則代表處理耗時點)
● 上圖顯示GPU線程消耗的時間 (幀為紅色:Widget太多 場景復雜 渲染耗時)
● 下圖顯示UI線程消耗的時間 (幀為紅色:Dart代碼有問題 查看build中是否做太多耗時操作)
也可以在代碼中開啟 PerformanceOverlay 控件
@override
Widget build(BuildContext context) {
Widget app = MaterialApp(
// true 展示PerformanceOverlay
showPerformanceOverlay: showPerformanceOverlay,
title: 'appname',
theme: ThemeData(
appBarTheme: AppBarTheme(brightness: Brightness.light),
textTheme: textTheme
),
home: _buildHome(context),
builder: (_, widget) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget
);
},
);
return WillPopScope(
child: app,
onWillPop: dealWillPop,
);
}
??showPerformanceOverlay 為MaterialApp自帶屬性,如果沒有使用MaterialApp,可以在代碼中通過PerformanceOverlay.allEnabled(checkerboardOffscreenLayers: true); 開啟
3.2 DevTools TimeLine
??使用 DevTools 可以在Performance Overlay大粒度卡頓范圍內深度挖掘
由于Observatory和DevTools功能類似,且正逐漸被DevTools替代,這里主要羅列DevTools的使用方式。
● 頂部部分列表類似于PerformanceOverlay ,深色代表GPU耗時,淺紅色代表UI層耗時(圖層過多)
● 中間部分為幀事件圖表,分位 UI 和 GPU兩部分組成
● 底部部分為CPU圖表,主要有三種不同呈現方式來展示CPU調用堆棧信息
??我們也可以通過添加一些系統api ,更方便我們定位事件圖表具體執行位置
● debugProfileBuildsEnabled - 向 Timeline 事件中添加 build 信息
● debugProfilePaintsEnabled - 向 timeline 事件中添加 paint 信息
● debugPrintRebuildDirtyWidgets - 記錄每幀重建的 widget
??和Android Profiler 類似,幀事件橫軸代表事件順序,縱軸 代表方法調用堆棧,我們可以通過事件寬度來判斷那些事件是比較耗時的。
3.3 Widget rebuild stats
??Android Studio 中 View > Tool Windows > Flutter Performance 打開性能工具窗口,在 Widget rebuild stats 中勾選 Track widget rebuilds 來查看 widget 的重建信息。重建信息包括 Widget 名字、源碼位置、上一幀中重建次數、當前頁面中重建次數。此外,Widget 名字前面還會顯示一個小圖標。
● 黃色旋轉圓圈 - 重建次數過多
● 灰色圓圈 - 未重建
● 灰色旋轉圓圈 其他情況
4 性能優化
??上一章,對于檢測工具大概的總結了使用方式和看法,下邊我們就根據工具,來定位界面的問題,并總結出我的優化方式。
4.1 列表優化
??在商品2.0中列表占比是比較多得,首頁列表的交互邏輯更為復雜,如何讓用戶用起來如絲般順滑,需要進行一些性能的打磨。
問題:
界面滑動的時候 會有輕微掉幀現象,檢測 fps長期飄紅(小于 60fps)
(圖一 Flutter Performance 截圖)
(圖二 DevTools Fps ui GPU 截圖)
方案:
??更換PowerScrollView 后列表 滑動FPS和部分DevTools截圖
PowerScrollView :概念鏈接
PowerScrollView 優化點:
- 針對PowerDataManager 數據可以做到增量更新。
- 添加緩存數組,監控_childElements方法來對element 進行緩存,當滾動超出 viewport 的顯示以及預加載范圍或者數據源發生變化,會通過調用 collectGarbage 方法回收不需要的 elements)
- cell 層面引入了 placeholder 的機制,快速滑動場景優化build工作量
- 更多樣的瀑布流 、列表樣式可供選擇等等優化
引入后效果,
(圖一 Flutter performance 截圖)
圖中紅色部分主要處于手指第一次按下,并且執行了頂部商品工具欄動畫的原因
(圖二 DevTools FPS UI GPU 截圖)
??上圖可以看到 gpu占比較多,列表滑動起始部分還是有卡頓部分,主要是由于首頁商品工具欄過度動畫 和 首頁復雜的UI展示引起的,后續可以通過
4.2 優化 ClipPath 和 ClipRPath
??在剛開始調試初期,使用 Timeline 查看渲染線程性能消耗,可以發現有多個 ClipRectLayer 和 ClipRRectLayer過程,對比商品首頁界面后,猜測是在Flutter殼工程中使用自帶Image展示圖片的時候,對于大圖沒有進行裁剪顯示,部分圓角Widget也需要優化,同時修復 radius 為0也會設置 ClipRRect 的問題。
4.3 常用優化方法
- 盡量將setState放在葉子節點,好處是build時影響范圍極小,局部刷新
- 使用ListView.builder()而不是直接使用ListView()來構建列表 (官方推薦)
- 對于頻繁更新的控件,使用RepaintBoundary隔離,讓其擁有一個獨立的paint區域
- 使用const來修飾永遠不需要變更的控件,如果寬高固定,推薦固定寬高,避免重復計算
- 按需使用StateLessWidget,并非全部用StateFulWidget
- 使用Visibility控件,避免布局樹頻繁切換
- 針對于使用Fish-redux,state 對象中的視圖數據真正發生變化的時候,新建 state 對象
- 使用圖片替換半透明效果,減少saveLayer 或者clipPath 的使用。
最后
不知不覺已經寫到了最后,對于Flutter的學習和使用,一直都在路上,文章中不免有些紕漏,還望大佬們海涵并指出,后續會繼續深耕Flutter 動態化方案和Flutter性能優化,繼續努力學習,繼續滿血沖沖沖!