著色器編譯時卡頓

如果你的行動應用程式上的動畫只在首次執行時卡頓,很可能是著色器編譯引起的。 Flutter 對著色器編譯卡頓的長期解決方案是 Impeller,在 iOS 上可以直接使用,在 Android 上透過啟用特定標誌來使用。

在我們努力讓 Impeller 為生產做好準備的時候,你可以嘗試將預編譯的著色器與 iOS 應用繫結在一起來減輕著色器編譯的卡頓。不幸的是,由於預編譯的著色器是針對裝置或特定 GPU 進行最佳化的,所以這種方法在 Android 上效果不佳。 Android 的硬體生態系統非常龐大,因此與應用程式繫結的特定 GPU 預編譯著色器只能在一小部分裝置上執行,而且很可能會加劇其他裝置上的卡頓問題,甚至引發渲染錯誤。

另外,請注意,我們並不打算改進下面描述的建立預編譯著色器。相反,為了真正解決這個問題,我們將主要精力集中在 Impeller 提供的更強大的解決方案上。

什麼是著色器編譯卡頓?

著色器是在 GPU(圖形處理單元)上執行的程式碼。當 Flutter 渲染的 Skia 圖形後端首次看到新的繪製指令序列時,它有時會生成和編譯一個自定義的 GPU 著色器用於該指令序列。使得該序列和潛在類似的序列能夠儘可能快地渲染。

然而不幸的是,Skia 著色器生成和編譯的過程與幀的工作是依次進行的。編譯過程可能需要幾百毫秒的時間,而對於 60 幀/秒 (frame-per-second) 的顯示來說,一個流暢的幀必須在 16 毫秒內繪製完成。因此,編譯過程可能導致數十幀被丟失,使幀數從 60 降到 6。這就是所謂的 編譯卡頓 。編譯完成之後,動畫應該會變得流暢。

另一方面,Impeller 在我們建立 Flutter 引擎時已經生成並編譯了所有必要的著色器。因此,在 Impeller 上執行的應用程式已經擁有了它們所需的所有著色器,並且這些著色器不會在動畫中引起卡頓。

要獲得更加確切的著色器編譯卡頓存在的證據,你可以在 --trace-skia 開啟時檢視追蹤檔案中的 GrGLProgramBuilder::finalize。下面的截圖展示了一個 timeline 追蹤的範例。

A tracing screenshot verifying jank

如何定義「首次執行」?

在 iOS 上來說,「首次執行」意味著使用者可能在每次開啟應用後,在動畫首次載入時都會出現卡頓。

如何使用 SkSL 預熱

Flutter 為應用開發者提供了一個指令行工具以收集終端使用者在 SkSL(Skia 著色器語言)進行格式化處理中需要用到的著色器。 SkSL 著色器可以被打包進應用,並提前進行預熱(預編譯),這樣當終端使用者第一次開啟應用時,就能夠減少動畫的編譯掉幀了。使用下面的指令收集並打包 SkSL 的著色器:

  1. ​ 開啟 --cache-sksl 執行你的應用以捕獲 SkSL 中的著色器:

    flutter run --profile --cache-sksl
    

    如果這個相同的應用之前執行的時候沒有使用 --cache-sksl,你需要加上 --purge-persistent-cache 標誌:

    flutter run --profile --cache-sksl --purge-persistent-cache
    

    這個標誌將會刪除可能干擾 SkSL 的較舊的非 SkSL 著色器快取捕獲的著色器。它還清除了 SkSL 著色器,因此在第一次使用 --cache-sksl 執行。

  2. 儘可能多觸發應用的動畫,特別是那些會引起編譯卡頓的。

  3. 在執行 flutter run 指令後行按下 M 鍵以捕獲 SkSL 著色器到一個類似 flutter_01.sksl.json 的檔案中。為了達到最好的效果,最好是能夠在 iOS 真機上抓取 SkSL 著色器,在模擬器上的抓取通常會是無效的。

  4. Build the app with SkSL warm-up using the following, as appropriate:

    在下面的指令中選擇合適的建立帶有 SkSL 預熱的應用:
    
    flutter build ios --bundle-sksl-path flutter_01.sksl.json
    

    如果它會建立一個類似 test_driver/app.dart 的驅動測試,請確保指定 --target=test_driver/app.dart。(例如 flutter build ios --bundle-sksl-path flutter_01.sksl.json --target=test_driver/app.dart

  5. Test the newly built app.

或者,你可以編寫一些整合測試來使用一個指令自動執行前三個步驟。例如:

flutter drive --profile --cache-sksl --write-sksl-on-exit flutter_01.sksl.json -t test_driver/app.dart

使用這樣的 整合測試,無論是程式碼發生改變或者 Flutter 更新了,你都可以輕鬆獲得可靠的著色器快取。這些測試也被用於驗證開啟著色器預熱前後的效能變化上。更好的做法是,你可以把這些測試放進 CI(持續整合)系統上,這樣就能在每次應用發布前自動生成並測試著色器快取了。

就拿原始版本的 Flutter Gallery 舉例。我們讓 CI 系統在每次 Flutter commit 後都生成著色器快取,並在 transitions_perf_test.dart 中驗證效能。更多詳細訊息請檢視 Flutter Gallery sksl 預熱轉場性能驗證,以及 Flutter Gallery sksl 預熱轉場在 iOS_32 上的效能驗證

在這種這種整合測試中,最差的幀光柵化時間是一個很好的指標來衡量著色器編譯卡頓的嚴重性。例如,上述步驟減少了 Flutter gallery 應用的著色器編譯卡頓,並減少了它在 Moto G4 手機上的最差的幀光柵化時間,從 ~90 ms 減少到 ~40 ms。在 iPhone 4s 上,它從 ~300 ms 減少到 ~80 ms。這種視覺差異如同本文開頭所示一樣。