??Flutter 性能優化實踐 總結??

??歡迎前往本人的GitHub查看更多內容。點擊前往GitHub

在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性能測試?

答案:
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.如何查看分析性能圖層?

答案:
性能圖層用兩張圖表顯示應用的耗時信息。如果 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)。該線程并不會在性能圖層中展示。


紅色豎條表明當前幀的渲染和繪制都很耗時 當兩張圖表都是紅色時,就要開始對 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 渲染









5.2 UI 調試步驟

1.在mian里面設置

  • debugDumpLayerTree ○ 查看layer樹
  • debugPaintLayerBordersEnabled ○ 查看layer界限
  • debugRepaintRainbowEnabled ○ 被重新繪制的RenderObject
  • debugProfilePaintsEnabled ○ 在觀測臺里顯示繪制樹

2.profile下真機運行

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

4.查看分析


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 圖形渲染


因為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開發的積累

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。