總的來說,Flutter 應用預設情況下是高效能的,因此您只需要避免常見的陷阱就能獲得出色的效能。這些最佳實踐建議將幫助您編寫出儘可能高效能的 Flutter 應用。

您如何設計一個 Flutter 應用以最高效地渲染您的場景?特別是,您如何確保框架生成的繪製程式碼儘可能高效?一些渲染和佈局操作已知速度較慢,但並非總是可以避免。應謹慎使用它們,並遵循以下指南。

最小化昂貴操作

#

某些操作比其他操作更昂貴,這意味著它們消耗更多的資源。顯然,您只希望在必要時使用這些操作。您的應用 UI 的設計和實現方式會對執行效率產生很大影響。

控制 build() 的成本

#

在設計 UI 時,請牢記以下幾點:

  • 避免在 build() 方法中進行重複且耗時的操作,因為當父級小部件重建時,build() 可能會被頻繁呼叫。
  • 避免建立具有大型 build() 函式的單個過大的 widget。根據封裝性以及它們的變化方式,將它們拆分成不同的 widget。
    • 當在 State 物件上呼叫 setState() 時,所有子孫 widget 都會重建。因此,將 setState() 呼叫限制在 UI 實際需要更改的子樹部分。如果更改僅限於子樹的一小部分,請避免在高層級呼叫 setState()
    • 當重新遇到與上一幀相同的子 widget 例項時,重建所有子孫的遍歷會停止。此技術在框架內大量用於最佳化動畫,當動畫不影響子孫子樹時。請參閱 TransitionBuilder 模式和 SlideTransition 的原始碼,它利用此原理在動畫時避免重建其子孫。(“相同的例項”透過 operator == 進行評估,但關於何時避免重寫 operator == 的建議,請參閱頁面末尾的陷阱部分。)
    • 儘可能多地在 widget 上使用 const 建構函式,因為它們允許 Flutter 縮短大部分重建工作。要自動獲得使用 const 的提醒,請啟用 flutter_lints 包中推薦的 lint。有關更多資訊,請檢視 flutter_lints 遷移指南
    • 要建立可重用的 UI 片段,請優先使用 StatelessWidget 而不是函式。

有關更多資訊,請參閱:


使用 StringBuffer 來高效地構建字串

#

當需要將多個部分合併成一個字串時,尤其是在迴圈內部,使用 + 運算子可能會效率低下,因為它會在每次連線時建立一個新的 String 物件。更好的方法是使用 StringBuffer,它會收集所有字串,並在呼叫 toString() 時只連線一次。

在 YouTube 上以新標籤頁觀看:“StringBuffer (Technique of the Week)”


謹慎使用 saveLayer()

#

一些 Flutter 程式碼使用 saveLayer()(一種昂貴的操作)來實現 UI 中的各種視覺效果。即使您的程式碼沒有顯式呼叫 saveLayer(),您使用的其他 widget 或包也可能在後臺呼叫它。也許您的應用呼叫 saveLayer() 的次數超過了必要,過多的 saveLayer() 呼叫會導致卡頓。

為什麼 saveLayer() 昂貴?

#

呼叫 saveLayer() 會分配一個螢幕外緩衝區,並將內容繪製到螢幕外緩衝區可能會觸發渲染目標切換。GPU 就像一個消防水龍帶,而渲染目標切換會迫使 GPU 暫時重定向該流,然後再將其導回。在移動 GPU 上,這對渲染吞吐量尤其具有破壞性。

什麼時候需要 saveLayer()?

#

在執行時,如果您需要動態顯示來自伺服器(例如)的各種形狀,每個形狀都有一些透明度,這些形狀可能會(也可能不會)重疊,那麼您幾乎必須使用 saveLayer()

除錯 saveLayer() 呼叫

#

您如何知道您的應用直接或間接呼叫 saveLayer() 的頻率?saveLayer() 方法會在 DevTools 時間軸上觸發一個事件;透過檢查 DevTools Performance 檢視中的 PerformanceOverlayLayer.checkerboardOffscreenLayers 開關,瞭解您的場景何時使用 saveLayer

最小化 saveLayer() 呼叫

#

您能否避免呼叫 saveLayer?這可能需要重新考慮您建立視覺效果的方式。

  • 如果呼叫來自您的程式碼,您能否減少或消除它們?例如,您的 UI 可能重疊了兩個形狀,每個形狀都有非零透明度。

    • 如果它們總是以相同的方式、相同的重疊量、相同的透明度重疊,您可以預先計算出這個重疊的半透明物件的外觀,將其快取起來,然後使用它而不是呼叫 saveLayer()。這適用於任何您可以預先計算的靜態形狀。
    • 能否重構您的繪製邏輯以完全避免重疊?
  • 如果呼叫來自您不擁有的包,請聯絡包所有者並詢問為什麼這些呼叫是必要的。能否減少或消除它們?如果不能,您可能需要尋找另一個包,或編寫自己的包。

其他可能觸發 saveLayer() 且可能昂貴的 widget:

  • ShaderMask
  • ColorFilter
  • Chip—如果 disabledColorAlpha != 0xff,可能會觸發對 saveLayer() 的呼叫。
  • Text—如果存在 overflowShader,可能會觸發對 saveLayer() 的呼叫。

最小化 opacity 和 clipping 的使用

#

Opacity 和 clipping 也是昂貴的操作。以下是一些您可能會覺得有用的技巧:

  • 僅在必要時使用 Opacity widget。請參閱 Opacity API 頁面上的 透明影像 部分,其中有一個直接將不透明度應用於影像的示例,這比使用 Opacity widget 更快。
  • 與其將簡單的形狀或文字包裝在 Opacity widget 中,不如通常使用半透明顏色繪製它們更快。(儘管這隻適用於要繪製的形狀中沒有重疊部分的情況。)
  • 要實現影像漸隱效果,請考慮使用 FadeInImage widget,它使用 GPU 的片段著色器應用漸進式不透明度。有關更多資訊,請檢視 Opacity 文件。
  • Clipping 不會呼叫 saveLayer()(除非明確使用 Clip.antiAliasWithSaveLayer 請求),因此這些操作不像 Opacity 那樣昂貴,但 clipping 仍然很昂貴,所以請謹慎使用。預設情況下,clipping 被停用(Clip.none),因此您必須在需要時顯式啟用它。
  • 要建立帶有圓角的矩形,請不要應用 clipping 矩形,而是考慮使用許多 widget 類提供的 borderRadius 屬性。

謹慎實現 grids 和 lists

#

您的 grids 和 lists 的實現方式可能會導致應用的效能問題。本節介紹建立 grids 和 lists 時的一項重要最佳實踐,以及如何確定您的應用是否使用了過多的佈局傳遞。

保持惰性!

#

構建大型 grid 或 list 時,請使用惰性構建器方法和回撥。這確保在啟動時只構建螢幕上可見的部分。

有關更多資訊和示例,請參閱:

避免 intrinsic 操作

#

有關 intrinsic 傳遞可能導致 grids 和 lists 出現問題的詳細資訊,請參閱下一節。


最小化由 intrinsic 操作引起的佈局傳遞

#

如果您已經進行了大量的 Flutter 程式設計,您可能熟悉 建立 UI 時佈局和約束的工作原理。您甚至可能已經記住了 Flutter 的基本佈局規則:約束向下傳遞,尺寸向上返回,父級設定位置。

對於某些 widget,尤其是 grids 和 lists,佈局過程可能會很昂貴。Flutter 努力只對 widget 進行一次佈局傳遞,但有時需要第二次傳遞(稱為intrinsic傳遞),這會降低效能。

什麼是 intrinsic 傳遞?

#

當您希望所有單元格具有最大或最小單元格的尺寸(或類似需要輪詢所有單元格的計算)時,就會發生 intrinsic 傳遞。

例如,考慮一個包含大量 Card 的 grid。一個 grid 應該具有統一大小的單元格,因此佈局程式碼會執行一次傳遞,從 grid 的根(在 widget 樹中)開始,詢問 grid 中的每個卡片(不只是可見的卡片)返回其intrinsic大小——即 widget 在沒有約束條件下的首選大小。有了這些資訊,框架會確定一個統一的單元格大小,並第二次遍歷所有 grid 單元格,告知每個卡片要使用的大小。

除錯 intrinsic 傳遞

#

要確定您是否有過多的 intrinsic 傳遞,請在 DevTools 中啟用Track layouts 選項(預設停用),並檢視應用的 堆疊跟蹤,瞭解執行了多少次佈局傳遞。啟用跟蹤後,intrinsic 時間軸事件會標記為 '$runtimeType intrinsics'。

避免 intrinsic 傳遞

#

您有幾種選擇可以避免 intrinsic 傳遞:

  • 預先將單元格設定為固定大小。
  • 選擇一個特定的單元格作為“錨點”單元格——所有單元格的大小都將相對於此單元格。編寫一個自定義 RenderObject,它首先定位子錨點,然後圍繞它佈局其他子項。

要更深入地瞭解佈局的工作原理,請參閱 Flutter 架構概述中的 佈局和渲染部分。


在 16 毫秒內構建和顯示幀

#

由於構建和渲染有兩個獨立的執行緒,因此在 60Hz 顯示器上,您有 16 毫秒用於構建,16 毫秒用於渲染。如果延遲是一個問題,請在 16 毫秒或更短的時間內構建和顯示一幀。請注意,這意味著在 8 毫秒或更短時間內構建,在 8 毫秒或更短時間內渲染,總共不超過 16 毫秒。

如果在profile 模式下,您的幀渲染時間遠低於 16 毫秒,即使存在一些效能陷阱,您可能也不必擔心效能,但您仍應努力盡快構建和渲染幀。原因如下:

  • 將幀渲染時間降低到 16 毫秒以下可能不會產生視覺上的差異,但它會提高電池續航能力並減少發熱問題。
  • 它可能在您的裝置上執行良好,但請考慮您要針對的最低端裝置的效能。
  • 隨著 120fps 裝置越來越普及,您將希望在 8 毫秒(總計)內渲染幀,以提供最流暢的體驗。

如果您想知道為什麼 60fps 能帶來流暢的視覺體驗,請觀看影片為什麼是 60fps?

陷阱

#

如果您需要調整應用的效能,或者 UI 不如您預期的流暢,DevTools Performance 檢視可以提供幫助!

此外,您 IDE 的 Flutter 外掛也可能很有用。在 Flutter Performance 視窗中,勾選Show widget rebuild information複選框。此功能可幫助您檢測何時渲染和顯示幀的時間超過 16 毫秒。在可能的情況下,外掛會提供指向相關提示的連結。

以下行為可能會對您的應用效能產生負面影響。

  • 避免使用 Opacity widget,尤其是在動畫中避免使用。請改用 AnimatedOpacityFadeInImage。有關更多資訊,請參閱 Opacity 動畫的效能注意事項

  • 在使用 AnimatedBuilder 時,避免在 builder 函式中放置一個會構建與動畫無關的 widget 的子樹。這個子樹會為動畫的每一次 tick 而重建。相反,一次性構建該子樹的一部分,並將其作為子項傳遞給 AnimatedBuilder。有關更多資訊,請參閱 效能最佳化

  • 在動畫中避免 clipping。如果可能,請在動畫化影像之前對其進行預裁剪。

  • 如果大多數子項不在螢幕上可見,請避免使用具有具體 List 子項的建構函式(例如 Column()ListView()),以避免構建成本。

  • 避免重寫 Widget 物件上的 operator ==。雖然它看起來可以避免不必要的重建,但實際上它會損害效能,因為它會導致 O(N²) 的行為。此規則的唯一例外是葉子 widget(沒有子項的 widget),在特定情況下,比較 widget 的屬性可能比重建 widget 更有效,並且 widget 的配置很少更改。即使在這種情況下,通常也最好依賴於快取 widget,因為即使一次對 operator == 的重寫也可能導致全域性效能下降,因為編譯器無法再假定呼叫始終是靜態的。

資源

#

有關更多效能資訊,請參閱以下資源: