Flutter效能優化—UI

語言: CN / TW / HK

耗時三個月總結全網大部分Flutter效能分析的方法論,並且結合實戰驗證效果和自己開發經驗,保姆式的一步步帶你瞭解優化中的細節和注意點,希望能給你帶來一些收穫

原理

Flutter的架構

Framework使用dart實現主要提供我們開發用的API

Engine使用C++實現,主要包括:Skia,Dart和Text。Skia是開源的二維圖形庫,提供了適用於多種軟硬體平臺的通用API

Embedder是一個嵌入層,即把Flutter嵌入到各個平臺上去,這裡做的主要工作包括渲染Surface設定,執行緒設定,以及外掛等

Flutter執行緒

flutter裡面有四個執行緒分別是UI執行緒,GPU執行緒,IO執行緒,Platform執行緒

UI執行緒執行dart程式碼,我們寫的程式碼都是在這個執行緒執行

GPU執行緒被用於執行裝置GPU的相關呼叫

IO執行緒主要功能是從圖片儲存(比如磁中讀取壓縮的圖片格式,將圖片資料進行處理為GPU Runner的渲染做好準備

Platform執行緒主要負責和Engine的所有互動

我們主要關注UI執行緒和GPU執行緒的效能問題就可以了,其他兩個主要跟底層互動多

Flutter檢視樹

Flutter檢視樹包含了三顆樹:Widget、Element、RenderObject

  • Widget: 存放渲染內容、它只是一個配置資料結構,建立是非常輕量的,在頁面重新整理的過程中隨時會重建
  • Element: 同時持有Widget和RenderObject,存放上下文資訊,通過它來遍歷檢視樹,支撐UI結構
  • RenderObject: 根據Widget的佈局屬性進行layout,paint ,負責真正的渲染

從建立到渲染的大體流程是:根據Widget生成Element,然後建立相應的RenderObject並關聯到Element.renderObject屬性上,最後再通過RenderObject來完成佈局排列和繪製。

例如下面這段佈局程式碼

image.png

對應三棵樹的結構如下圖

不同樹對應執行的位置如下圖

瞭解了這三棵樹,我們再來看下頁面重新整理的時候具體做了哪些操作。

當需要更新UI的時候,Framework通知Engine,Engine會等到下個Vsync訊號到達的時候,會通知Framework進行animate, build,layout,paint,最後生成layer提交給Engine。Engine會把layer進行組合,生成紋理,最後通過Open Gl介面提交資料給GPU, GPU經過處理後在顯示器上面顯示,如下圖:

Flutter繪製流程

結合前面的例子,如果text文字或者image內容發生變化會觸發哪些操作呢?

Widget是不可改變,需要重新建立一顆新樹,build開始,然後對上一幀的element樹做遍歷,呼叫他的updateChild,看子節點型別跟之前是不是一樣,不一樣的話就把子節點扔掉,創造一個新的,一樣的話就做內容更新,對renderObject做updateRenderObject操作,updateRenderObject內部實現會判斷現在的節點跟上一幀是不是有改動,有改動才會別標記dirty,重新layout、paint,再生成新的layer交給GPU,

流程如下圖:

flutter執行模式

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

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

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

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

建議在Profile模式下測試效能

實戰

performance overlay

先來張效果圖

開啟performance overlay

方式1:在MaterialApp的屬性showPerformanceOverlay設定true開啟(缺點:不能動態控制顯示隱藏)

方式2: 通過performance開啟(動態開啟關閉)

如果AS右邊沒有快捷鍵可以在tools找到開啟

分析

開啟showPerformanceOverlay可以看模擬器上面有兩個柱狀圖: 上面柱狀圖圖是GPU執行緒,下面樹UI執行緒,綠色表示當前幀資料情況, 每一個柱狀圖橫向有三格每格時間16ms,縱向是最後三百幀, 如果GPU線那幀顏色紅色說明繪製過於複雜的圖形,或者執行過多的GPU操作,如果CPU執行緒那幀顏色紅色說明dart程式碼執行耗時操作導致ui執行緒阻塞。

案列

通過performance overlay我們可以快速直觀發現那個頁面效能有問題 具體定位問題程式碼需要藉助DevTools工具分析具體情況,下面我通過一個案例來演示整個優化過程,通過上面原理我們知道繪製流程是animate, build,layout,paint,那麼我們優化入口就是從這四個函式切入

下面是演示程式碼

執行效果

DevTools工具(AS開發工具)

可以直接點選快捷鍵開啟(建議你的flutterSDK最好在2.8.1及以上,低版本的sdk會導致沒有演示的部分功能按鈕),

如果SDK低於2.8.1可以設定屬性獲取資料

VSCode開發工具(沒有用vscode開發沒有驗證如果不行可以使用下面命令開啟)

可以安裝外掛https://flutter.cn/docs/development/tools/devtools/vscode

使用命令開啟 flutter run --profile

執行命令後點擊連結就開啟DevTools

開啟DevTools效果如下

選中select Widget Mode可以使我們直觀看到我們寫的程式碼的widget樹結構,點選 右邊Layout Explorer裡面的圖層模擬器可以顯示對應的widget。

優化build

點選Performance裡面的EnhanceTracing 選中Track Widget Builds

上面的柱狀圖是每幀的耗時情況,我們點選一幀看看裡面具體情況

我們可以看到build的層級很長,實際情況我們只需要從改變顯示計數器的Text開始build就可以了。

提取Text前程式碼                                   

提取後代碼

image.png image.png

提取後效果

image.png

可以看到提取後Build的層級只有兩層了,時間也少了很多

優化paint

點選Performance裡面的EnhanceTracing 選中Track Paints

image.png

點選一幀我們看具體分析資料

image.png

我們可以看到paint繪製層級也是很多,實際我們只需要繪製變化的Text,其他沒有邊的widget並不需要paint,我們可以使用RepaintBoundary包裹Text,利用RepaintBoundary提高paint效率,它為經常發生顯示變化的內容提供一個新的隔離layer,新的layer paint不會影響到其他layer,

變化程式碼

image.png

使用後效果

image.png

我們可以看到piant層級只有一層了 ,時間也少了很多了。

AS也有些快捷鍵可以看到build的層級,例如Flutter Performance勾選Track widget reBuilds

image.png

小結:

  • 提高build效率,setState重新整理資料儘量下發到底層節點
  • 提高paint效率,RepaintBoundry建立單獨layer減少重繪區域

Timeline工具

Timeline工具其實是DevTools工具的前世版本,那我們都介紹了DevTools工具了為什麼還要介紹Timeline工具呢,因為DevTools工具現在功能還不完善,也不夠穩定,所有我們想分析更多資料暫時還需要藉助Timeline工具雖然他不是那麼好用,後期穩定了就可以開心的用DevTools了。

注意:先配置你需要測試的資料,預設Timeline是不會記錄構建具體資料的

image.png

執行命令 flutter run --profile 點選 Timeline連結

image.png

開啟後效果圖

image.png

我們主要分析UI效能所以點選timeline 

image.png

可以看到左邊是四大執行緒,右邊是一幀的執行情況,如果你想測試那個頁面效能就在那個頁面多操作下,選中Flutter Developer,再點選有右上角Refresh重新整理,再利用縮放移動顯示下面的build,layout,paint的資料情況(選中向下箭頭圖示,當你滑鼠點選圖表左移動縮小,滑鼠點選圖表右移動放大;選中十字圖示,就圖表就可以跟著滑鼠移動)優化思路跟Devtools工具一樣。

檢查螢幕之外的檢視

主要檢查呼叫SaveLayout的Widget,因為呼叫saveLayout會再GPU裡面開啟一個新的繪圖快取區,切換繪圖目標,這個操作會導致GPU非常耗時,所以我們儘量少用這些Widget元件,使用其他Widget替代實現。

開啟檢查

image.png

程式碼 

image.png

當我們滑動時,頂部AppBar會不斷閃爍,說明呼叫savelayout,

知道時問題所在,那就開始分析優化方案了,使用CupertinoNavigationBar預設不設定透明度屬性是沒有半透明玻璃效果,但是卻導致我們效能下降了,如果ui原型只是普通背景顏色就直接使用Scaffold的AppBar ,如果需要我們可以給AppBar設定半透明效果最大還原ui需要的效果,當然需求一定需要,那就需要在效能和需求之間做平衡了。

優化方案

image.png image.png 檢查圖片是否光柵格化快取

從資源的角度看,另一類非常消耗效能的操作是渲染影象,因為影象渲染會涉及 I/O、GPU 儲存以及不同通道的資料格式轉換,因此渲染過程的構建需要消耗大量資源。為了緩解 GPU 的壓力,Flutter 提供了多層次的快取快照,這樣 Widget 重建時就無需重新繪製靜態影象了,開啟後如果出現閃爍框就是沒有快取,我們可以使用RepaintBoundary包裹,讓系統快取起來

image.png

總結

1:開啟flutter程式碼規範,使用flutter團隊程式碼規範使用我們程式碼更效能更高效,不知道怎麼開啟程式碼規範的可以看下面這篇

https://blog.csdn.net/qq_36237165/article/details/123325012?spm=1001.2014.3001.5501

2:使用const 修飾widget,這樣可以使用系統知道我們的widget可以快取下來,提高rebuild的效率。

3:我們再開發中經常會把複雜的wiedget提取出一個方法使用程式碼看起來比較簡潔,其實Flutter團隊建議封裝成個StatelessWidget效能會更好,你封裝成方法底層框架不是你這方法是否需要複用所以他是不會快取起來,如果你封裝成一個StatelessWidget系統就知道你這個Widget是可以快取複用的,間距提供效率。

https://stackoverflow.com/questions/53234825/what-is-the-difference-between-functions-and-classes-to-create-reusable-widgets

4:使用狀態管理框架對widget實現區域性重新整理,常用的狀態管理框架有provider,mobx,get,前面兩個都是Flutter社群推薦的。

5:避免更改元件樹的結構和元件的型別,樹結構改變會導致樹重新rebuild,

有如下場景,有一個 Text 元件有可見和不可見兩種狀態

image.png

如果需要返回兩種不同型別的widget,可以把變化的widget封裝到StatefulWidget裡面,如果你使用狀態管理框架可以封裝到StatelessWidget裡面,

image.png 優化後 image.png

6:ListView是我們最常用的元件之一,用於展示大量資料的列表。如果展示大量資料請使用 ListView.builder 或者 ListView.separated, 實現複用,如果item佈局高度可以固定可以使用itemExtent 屬性提高效能(提高系統測量效率)

image.png

7:AnimatedBuilder 、TweenAnimationBuilder 等一類的元件的問題,這些元件都有一個共同點,帶有 builder 且其引數重有 child。以 AnimatedBuilder 為例,如果 builder 中構建的樹中包含與動畫無關的元件,將這些無關的元件當作 child 傳遞到 builder 中比直接在 builder 中構建更加有效

WT8mxX1z8S.png

8:避免呼叫 saveLayer(呼叫 saveLayer() 會開闢一片離屏緩衝區。將內容繪製到離屏緩衝區可能會觸發渲染目標切換,這些切換在較早期的 GPU 中特別慢)

1 ShaderMask

2 ColorFilter

3 Chip -- might cause call to saveLayer() if disabledColorAlpha != 0xff (簡單的圓角效果可以使用Container 實現)

4 Text -- might cause call to saveLayer() if there’s an overflowShader

5 Opacity減少使用特別是動畫中,淡入效果可以使用AnimatedOpacity和FadeInImage,透明效果可以設定widget的背景顏色實現

9:如果你要執行一些比較耗時操作建議使用isolate,如果任務比較耗時使用非同步也會導致ui卡頓

image.png

當你執行上面同步和非同步任務發現載入進度動畫會被卡住,使用isolate不會導致載入動畫卡頓

10:優先使用StateLessWidget,而不是全部用StateFulWidget

11:對於頻繁更新的控制元件(比如倒計時,秒錶),使用RepaintBoundary隔離它,讓他在一個獨立的paint區域 

12:簡單的ui樣式,可以自己封裝實現

image.png

程式碼:

image.png

重新整理率對比:

GiBnCgyDw3.png

使用自定義的按鈕重新整理率更高,因為系統封裝的按鈕功能比較豐富裡面的Widget巢狀很多,但是實際業務需求可能只需要些普通效果,我們為了提升效能可以根據需求自定義封裝達到突破效能瓶頸

13:建議及時更新Flutter版本,每個新的版本都會有很多效能上的優化,白嫖Flutter團隊帶來的效能優化,如下是Flutter2.8

image.png

討論

  • 流暢:一幀耗時低於 18ms
  • 良好:一幀耗時在 18ms-33ms 之間
  • 輕微卡頓:一幀耗時在 33ms-67ms 之間
  • 卡頓:一幀耗時大於 66.7ms

image.png

「其他文章」