??Flutter 性能優(yōu)化實踐 總結(jié)??

??歡迎前往本人的GitHub查看更多內(nèi)容。點擊前往GitHub

在flutter的開發(fā)和工作中,因為工作內(nèi)容的要求越來越高,加上一位優(yōu)秀的同事,自己也對自己的寫的代碼除了規(guī)范的要求,也開始對性能做了優(yōu)化。我們開發(fā)的App屬于首頁就是重點,剛好是我負責(zé),所以再簡單的UI和邏輯搭建完成后,要求達到一定的性能優(yōu)化,所以自己開始了解和學(xué)習(xí)相關(guān)的處理。






0.渲染相關(guān)知識了解

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

Debug
??Debug模式可以在真機和模擬器上同時運行:會打開所有的斷言,包括debugging信息、debugger aids(比如observatory)和服務(wù)擴展。優(yōu)化了快速develop/run循環(huán),但是沒有優(yōu)化執(zhí)行速度、二進制大小和部署。命令flutter run就是以這種模式運行的,通過sky/tools/gn --android或者sky/tools/gn --ios來build。有時候也被叫做“checked模式”或者“slow模式”。

Release
??Release模式只能在真機上運行,不能在模擬器上運行:會關(guān)閉所有斷言和debugging信息,關(guān)閉所有debugger工具。優(yōu)化了快速啟動、快速執(zhí)行和減小包體積。禁用所有的debugging aids和服務(wù)擴展。這個模式是為了部署給最終的用戶使用。命令flutter run --release就是以這種模式運行的,通過sky/tools/gn --android --runtime-mode=release或者sky/tools/gn --ios --runtime-mode=release來build。

Profile
?? Profile模式只能在真機上運行,不能在模擬器上運行:基本和Release模式一致,除了啟用了服務(wù)擴展和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的架構(gòu)主要分成三層:Framework,Engine,Embedder。

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


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


3.Embedder是一個嵌入層,即把Flutter嵌入到各個平臺上去,這里做的主要工作包括渲染Surface設(shè)置,線程設(shè)置,以及插件等。
從這里可以看出,F(xiàn)lutter的平臺相關(guān)層很低,平臺(如iOS)只是提供一個畫布,剩余的所有渲染相關(guān)的邏輯都在Flutter內(nèi)部,這就使得它具有了很好的跨端一致性。

0.2 Widget、Element、RenderObject三者的關(guān)系如下:

Widget實際上就是Element的配置數(shù)據(jù),Widget樹實際上是一個配置樹,而真正的UI渲染樹是由Element構(gòu)成;不過,由于Element是通過Widget生成,所以它們之間有對應(yīng)關(guān)系,所以在大多數(shù)場景,我們可以寬泛地認為Widget樹就是指UI控件樹或UI渲染樹。

一個Widget對象可以對應(yīng)多個Element對象。這很好理解,根據(jù)同一份配置(Widget),可以創(chuàng)建多個實例(Element)。

從創(chuàng)建到渲染的大體流程是:根據(jù)Widget生成Element,然后創(chuàng)建相應(yīng)的RenderObject并關(guān)聯(lián)到Element.renderObject屬性上,最后再通過RenderObject來完成布局排列和繪制。






1.能否在模擬器下進行性能調(diào)試?

答案:可以,但是調(diào)試很不準(zhǔn)確。所以不建議使用模擬器進行性能調(diào)試。
幾乎全部的 Flutter 應(yīng)用性能調(diào)試都應(yīng)該在真實的 Android 或者 iOS 設(shè)備上以分析模式進行。
通常來說,調(diào)試模式或者是模擬器上運行的應(yīng)用的性能指標(biāo)和發(fā)布模式的表現(xiàn)并不相同。 
應(yīng)該考慮在用戶使用的最慢的設(shè)備上檢查性能。

  • 為什么應(yīng)該在真機上運行:

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

    • 調(diào)試模式相比分析模式或者發(fā)布編譯來說,增加了額外的檢查(例如斷言),這些檢查可能相當(dāng)耗費資源。

  • 調(diào)試模式和發(fā)布模式代碼執(zhí)行的方式也是不同的。調(diào)試編譯采用的是“just in time”(JIT)模式運行應(yīng)用,而分析和發(fā)布模式則是預(yù)編譯到本地指令(“ahead of time”,或者叫 AOT)之后再加載到設(shè)備中。JIT本身的編譯就可能導(dǎo)致應(yīng)用暫停,從而導(dǎo)致卡頓。







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

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

2.在 VS Code中,打開 launch.json 文件,設(shè)置 flutterMode 屬性為 profile(當(dāng)分析完成后,改回 release 或者 debug)
    2.1 選擇 View > Command Palette… 來打開 command palette。
    2.2 在文本框中輸入“performance”并在彈出列表中選中 Toggle Performance Overlay。如果命令不可用,請確保應(yīng)用在運行狀態(tài)。

3.From the command line, use the --profile flag: 命令行使用 --profile 參數(shù)運行
  3.1 flutter run --profile
  3.2 使用 p 參數(shù)觸發(fā)性能圖層
  
  
4.可以通過在 MaterialApp 或者 WidgetsApp 的構(gòu)造方法中設(shè)置 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.如何查看分析性能圖層?

答案:
性能圖層用兩張圖表顯示應(yīng)用的耗時信息。如果 UI 產(chǎn)生了卡頓(跳幀),這些圖表可以幫助分析原因。圖表在當(dāng)前應(yīng)用的最上層展示,但并不是用普通的 widget 方式繪制的—Flutter 引擎自身繪制了該圖層來盡可能減少對性能的影響。每一張圖表都代表當(dāng)前線程的最近 300 幀表現(xiàn)。

左圖:GPU 線程的性能情況在上面,UI 線程顯示在下面,垂直的綠色條條代表的是當(dāng)前幀。
右圖:每一幀渲染過程當(dāng)中總共使用的時間
Flutter 用了一些額外的線程來完成這項工作。開發(fā)者的 Dart 代碼都在 UI 線程運行。盡管沒有直接訪問其他線程的權(quán)限,但 UI 線程的動作還是對其他線程的性能有影響的。

平臺線程
該平臺的主線程。插件代碼在這里運行。更多信息請參閱:iOS 的 UIKit 文檔,或者 Android 的 MainThread 文檔。性能圖層并不會展示該線程。

UI 線程
UI 線程在 Dart VM 執(zhí)行 Dart 代碼。該線程包括開發(fā)者寫下的代碼和 Flutter 框架根據(jù)應(yīng)用行為生成的代碼。當(dāng)應(yīng)用創(chuàng)建和展示場景的時候,UI 線程首先建立一個 圖層樹(layer tree) ,一個包含設(shè)備無關(guān)的渲染命令的輕量對象,并將圖層樹發(fā)送到 GPU 線程來渲染到設(shè)備上。 不要阻塞這個線程! 在性能圖層的最低欄展示該線程。

GPU 線程
GPU 線程取回圖層樹并通知 GPU 渲染。盡管無法直接與 GPU 線程或其數(shù)據(jù)通信,但如果該線程變慢,一定是開發(fā)者 Dart 代碼中的某處導(dǎo)致的。圖形庫 Skia 在該線程運行,有時也被叫做 光柵器(rasterizer)線程 。在性能圖層的最頂欄展示該線程。

I/O 線程
可能阻塞 UI 或者 GPU 線程的耗時任務(wù)(大多數(shù)情況下是I/O)。該線程并不會在性能圖層中展示。


紅色豎條表明當(dāng)前幀的渲染和繪制都很耗時 當(dāng)兩張圖表都是紅色時,就要開始對 UI 線程(Dart VM)進行診斷了。

每一幀都應(yīng)該在 1/60 秒(大約 16ms)內(nèi)創(chuàng)建并顯示。
如果有一幀超時(任意圖像)而無法顯示,就導(dǎo)致了卡頓,圖表之一就會展示出來一個紅色豎條。
如果是在 UI 圖表出現(xiàn)了紅色豎條,則表明 Dart 代碼消耗了大量資源。
而如果紅色豎條是在 GPU 圖表出現(xiàn)的,意味著場景太復(fù)雜導(dǎo)致無法快速渲染。
為什么需要在 16ms 內(nèi)渲染完成每一幀
1.將幀渲染時間降低到 16ms 以下可能在視覺上看不出來什么變化,但可以延長電池壽命以及避免發(fā)熱問題。
2.可能在你當(dāng)前測試設(shè)備上運行良好,但請考慮在應(yīng)用所支持的最低端設(shè)備上的情況。
3.當(dāng) 120fps 的設(shè)備普及之后,便需要在 8ms 之內(nèi)完成每一幀的渲染來保證流暢平滑的體驗。






4.如何進行性能分析并開始處理?

4.1 定位 UI 圖表中的問題

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

使用 Dart DevTool 進行性能分析
Dart DevTool 提供諸如性能分析、堆測試以及顯示代碼覆蓋率等功能。
DevTool 的 timeline 界面可以讓開發(fā)者逐幀分析應(yīng)用的 UI 性能。

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

4.2 定位 GPU 圖表中的問題

有些情況下界面的圖層樹構(gòu)造起來雖然容易,但在 GPU 線程下渲染卻很耗時。
這種情況發(fā)生時,UI 圖表沒有紅色,但 GPU 圖表會顯示紅色。
這時需要找出代碼中導(dǎo)致渲染緩慢的原因。
特定類型的負載對 GPU 來說會更加復(fù)雜。
可能包括不必要的對 saveLayer 的調(diào)用,許多對象間的復(fù)雜操作,還可能是特定情形下的裁剪或者陰影。

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

4.2.1 檢查屏幕之外的視圖

saveLayer

saveLayer 方法是 Flutter 框架中最重量的操作之一。 更新屏幕時這個方法很有用,但它可能使應(yīng)用變慢,如果不是必須的話,應(yīng)該避免使用這個方法。 即便沒有顯式地調(diào)用 saveLayer,也可能在其他操作中間接調(diào)用了該方法??梢允褂?PerformanceOverlayLayer.checkerboardOffscreenLayers 開關(guān)來檢查場景是否使用了 saveLayer。 打開開關(guān)之后,運行應(yīng)用并檢查是否有圖像的輪廓閃爍。如果有新的幀渲染的話,容器就會閃爍。 舉個例子,也許有一組對象的透明度要使用 saveLayer 來渲染。 在這種情況下,相比通過 widget 樹中高層次的父 widget 操作,單獨對每個 widget 來應(yīng)用透明度可能性能會更好。其他可能大量消耗資源的操作也同理,比如裁剪或者陰影。

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

4.2.2 檢查沒有緩存的圖像

RepaintBoundary
使用 RepaintBoundary 來緩存圖片是個好主意, 當(dāng)需要的時候 。 從資源的角度看,最重量級的操作之一是用圖像文件來渲染紋理。 首先,需要從持久存儲中取出壓縮圖像,然后解壓縮到宿主存儲中(GPU 存儲),再傳輸?shù)皆O(shè)備存儲器中(RAM)。也就是說,圖像的 I/O 操作是重量級的。 緩存提供了復(fù)雜層次的快照,這樣就可以方便地渲染到隨后的幀中。 因為光柵緩存入口的構(gòu)建需要大量資源,同時增加了 GPU 存儲的負載,所以只在必須時才緩存圖片。 打開PerformanceOverlayLayer.checkerboardRasterCacheImages 開關(guān)可以檢查哪些圖片被緩存了。 運行應(yīng)用來查看使用隨機顏色網(wǎng)格渲染的圖像,標(biāo)識被緩存的圖像。當(dāng)和場景交互時,網(wǎng)格里的圖片應(yīng)該是靜止的—代表重新緩存圖片的閃爍視圖不應(yīng)該出現(xiàn)。 大多數(shù)情況下,開發(fā)者都希望在網(wǎng)格里看到的是靜態(tài)圖片,而不是非靜態(tài)圖片。如果靜態(tài)圖片沒有被緩存,可以將其放到 RepaintBoundary widget 中來緩存。雖然引擎也可能忽略 repaint boundary,如果它認為圖像還不夠復(fù)雜的話。

4.2.3 檢視 widget 重建性能

顯示性能數(shù)據(jù)
Flutter 框架的設(shè)計使得構(gòu)建達不到 60fps 流暢度的應(yīng)用變得困難。通常情況下如果卡頓,就是因為每一幀被重建的 UI 比需求更多的簡單 bug。Widget rebuild profiler 可以幫助調(diào)試和修復(fù)這些問題引起的 bug。 可以檢視 widget inspector 中當(dāng)前屏幕和幀下的 widget 重建數(shù)量。了解細節(jié),可以參考 在 Android Studio 或類 IntelliJ 里開發(fā) Flutter 應(yīng)用 中的 顯示性能數(shù)據(jù)。






5.UI 應(yīng)用性能優(yōu)化總結(jié)

5.1 UI 渲染









5.2 UI 調(diào)試步驟

1.在mian里面設(shè)置

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

2.profile下真機運行

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

4.查看分析


5.3 UI 提高性能的總結(jié)

1.避免在 build() 方法中進行重復(fù)且耗時的工作,因為當(dāng)父 Widget 重建時,子 Wdiget 的 build() 方法會被頻繁地調(diào)用。



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



3.當(dāng)重新遇到與前一幀相同的子 Widget 實例時,將停止遍歷。這種技術(shù)在框架內(nèi)部大量使用,用于優(yōu)化動畫不影響子樹的動畫。請參閱 TransitionBuilder 模式和使用此原則的 SlideTransition,以避免在動畫過程中重建其后代 Widget?!咎岣遙uild的效率- 停止樹的遍歷】



4.需要更新的地方添加RepaintBoundary去設(shè)置一個獨立圖層,來減少圖層更新節(jié)點的數(shù)量【提高paint的效率】






6.GPU 應(yīng)用性能優(yōu)化總結(jié)

6.1 GPU 圖形渲染


因為Dart代碼直接調(diào)用SKia的C和C++代碼,當(dāng)Dart代碼能夠媲美Java代碼就能夠達到Flutter App的性能媲美原生App。

Skia(開源圖形引擎)是一個C++的開源2D向量圖形處理函數(shù)庫(Cairo是一個矢量庫),包括字型、坐標(biāo)轉(zhuǎn)換、位圖等等,相當(dāng)于輕量級的Cairo,目前主要用于Google的Android和Chrome平臺,Skia搭配OpenGL/ES與特定的硬件特征,強化顯示的效果。另外,Skia是WebKit支持的眾多圖形平臺之一,在WebKit的GraphicsContext.h/.c中有相關(guān)實現(xiàn)。

6.2 GPU 調(diào)試步驟

使用真機進行性能調(diào)試,Skia 有兩套很不同的后端,F(xiàn)lutter在iOS模擬器中使用純CPU后端,而真機設(shè)備一般使用GPU硬件加速后端,所以性能特性很不一樣

1.在項目路徑下運行:flutter run --profile --trace-skia

2.點擊運行完成后的鏈接,打開的其實就是TimeLine View,但這時候需要選擇All,把所有函數(shù)都勾選上

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

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

6.3 GPU 提高性能的總結(jié)

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

有關(guān)將透明度直接應(yīng)用于圖像的示例,請參見 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 不會調(diào)用 saveLayer()(除非明確使用 Clip.antiAliasWithSaveLayer),因此這些操作沒有 Opacity 那么耗時,但仍然很耗時,所以請謹慎使用。



3.如果大多數(shù) children widget 在屏幕上不可見,請避免使用返回具體列表的構(gòu)造函數(shù)(例如 Column() 或 ListView()),以避免構(gòu)建成本。使用帶有回調(diào)的惰性方法(例如ListView.builder)。



4.避免調(diào)用 saveLayer()。

【為什么 saveLayer 代價很大?】
調(diào)用 saveLayer() 會開辟一片離屏緩沖區(qū)。將內(nèi)容繪制到離屏緩沖區(qū)可能會觸發(fā)渲染目標(biāo)切換,這些切換在較早期的 GPU 中特別慢。

下面可能觸發(fā)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 
  
 避免調(diào)用 saveLayer() 的方式: 
  1: 要在圖像中實現(xiàn)淡入淡出,請考慮使用 FadeInImage 小部件,該小部件使用 GPU 的片段著色器應(yīng)用漸變不透明度。了解更多詳情,請參見 Opacity 文檔。
  2: 要創(chuàng)建帶圓角的矩形,而不是應(yīng)用剪切矩形,請考慮使用很多 widget 都提供的 borderRadius屬性。



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



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



7.避免在動畫中剪裁。如果可能,請在動畫開始之前預(yù)先剪切圖像。



8.優(yōu)化頁面當(dāng)有大量圖片加載的時候,性能的消耗,比如降低圖片質(zhì)量來降低






參考:

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

??推薦??:

日常學(xué)習(xí)Flutter開發(fā)的積累

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容