0%

【文章翻譯】Raster thread performance optimization tips

【文章內容使用 Gemini 1.5 Pro 自動產生】

柵格執行緒效能優化技巧

最近,我坐下來調整 FlutterFolio 的效能,這個應用程式是作為 Flutter Engage 的設計展示而建立的。只改變了一處,我就讓 FlutterFolio 變得快了很多。

不過,首先,我必須找出要改什麼。這篇文章就是關於這個搜尋的。

FlutterFolio 是一個功能完整的應用程式,從設計到實作,花了 6 個星期 (!) 時間完成,支援行動裝置、桌面和網頁。開發團隊顯然必須做一些妥協 - 沒有任何批評。專案的範圍和非常短的時程迫使他們這樣做。

事實上,這是一個絕佳的機會,因為這個應用程式比我想到的所有範例應用程式都更加「真實」。

而且,效能優化在真實應用程式上比在合成問題上更好解釋。

第 1 步:效能分析

任何優化的第一步是什麼?量測。知道一個應用程式看起來很慢是不夠的。你需要更精確一點。兩個原因:

  1. 量測可以將我們引導到最糟糕的肇事者。任何應用程式的每個部分都可以變得更快、更有效率。但是,你必須從某個地方開始。效能分析讓我們看到哪些部分做得還可以,哪些部分做得不好。然後,我們可以專注於做得不好的部分,並在有限的時間內取得更大的進展。
  2. 我們可以比較前後。有時,程式碼變更看起來像是個好主意,但實際上,它並沒有產生顯著的差異。有了基準(之前)就表示我們可以量化變更的影響。我們可以將之前與之後進行比較。

應用程式的效能分析很困難。我在 2019 年寫了一篇關於這方面的 長篇文章。所以,讓我們從簡單的開始。我們在 profile 模式下執行應用程式,開啟效能覆蓋圖,並使用應用程式,同時觀察效能覆蓋圖的圖表。

我們立即看到柵格執行緒正在努力。

這尤其發生在捲軸瀏覽應用程式的主頁時。您應該始終優先考慮使用者花費大部分時間的部分或效能問題對使用者特別明顯的部分。換句話說,如果您有兩個效能問題,其中一個發生在開始畫面,另一個埋藏在設定頁面中,請先修復開始畫面。

讓我們看看柵格執行緒在做什麼。

旁白:UI 執行緒 vs. 柵格執行緒

其實,讓我們先澄清柵格執行緒做什麼。

所有 Flutter 應用程式都在至少兩個並行執行緒上運行:UI 執行緒和柵格執行緒。UI 執行緒是您建立 Widget 和應用程式邏輯運行的地方。(您可以建立隔離區,這表示您可以在其他執行緒上運行邏輯,但為了簡單起見,我們將忽略這一點。)柵格執行緒是 Flutter 用來 柵格化 您的應用程式的方式。它接收來自 UI 執行緒的指令,並將它們轉換成可以傳送到圖形卡的東西。

為了更具體,讓我們看看一個 build 函數:

1
2
3
Widget build(BuildContext context) {
return Image.asset('dash.png');
}

上面的程式碼在 UI 執行緒上運行。Flutter 架構找出要放置 Widget 的位置、要給它什麼尺寸,等等 - 仍然在 UI 執行緒上。

然後,在 Flutter 知道有關畫面的所有資訊之後,它就會交給柵格執行緒。柵格執行緒會取得 dash.png 中的位元組,調整圖片的大小(如果需要),然後應用不透明度、混合模式、模糊等等,直到它取得最終的像素。然後,柵格執行緒將得到的資訊傳送到圖形卡,因此也傳送到螢幕。

第 2 步:深入時間軸

好的,回到 FlutterFolio。開啟 Flutter DevTools 讓我們可以更仔細地查看時間軸。

效能 標籤中,您可以看到 UI 執行緒(淡藍色條)做得很好,而柵格執行緒(深藍色和紅色條)在每個畫面中花費了令人驚訝的時間,特別是在捲軸向下瀏覽主頁時。因此,問題不在於低效的 build 方法或商業邏輯。問題是要求柵格執行緒做太多事。

事實上,每個畫面 都花費了很長時間在柵格執行緒上,這告訴了我們一些事情。它表示我們要求柵格執行緒做一些工作 一次又一次 - 它不是偶爾做一次的事情。

讓我們選擇一個畫面,看看 時間軸事件 面板。

時間軸的頂部,帶有淺灰色背景,是 UI 執行緒。再次,您可以看到 UI 執行緒不是問題。

在 UI 執行緒下方,您可以看到柵格執行緒上的事件,從 GPURasterizer:Draw 開始。不幸的是,這就是事情變得有點模糊的地方。有許多對異國情調名稱方法的呼叫,例如 TransformLayer::Preroll、OpacityLayer::Preroll、PhysicalShapeLayer::Paint 等等。沒有關於這些方法中發生了什麼事情的詳細資訊,而且這些不是大多數 Flutter 開發人員認識的名稱。

它們是來自 Flutter 引擎的 C++ 方法。如果您想,您可以 搜尋 這些方法名稱,閱讀程式碼和註解,看看幕後發生了什麼。有時,這可以讓您對柵格執行緒在做什麼有更多直覺。但是,這種研究對於找出效能問題來說並不嚴格需要。(我直到相對最近才做過這種研究,但仍然能夠優化許多應用程式的效能。)

然後,有一個標記為 SkCanvas::Flush 的長事件。它花了 18 毫秒,遠遠超過合理範圍。不幸的是,該事件也沒有任何詳細資訊,因此我們需要做一點偵探工作。

SkCanvas 中的 Sk 代表 Skia,這是 Flutter 用於在其堆疊最底層進行渲染的圖形引擎。SkCanvas 是一個低階 C++ 類別,類似於 Flutter 自己 的 Canvas(如果您使用 CustomPaint,您可能已經熟悉它)。您應用程式的所有像素、線條、漸變 - 所有 UI - 都會經過 SkCanvas。而且,SkCanvas::Flush 是這個類別在收集到所有需要的資訊後進行大部分工作的地方。文件 指出 Flush 方法「解決所有未決的 GPU 作業」。

讓我們回顧一下我們從效能時間軸中学到的东西:

  • 柵格執行緒是主要問題。UI 執行緒做得相對良好。
  • 在捲軸時,柵格執行緒在 每個畫面 中花費了很長時間。一些昂貴的柵格化工作一直在進行。
  • SkCanvas::Flush 花費了很長時間,這表示 Skia 正在做很多功課。

我們 不知道 那個功課是什麼。讓我們回顧一下程式碼。

第 3 步:閱讀程式碼

有了知識,讓我們看看原始碼。如果程式碼不熟悉(就像我對 FlutterFolio 的情況一樣),最好從 profile 模式切換到 debug 模式,並使用 Flutter Inspector 跳轉到相關 Widget 的原始碼。

FlutterFolio 的主頁,至少在行動裝置上,似乎基本上是一個由 BookCoverWidgets 填充的垂直 PageView。查看 BookCoverWidget,您可以看到它本質上是一個 各種 Widget 的堆疊,從底部的大型圖片開始,繼續是一些動畫覆蓋層、主要文字內容,最後是頂部的滑鼠懸停覆蓋層。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
child: Stack(fit: StackFit.expand, children: [
/// /////////////////////////////<br>
/// 背景圖片
// 當我們滑鼠懸停時,動畫縮放
AnimatedScale(
duration: Times.slow,
begin: 1,
end: isClickable ? 1.1 : 1,
child: BookCoverImage(widget.data),
),

/// 黑色覆蓋層,在滑鼠懸停時淡出
AnimatedContainer(duration: Times.slow,
color: Colors.black.withOpacity(overlayOpacity)),

/// 當處於大型模式時,顯示一些漸變,
/// 應該位於文字元素下方
if (widget.largeMode) ...[
FadeInLeft(
duration: Times.slower,
child: _SideGradient(Colors.black),
),
FadeInUp(child: _BottomGradientLg(Colors.black))
] else ...[
FadeInUp(child: _BottomGradientSm(Colors.black)),
],

/// 放在文字內容下方,點擊時取消焦點。
GestureDetector(behavior: HitTestBehavior.translucent,
onTap: InputUtils.unFocus),

/// BookContent,顯示大型封面或小型封面
Align(
alignment: widget.topTitle ? Alignment.topLeft : Alignment.bottomLeft,
// 根據我們處於哪種模式,對填補進行動畫轉場
child: AnimatedContainer(
duration: Times.slow,
padding: EdgeInsets.all(widget.largeMode ? Insets.offset : Insets.sm),
child: (widget.largeMode)
? LargeBookCover(widget.data)
: SmallBookCover(widget.data, topTitle: widget.topTitle),
),
),

/// 滑鼠懸停效果
if (isClickable) ...[
Positioned.fill(child: FadeIn(child: RoundedBorder(color: theme.accent1, ignorePointer: false))),
],
]),

現在,請記住:您正在尋找的是在每個畫面中都會發生的東西(也就是說,它始終存在),並且可能對於 Skia 渲染器來說繪製起來很昂貴(圖片、模糊、混合等等)。

第 4 步:深入探討

現在,您需要深入探討以找出可能存在问题的 Widget。一種方法是暫時從應用程式中移除各種 Widget,看看這對效能有什麼影響。

請記住,堆疊的第一個子元素是背景,每個後續子元素都是之前 Widget 上方的圖層。所以,第一個子元素是背景圖片,由 BookCoverImage 表示。您可以移除它,但主頁看起來會像這樣:

這就失去了整個頁面的意義。仔細查看 BookCoverImage,您可以看到它只是一個簡單的 Image 函式包裝器。除了一个值得注意的例外(本文稍後會提到)之外,這裡沒有什麼可以改進的地方。

繼續,有這段程式碼:

1
2
3
/// 黑色覆蓋層,在滑鼠懸停時淡出
AnimatedContainer(duration: Times.slow,
color: Colors.black.withOpacity(overlayOpacity)),

這是一個用透明黑色覆蓋整個圖片的 Widget。overlayOpacity 預設情況下為 0(而且大部分時間都是如此),所以這個圖層是完全透明的。嗯。讓我們將它移除,並再次在 profile 模式下執行應用程式。

有趣!柵格執行緒仍然承擔了大量負載,但效能有了顯著改善。

我決定為 FlutterFolio 實作一個更強大的效能分析工具,以便我可以證明改進是真實的,而不仅仅是偶然。這個變更讓我柵格化所花費的 CPU 時間整體減少了 20%,潛在的卡頓減少了 50%。

總的來說,這是一個巨大的變化,因為移除了一個大部分時間都無效的單個 Widget。

修復 很簡單:

1
2
3
4
/// 黑色覆蓋層,在滑鼠懸停時淡出
if (overlayOpacity > 0)
AnimatedContainer(duration: Times.slow,
color: Colors.black.withOpacity(overlayOpacity)),

現在,您只在它具有非零不透明度(也就是說,它至少部分可見)時才會加入透明覆蓋層。您避免了(非常常見的!)情況,即建立並柵格化一個完全透明的圖層,但它没有任何效果。

就這樣,應用程式變得更加流暢,也更節省電量。

注意:為什麼你需要這樣做?Flutter 不應該足夠聰明地為我們執行這個優化嗎?請閱讀 此處 的議題,了解為什麼它做不到。為什麼透明不透明度在第一時間會變慢?這超出了本文的範圍,但它與堆疊中更上方的 BackdropFilter Widget 相關,它會與下方每個 Widget 相互作用。

本文的主要目的不是教你關於這個特定效能陷阱的知識。你可能永遠不會再看到它。我的目標是教你如何一般性地優化柵格執行緒效能。

第 5 步:概括

在繼續處理一個完全不同的問題之前,通常最好查看專案中的其他地方,看看是否有類似的問題。我們的應用程式中是否有其他地方使用了大面積覆蓋層?你能避免它們嗎?

在這種情況下,接下來的幾行會建立在您捲軸時會淡入的大型漸變:

1
2
3
4
5
6
7
8
9
10
11
/// 當處於大型模式時,顯示一些漸變,
/// 應該位於文字元素下方
if (widget.largeMode) ...[
FadeInLeft(
duration: Times.slower,
child: _SideGradient(Colors.black),
),
FadeInUp(child: _BottomGradientLg(Colors.black))
] else ...[
FadeInUp(child: _BottomGradientSm(Colors.black)),
],

而且,果然,移除這些動畫的、幾乎佔據全螢幕的漸變會顯著改善捲軸效能。不幸的是,在這種情況下,解決方案不像之前的範例那样简单。這些漸變不是無形的。它們會在使用者到達特定封面時開始淡入。移除它们 确实 会造成視覺上的差異。

一個想法是稍微延遲淡入,這樣動畫只会在使用者降落在特定 BookCover 上時才會開始。這樣一來,您就可以減輕柵格執行緒的負載,而使用者在捲軸時,也希望可以避免一些潛在的卡頓。

但是,這涉及到對應用程式的動態設計進行修改,因此需要與更廣泛的團隊討論。許多效能優化都會屬於這類。效能優化通常是折衷的過程。

重複步驟 2-5 直到滿意

到目前為止,我們只查看了一種類型的問題。總是會有更多問題。

以下是一個關於下一步的想法:應用程式的圖片資產是否太大?請記住,柵格執行緒負責取得圖片位元組、解碼它們、調整大小、應用濾鏡等等。如果它將一個 20 MB 的高解析度圖片載入到螢幕上的一個小頭像圖片中,那麼您就是在浪費資源。

當您的應用程式在 debug 模式下執行時,您可以使用 Flutter Inspector 來 反轉過大的圖片

這將會反轉顏色並翻轉應用程式中對於實際用途來說太大的所有圖片。然后,您可以仔细检查应用程序,并注意不自然的变化。

debug 模式還會在每次遇到這種圖片時報告一個錯誤,例如:

[錯誤] 圖片 assets/images/empty-background.png 的顯示大小為 411×706,但解碼大小為 2560×1600,額外使用了 19818KB。

不過,這裡的解決方案並不直接。在行動裝置上,您不需要 2560×1600 的圖片,但在桌面上,您可能需要。請記住,FlutterFolio 是一個在所有 Flutter 目標上執行的應用程式,包括桌面。如有疑問,請 閱讀 API 文件

結語

如您所見,優化效能是一门艺术和科学。強大的基準測試以及對框架及其內建 Widget 的深刻理解都有助於這一點。

最终,熟能生巧。優化足夠多的應用程式,你就會變得更好。

祝您狩獵愉快。


柵格執行緒效能優化技巧 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

https://creativecommons.org/licenses/by-nc/4.0/