⚠️Flutter 效能優化實踐 總結⚠️

iOShuyang發表於2019-12-20

在flutter的開發和工作中,因為工作內容的要求越來越高,加上一位優秀的同事,自己也對自己的寫的程式碼除了規範的要求,也開始對效能做了優化。我們開發的App屬於首頁就是重點,剛好是我負責,所以再簡單的UI和邏輯搭建完成後,要求達到一定的效能優化,所以自己開始瞭解和學習相關的處理。




0.渲染相關知識瞭解

0.0 Flutter有四種執行模式:Debug、Release、Profile和test,這四種模式在build的時候是完全獨立的。

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

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

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

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

0.1 Flutter的架構主要分成三層:Framework,Engine,Embedder。

1.Framework使用dart實現,包括Material 
Design風格的Widget,Cupertino(針對iOS)風格的Widgets,文字/圖片/按鈕等基礎Widgets,渲染,動畫,手勢等。
此部分的核心程式碼是:flutter倉庫下的flutter 
package,以及sky_engine倉庫下的io,async,ui(dart:ui庫提供了Flutter框架和引擎之間的介面)等package。


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


3.Embedder是一個嵌入層,即把Flutter嵌入到各個平臺上去,這裡做的主要工作包括渲染Surface設定,執行緒設定,以及外掛等。
從這裡可以看出,Flutter的平臺相關層很低,平臺(如iOS)只是提供一個畫布,剩餘的所有渲染相關的邏輯都在Flutter內部,這就使得它具有了很好的跨端一致性。
複製程式碼

0.2 Widget、Element、RenderObject三者的關係如下:

Widget實際上就是Element的配置資料,Widget樹實際上是一個配置樹,而真正的UI渲染樹是由Element構成;不過,由於Element是通過Widget生成,所以它們之間有對應關係,所以在大多數場景,我們可以寬泛地認為Widget樹就是指UI控制元件樹或UI渲染樹。

一個Widget物件可以對應多個Element物件。這很好理解,根據同一份配置(Widget),可以建立多個例項(Element)。

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



1.能否在模擬器下進行效能除錯?

答案:可以,但是除錯很不準確。所以不建議使用模擬器進行效能除錯。
幾乎全部的 Flutter 應用效能除錯都應該在真實的 Android 或者 iOS 裝置上以分析模式進行。
通常來說,除錯模式或者是模擬器上執行的應用的效能指標和釋出模式的表現並不相同。 
應該考慮在使用者使用的最慢的裝置上檢查效能。
複製程式碼

  • 為什麼應該在真機上執行:

    • 各種模擬器使用的硬體並不相同,因此效能也不同—模擬器上的一些操作會比真機快,而另一些操作則會比真機慢。

    • 除錯模式相比分析模式或者釋出編譯來說,增加了額外的檢查(例如斷言),這些檢查可能相當耗費資源。

  • 除錯模式和釋出模式程式碼執行的方式也是不同的。除錯編譯採用的是“just in time”(JIT)模式執行應用,而分析和釋出模式則是預編譯到本地指令(“ahead of time”,或者叫 AOT)之後再載入到裝置中。JIT本身的編譯就可能導致應用暫停,從而導致卡頓。





2.如何進行App效能測試?

⚠️Flutter 效能優化實踐 總結⚠️

答案:
1.在 Android Studio 和 IntelliJ 使用 Run > Flutter Run main.dart in Profile Mode 選項
    1.1 選擇 View > Tool Windows > Flutter Inspector。
    1.2 在工具欄中選擇書架圖示。

2.在 VS Code中,開啟 launch.json 檔案,設定 flutterMode 屬性為 profile(當分析完成後,改回 release 或者 debug)
    2.1 選擇 View > Command Palette… 來開啟 command palette。
    2.2 在文字框中輸入“performance”並在彈出列表中選中 Toggle Performance Overlay。如果命令不可用,請確保應用在執行狀態。

3.From the command line, use the --profile flag: 命令列使用 --profile 引數執行
  3.1 flutter run --profile
  3.2 使用 p 引數觸發效能圖層
  
  
4.可以通過在 MaterialApp 或者 WidgetsApp 的構造方法中設定 showPerformanceOverlay 屬性為 true 來展示 PerformanceOverlay widget:
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showPerformanceOverlay: true,
      title: 'My Awesome App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'My Awesome App'),
    );
  }
}
複製程式碼



3.如何檢視分析效能圖層?

⚠️Flutter 效能優化實踐 總結⚠️

答案:
效能圖層用兩張圖表顯示應用的耗時資訊。如果 UI 產生了卡頓(跳幀),這些圖表可以幫助分析原因。圖表在當前應用的最上層展示,但並不是用普通的 widget 方式繪製的—Flutter 引擎自身繪製了該圖層來儘可能減少對效能的影響。每一張圖表都代表當前執行緒的最近 300 幀表現。

左圖:GPU 執行緒的效能情況在上面,UI 執行緒顯示在下面,垂直的綠色條條代表的是當前幀。
右圖:每一幀渲染過程當中總共使用的時間
複製程式碼
Flutter 用了一些額外的執行緒來完成這項工作。開發者的 Dart 程式碼都在 UI 執行緒執行。儘管沒有直接訪問其他執行緒的許可權,但 UI 執行緒的動作還是對其他執行緒的效能有影響的。

平臺執行緒
該平臺的主執行緒。外掛程式碼在這裡執行。更多資訊請參閱:iOS 的 UIKit 文件,或者 Android 的 MainThread 文件。效能圖層並不會展示該執行緒。

UI 執行緒
UI 執行緒在 Dart VM 執行 Dart 程式碼。該執行緒包括開發者寫下的程式碼和 Flutter 框架根據應用行為生成的程式碼。當應用建立和展示場景的時候,UI 執行緒首先建立一個 圖層樹(layer tree) ,一個包含裝置無關的渲染命令的輕量物件,並將圖層樹傳送到 GPU 執行緒來渲染到裝置上。 不要阻塞這個執行緒! 在效能圖層的最低欄展示該執行緒。

GPU 執行緒
GPU 執行緒取回圖層樹並通知 GPU 渲染。儘管無法直接與 GPU 執行緒或其資料通訊,但如果該執行緒變慢,一定是開發者 Dart 程式碼中的某處導致的。圖形庫 Skia 在該執行緒執行,有時也被叫做 光柵器(rasterizer)執行緒 。在效能圖層的最頂欄展示該執行緒。

I/O 執行緒
可能阻塞 UI 或者 GPU 執行緒的耗時任務(大多數情況下是I/O)。該執行緒並不會在效能圖層中展示。
複製程式碼

⚠️Flutter 效能優化實踐 總結⚠️
紅色豎條表明當前幀的渲染和繪製都很耗時 當兩張圖表都是紅色時,就要開始對 UI 執行緒(Dart VM)進行診斷了。

每一幀都應該在 1/60 秒(大約 16ms)內建立並顯示。
如果有一幀超時(任意影象)而無法顯示,就導致了卡頓,圖表之一就會展示出來一個紅色豎條。
如果是在 UI 圖表出現了紅色豎條,則表明 Dart 程式碼消耗了大量資源。
而如果紅色豎條是在 GPU 圖表出現的,意味著場景太複雜導致無法快速渲染。
複製程式碼
為什麼需要在 16ms 內渲染完成每一幀
1.將幀渲染時間降低到 16ms 以下可能在視覺上看不出來什麼變化,但可以延長電池壽命以及避免發熱問題。
2.可能在你當前測試裝置上執行良好,但請考慮在應用所支援的最低端裝置上的情況。
3.當 120fps 的裝置普及之後,便需要在 8ms 之內完成每一幀的渲染來保證流暢平滑的體驗。
複製程式碼



4.如何進行效能分析並開始處理?

4.1 定位 UI 圖表中的問題

如果效能圖層的 UI 圖表顯示紅色,就要從分析 Dart VM 開始著手了,即使 GPU 圖表同樣顯示紅色。

使用 Dart DevTool 進行效能分析
Dart DevTool 提供諸如效能分析、堆測試以及顯示程式碼覆蓋率等功能。
DevTool 的 timeline 介面可以讓開發者逐幀分析應用的 UI 效能。
複製程式碼

(Observatory 被 Dart DevTools 取代了。這個基於瀏覽器的工具仍在開發中,但只用來預覽。參考 DevTools’ docs 頁面來獲取安裝和使用指導。)

4.2 定位 GPU 圖表中的問題

有些情況下介面的圖層樹構造起來雖然容易,但在 GPU 執行緒下渲染卻很耗時。
這種情況發生時,UI 圖表沒有紅色,但 GPU 圖表會顯示紅色。
這時需要找出程式碼中導致渲染緩慢的原因。
特定型別的負載對 GPU 來說會更加複雜。
可能包括不必要的對 saveLayer 的呼叫,許多物件間的複雜操作,還可能是特定情形下的裁剪或者陰影。
複製程式碼

如果推斷的原因是動畫中的卡頓的話,可以使用 timeDilation 屬性來極大地放慢動畫。也可以使用 Flutter Inspector 來減慢動畫速度。在 inspector 的 gear 選單下選中 Enable Slow Animations。如果想對動畫速度進行更多操作,請在程式碼中設定 timeDilation 屬性。卡頓是第一幀發生的還是貫穿整個動畫過程呢?如果是整個動畫過程的話,會是裁剪導致的麼?也許有可以替代裁剪的方法來繪製場景。比如說,不透明圖層的長方形中用尖角來取代圓角裁剪。如果是一個靜態場景的淡入、旋轉或者其他操作,可以嘗試使用 RepaintBoundary。

4.2.1 檢查螢幕之外的檢視

saveLayer

saveLayer 方法是 Flutter 框架中最重量的操作之一。 更新螢幕時這個方法很有用,但它可能使應用變慢,如果不是必須的話,應該避免使用這個方法。 即便沒有顯式地呼叫 saveLayer,也可能在其他操作中間接呼叫了該方法。可以使用 PerformanceOverlayLayer.checkerboardOffscreenLayers 開關來檢查場景是否使用了 saveLayer。 開啟開關之後,執行應用並檢查是否有影象的輪廓閃爍。如果有新的幀渲染的話,容器就會閃爍。 舉個例子,也許有一組物件的透明度要使用 saveLayer 來渲染。 在這種情況下,相比通過 widget 樹中高層次的父 widget 操作,單獨對每個 widget 來應用透明度可能效能會更好。其他可能大量消耗資源的操作也同理,比如裁剪或者陰影。

透明度(Opacity)、裁剪(clipping)以及陰影(shadows)它們本身並不是個糟糕的注意。然而對 widget 樹頂層 widget 的操作可能導致額外對 saveLayer 的呼叫以及無用的處理。

4.2.2 檢查沒有快取的影象

RepaintBoundary 使用 RepaintBoundary 來快取圖片是個好主意, 當需要的時候 。 從資源的角度看,最重量級的操作之一是用影象檔案來渲染紋理。 首先,需要從持久儲存中取出壓縮影象,然後解壓縮到宿主儲存中(GPU 儲存),再傳輸到裝置儲存器中(RAM)。也就是說,影象的 I/O 操作是重量級的。 快取提供了複雜層次的快照,這樣就可以方便地渲染到隨後的幀中。 因為光柵快取入口的構建需要大量資源,同時增加了 GPU 儲存的負載,所以只在必須時才快取圖片。 開啟PerformanceOverlayLayer.checkerboardRasterCacheImages 開關可以檢查哪些圖片被快取了。 執行應用來檢視使用隨機顏色網格渲染的影象,標識被快取的影象。當和場景互動時,網格里的圖片應該是靜止的—代表重新快取圖片的閃爍檢視不應該出現。 大多數情況下,開發者都希望在網格里看到的是靜態圖片,而不是非靜態圖片。如果靜態圖片沒有被快取,可以將其放到 RepaintBoundary widget 中來快取。雖然引擎也可能忽略 repaint boundary,如果它認為影象還不夠複雜的話。

4.2.3 檢視 widget 重建效能

顯示效能資料 Flutter 框架的設計使得構建達不到 60fps 流暢度的應用變得困難。通常情況下如果卡頓,就是因為每一幀被重建的 UI 比需求更多的簡單 bug。Widget rebuild profiler 可以幫助除錯和修復這些問題引起的 bug。 可以檢視 widget inspector 中當前螢幕和幀下的 widget 重建數量。瞭解細節,可以參考 在 Android Studio 或類 IntelliJ 裡開發 Flutter 應用 中的 顯示效能資料。




5.UI 應用效能優化總結

5.1 UI 渲染

⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️

⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️
⚠️Flutter 效能優化實踐 總結⚠️

5.2 UI 除錯步驟

1.在mian裡面設定

  • debugDumpLayerTree ○ 檢視layer樹
  • debugPaintLayerBordersEnabled ○ 檢視layer界限
  • debugRepaintRainbowEnabled ○ 被重新繪製的RenderObject
  • debugProfilePaintsEnabled ○ 在觀測臺裡顯示繪製樹

2.profile下真機執行

3.選擇Open TimeLine View,建議使用chrome開啟

⚠️Flutter 效能優化實踐 總結⚠️

4.檢視分析

⚠️Flutter 效能優化實踐 總結⚠️

5.3 UI 提高效能的總結

1.避免在 build() 方法中進行重複且耗時的工作,因為當父 Widget 重建時,子 Wdiget 的 build() 方法會被頻繁地呼叫。



2.當在 State 上呼叫 setState()時,所有後代 Widget 都將重建。因此,將 setState() 的呼叫轉移到其 UI 實際需要更改的 Widget 子樹部分。如果改變的部分僅包含在 Widget 樹的一小部分中,請避免在 Widget 樹的更高層級中呼叫 setState()。【提高build的效率- 降低遍歷的出發點】



3.當重新遇到與前一幀相同的子 Widget 例項時,將停止遍歷。這種技術在框架內部大量使用,用於優化動畫不影響子樹的動畫。請參閱 TransitionBuilder 模式和使用此原則的 SlideTransition,以避免在動畫過程中重建其後代 Widget。【提高build的效率- 停止樹的遍歷】



4.需要更新的地方新增RepaintBoundary去設定一個獨立圖層,來減少圖層更新節點的數量【提高paint的效率】




6.GPU 應用效能優化總結

6.1 GPU 圖形渲染

⚠️Flutter 效能優化實踐 總結⚠️
因為Dart程式碼直接呼叫SKia的C和C++程式碼,當Dart程式碼能夠媲美Java程式碼就能夠達到Flutter App的效能媲美原生App。

Skia(開源圖形引擎)是一個C++的開源2D向量圖形處理函式庫(Cairo是一個向量庫),包括字型、座標轉換、點陣圖等等,相當於輕量級的Cairo,目前主要用於Google的Android和Chrome平臺,Skia搭配OpenGL/ES與特定的硬體特徵,強化顯示的效果。另外,Skia是WebKit支援的眾多圖形平臺之一,在WebKit的GraphicsContext.h/.c中有相關實現。

6.2 GPU 除錯步驟

使用真機進行效能除錯,Skia 有兩套很不同的後端,Flutter在iOS模擬器中使用純CPU後端,而真機裝置一般使用GPU硬體加速後端,所以效能特性很不一樣

1.在專案路徑下執行:flutter run --profile --trace-skia

2.點選執行完成後的連結,開啟的其實就是TimeLine View,但這時候需要選擇All,把所有函式都勾選上

3.然後操作App,點選refresh生成渲染圖表。

4.flutter 將一幀錄製成SkPicture(skp)送給Skia進行渲染。
用flutter screenshot --type=skia --observatory-port=<port>捕捉skp,並利用[debugger.skia.org]()我們可以上傳skp然後單步分析每一條繪圖指令。
複製程式碼

6.3 GPU 提高效能的總結

1.避免使用 Opacity widget,尤其是在動畫中避免使用。請用 AnimatedOpacity 或 FadeInImage 進行代替。更多資訊,請參閱:Performance considerations for opacity animation

有關將透明度直接應用於影象的示例,請參見 Transparent image,這比使用 Opacity widget 更快。
  For example:
  Container(color: Color.fromRGBO(255, 0, 0, 0.5))  ?
  Opacity(opacity: 0.5, child: Container(color: Colors.red)). ?
複製程式碼



2.Clip 不會呼叫 saveLayer()(除非明確使用 Clip.antiAliasWithSaveLayer),因此這些操作沒有 Opacity 那麼耗時,但仍然很耗時,所以請謹慎使用。



3.如果大多數 children widget 在螢幕上不可見,請避免使用返回具體列表的建構函式(例如 Column() 或 ListView()),以避免構建成本。使用帶有回撥的惰性方法(例如ListView.builder)。



4.避免呼叫 saveLayer()。

【為什麼 saveLayer 代價很大?】
呼叫 saveLayer() 會開闢一片離屏緩衝區。將內容繪製到離屏緩衝區可能會觸發渲染目標切換,這些切換在較早期的 GPU 中特別慢。

下面可能觸發saveLayer
  1  ShaderMask
  2  ColorFilter
  3  Chip -- might cause call to saveLayer() if disabledColorAlpha != 0xff
  4 Text -- might cause call to saveLayer() if there’s an overflowShader 
  
 避免呼叫 saveLayer() 的方式: 
  1: 要在影象中實現淡入淡出,請考慮使用 FadeInImage 小部件,該小部件使用 GPU 的片段著色器應用漸變不透明度。瞭解更多詳情,請參見 Opacity 文件。
  2: 要建立帶圓角的矩形,而不是應用剪下矩形,請考慮使用很多 widget 都提供的 borderRadius屬性。
複製程式碼



5.當有些widget被遮擋住了,不需要渲染了,可以使用Visibility來控制不可見。



6.使用 AnimatedBuilder 時,請避免在不依賴於動畫的 widget 的構造方法中構建 widget 樹。動畫的每次變動都會重建這個 widget 樹。而應該構建子樹的那一部分,並將其作為 child 傳遞給 AnimatedBuilder。



7.避免在動畫中剪裁。如果可能,請在動畫開始之前預先剪下影象。



8.優化頁面當有大量圖片載入的時候,效能的消耗,比如降低圖片質量來降低




參考:

  1. Flutter 應用效能優化最佳實踐
  2. Flutter 的效能測試和理論(剖析你的 Flutter app)
  3. Flutter 的高效能渲染原理

?推薦?:

日常學習Flutter開發的積累

相關文章