HarmonyOS:幀率和丟幀分析實踐

为敢技术發表於2024-10-31

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤部落格園地址:為敢技術(https://www.cnblogs.com/strengthen/
➤GitHub地址:https://github.com/strengthen
➤原文地址:https://www.cnblogs.com/strengthen/p/18517566
➤如果連結不是為敢技術的部落格園地址,則可能是爬取作者的文章。
➤原文已修改更新!強烈建議點選原文地址閱讀!支援作者!支援原創!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

丟幀問題概述

應用丟幀通常指的是在應用程式的介面繪製過程中,由於某些原因導致介面繪製的幀率下降,從而造成介面卡頓、動畫不流暢等問題。以60Hz重新整理率為例子,想要達到每秒60幀(即60fps)的流暢體驗,每一幀需要在16.7ms內完成,如果超過16.7ms未完成渲染,就可能會出現丟幀。

本文主要是以Trace資料為切入點進行分析,相應的工具可以使用DevEco Studio內建的Frame分析,若開發者需要補充Trace相關知識,可以參考Trace打點資訊說明等應用開發文件。

本文將主要介紹以下內容,來幫助開發者解決應用丟幀問題:

  • 丟幀問題原理
  • 發現丟幀問題
  • 丟幀問題分析
  • 常見丟幀問題
  • 丟幀問題最佳化建議

丟幀問題原理

在瞭解如何定位應用丟幀問題之前,開發者需要簡單瞭解HarmonyOS中圖形渲染的流程,便於在遇到卡頓時可以分析卡頓可能出現的階段和原因。

在HarmonyOS中,圖形系統採用了統一渲染的模式,遵循著一個典型的流水線模式,以90Hz重新整理率為例,每個Vsync週期是11.1ms,整個過程如下圖所示。如果是60Hz,每個Vsync的週期是16.7ms;如果是120Hz,則每個Vsync的週期是8.3ms。

圖1 90Hz重新整理率渲染流程

在整個渲染流程中,首先是由應用側響應消費者的螢幕點選等輸入事件,由應用側處理完成後再提交給Render Service,由Render Service協調GPU等資源處理後,再將最終的影像統一送到螢幕上進行顯示。

  1. 應用側(App)處理使用者的螢幕點選等輸入事件,生成當前介面描述的資料結構。其中,介面描述資料包括UI元素的位置,大小,資源,UI元素的繪製指令,動效屬性等。
  2. Render Service(渲染服務部件)是圖形棧中負責介面內容繪製的模組,其主要職責就是對接ArkUI框架,支撐ArkUI應用的介面顯示,包括控制元件、動效等UI元素。Render Service的RenderThread執行緒在Vsync下觸發UI繪製,繪製過程包含3個階段:Animation動效,Draw描畫和Flush提交。
  3. Display是顯示螢幕的抽象概念,可以是實際的物理屏也可以是虛擬屏。

其中應用側的渲染流程如下圖所示,瞭解ArkUI的渲染流程有助於我們定位應用側的卡頓問題出現在哪個環節:

圖2 ArkUI渲染管線結構與Frame Insight效能打點

  • Animation:動畫階段,在動畫過程中會修改相應的FrameNode節點觸發髒區標記,在特定場景下會執行使用者側ets程式碼實現自定義動畫;
  • Events:事件處理階段,比如手勢事件處理。在手勢處理過程中也會修改FrameNode節點觸發髒區標記,在特定場景下會執行使用者側ets程式碼實現自定義事件;
  • UpdateUI:自定義元件(@Component)在首次建立掛載或者狀態變數變更時會標記為需要rebuild狀態,在下一次Vsync過來時會執行rebuild流程,rebuild流程會執行程式UI程式碼,透過呼叫View的方法生成相應的元件樹結構和屬性樣式修改任務。
  • Measure:佈局包裝器執行相關的大小測算任務。
  • Layout:佈局包裝器執行相關的佈局任務。
  • Render:繪製任務包裝器執行相關的繪製任務,執行完成後會標記請求重新整理RSNode繪製
  • SendMessage:請求重新整理介面繪製。

在整個處理流程中,應用側和Render Service側都有可能出現卡頓導致終端使用者觀測到丟幀的可能,我們分別將這兩種情況命名為AppDeadlineMissed和RenderDeadlineMissed。一般而言,前者可能是應用邏輯處理程式碼不夠高效導致的,後者可能是介面結構過於複雜或者GPU負載過大等原因導致的。這兩個故障模型透過Frame模板都可以直觀地看到。相應的故障模型如下面兩幅圖所示。

圖3 應用卡頓導致丟幀的故障模型

圖4 Render Service卡頓導致丟幀的故障模型

丟幀問題思路分析

補充了影像渲染流程的基本知識和丟幀的故障模型後,接下來我們介紹丟幀問題的分析思路,下圖展示瞭解決丟幀問題的簡要流程:

圖5 丟幀問題處理流程

從上圖可以看到處理丟幀問題一般需要以下四個步驟:

  1. 識別卡頓:首先使用AppAnalyzer檢測應用是否存在效能問題,如果檢測存在丟幀問題,然後使用Frame Profiler、SmartPerf Host等工具錄製Trace,檢視應用平均幀率、丟幀率等,同時檢視丟幀發生的位置。
  2. 分析丟幀原因:首先檢視CPU呼叫判斷系統是否存在異常,如果判斷系統異常開發可以透過線上提單的方式進行反饋;如果系統沒有異常,可以繼續分析Trace檢視卡頓幀的詳細資訊。最後檢視函式呼叫棧,檢視是否存在耗時函式。
  3. 選擇最佳化方案:根據步驟2分析的丟幀原因,選擇適合的最佳化方案。
  4. 驗證最佳化效果:最佳化完成後需要重新測試驗證丟幀問題是否得到解決,這裡可以再次透過步驟1來確認最佳化效果

接下來本文將以“HMOS世界”應用的首頁列表為例,介紹如何透過Frame分析、定位、解決卡頓問題的全過程。為了便於演示這個長列表的調優過程,這個列表初始載入了1000條資料。

我們在滑動列表的過程中,隨著時間的推移,我們可以感覺到越來越卡頓,接下來我們將介紹如何分析並解決這個卡頓問題。

圖6 ”HMOS世界”首頁長列表示意圖
HarmonyOS:幀率和丟幀分析實踐

第1步:識別丟幀

使用AppAnalyzer檢測效能問題

首先使用AppAnalyzer工具進行效能問題檢測,AppAnalyzer是DevEco Studio中提供的檢測評分工具,用於測試並評價HarmonyOS應用或元服務的質量,能快速提供評估結果和改進建議,當前支援的測試型別包括相容性、效能、UX測試和最佳實踐等。因為本文主要是介紹丟幀問題的分析,所以下面重點介紹了使用AppAnalyzer對列表滑動響應和滑動過程中的流暢效能檢測,具體使用可參考《應用與服務體檢》

  1. 啟動DevEco Studio,連線裝置,開啟應用。

    1. 單擊選單欄Tools > AppAnalyzer。
    2. 在AppAnalyzer頁面Module選擇框選擇應用/服務工程模組。
    3. 根據應用的類別選擇Category。
    4. 選擇Rules,這裡選擇Benchmark(效能套餐),勾選”Fast Response to In-app Swipes”(應用內滑動操作響應快)、“Smooth In-app Swiping“(應用內滑動過程流暢)和“Smooth In-app Transitions“(應用內轉場操作流暢)。

      HarmonyOS:幀率和丟幀分析實踐

  2. 點選Start啟動檢測,檢測過程中,手機需要保持解鎖亮屏狀態。

    1. 工具會先對應用功能進行自動檢測,開發者不需要進行操作,在自動檢測結束後需要根據提示手動遍歷應用功能。

      HarmonyOS:幀率和丟幀分析實踐

    2. 自動檢測和手動遍歷完成後點選Stop停止測試任務。

      HarmonyOS:幀率和丟幀分析實踐

  3. 獲得檢測結果,下面列舉了檢測透過和未透過的示例。

    • 檢測透過示例,如下圖所示,無異常資訊。

      HarmonyOS:幀率和丟幀分析實踐

    • 檢測未透過示例,例如下圖結果,有多項檢測未透過。

      HarmonyOS:幀率和丟幀分析實踐

      點選左側的選單欄對應的選項,可以檢視異常的具體資訊,這裡以”Fast Detection Of Smoothness During Sliding”選項為例,應用滑動時的卡頓率應該小於5ms/s,但是示例中有多幀超時達到8.49ms,存在進一步最佳化的空間,關於應用滑動流暢的體驗標準可以參考應用/服務體檢規則

      HarmonyOS:幀率和丟幀分析實踐

錄製Frame模板

發現卡頓丟幀問題後建立Frame模板錄製,在錄製期間復現卡頓丟幀場景,具體操作步驟請參見效能問題定位:深度錄製

錄製完成後,在時間軸上拖動滑鼠選定要檢視的時間段,這裡選擇了一個2.5s的時間區段。選中Frame主泳道,檢視下面的Statistics欄,可以發現應用在這個時間段內丟了16幀,丟幀率達到了7%。

HarmonyOS:幀率和丟幀分析實踐

認識卡頓幀

下面是使用Frame Profiler錄製的一段Trace,在時間軸上拖動滑鼠選定要檢視的時間段,這裡我們選擇了一個2.5s的時間區段。選中Frame主泳道,檢視下面的Statistics欄,可以發現應用在這個時間段內丟了16幀,丟幀率達到了7%。

HarmonyOS:幀率和丟幀分析實踐

丟幀問題可能出現在Render Service側,也有可能出現在App側。上圖中的丟幀主要出現在應用幀,針對這種丟幀現象我們繼續分析,放大右側的圖表,選中超時的幀檢視詳細資料,期望時間為8.3ms(當前裝置為120Hz),而實際處理時間為8.9ms。

HarmonyOS:幀率和丟幀分析實踐

說明

在“RS Frame”和“App Frame”標籤的泳道中,正常完成渲染的幀顯示為綠色,出現卡頓的幀顯示為紅色。其中期望結束時間點之前的部分為淺紅色(兩條白色豎線區間),超出期望結束時間的部分為深紅色,異常幀顯示為黃色。

發現問題後,接下來我們來分析這個丟幀問題。導致應用丟幀的原因非常多,可能是應用本身原因,可能是系統原因,也有可能是硬體層原因。不同卡頓原因在Trace中有不同表現,識別需要大量經驗積累。

第2步:分析丟幀原因

丟幀問題分析過程,主要是結合App主程序和Render Service渲染程序Trace資料,先排查系統是否異常,再分析應用本身原因,開發者可以透過以下步驟來定位丟幀問題。

2.1 看執行緒狀態和執行核,看是否被其他程序搶佔資源,排除系統側執行異常。

看執行緒狀態

從下圖可以看到,應用執行緒大部分時間處於Running狀態,無特殊異常。執行在CPU10和CPU11上

圖7 丟幀處應用主執行緒狀態
HarmonyOS:幀率和丟幀分析實踐

看執行頻率

檢視關鍵任務是否跑在了小核,以低頻執行。從CPU Slice和Frequency泳道,如圖8所示,可以看到丟幀處應用執行緒和前面正常幀類似,都主要執行在大核上(該裝置0~3號CPU是小核,4~11號CPU為大核)。滑鼠懸浮在Frequency泳道上,可以看到CPU執行頻率。

圖8 丟幀處應用主執行緒執行核

HarmonyOS:幀率和丟幀分析實踐

透過上面的分析,可以看到應用執行緒正常執行在CPU大核上,且執行頻率正常。到這裡,這個示例可以排除系統異常。

如果應用執行緒執行出現以下問題,開發者可以進行線上提單反饋異常。

  • 執行頻率較低
  • 執行緒在小核上工作
  • 執行緒頻繁在Running和Runnable之間切換
  • 執行緒頻繁在Running和Sleep之間切換
  • 不重要的執行緒佔用了大核
說明

出於兼顧高效能、低功耗的需求,多核工程機常採用異構架構設計,根據CPU頻率,區分大中小核等。

2.2找到Trace中每一幀耗時的部分,大致定位是App側問題還是RS側問題,並結合Trace標籤,初步定位原因。

透過Frame泳道,我們可以快速發現丟幀的位置,並完成初步的定界:

  • App側有紅色出現,需要審視UI執行緒的處理邏輯是否過於複雜或低效,以及是否被其它任務搶佔資源。
  • 如果是Render Service幀處理有紅色出現,需要審視是否是介面佈局過於複雜。可以藉助DevEco Studio內的ArkUI Inspector、HiDumper等工具進一步分析,可以參考佈局巢狀過深示例。

前面示例中的丟幀主要出現在應用側,針對這種丟幀現象我們繼續分析,放大右側的圖表,選中超時的幀(220#幀)檢視詳細資料,期望時間為8.3ms(當前裝置為120Hz),而實際處理時間為8.9ms。

HarmonyOS:幀率和丟幀分析實踐

接下來透過Trace再看看每一幀的具體耗時情況。這裡有一個小技巧,我們可以點選泳道資訊區的收藏按鈕,將應用幀處理的泳道收藏置頂,可以有效防止上下文資訊丟失。點選圖示跳轉到卡頓幀應用側Trace詳情,如下圖所示:

HarmonyOS:幀率和丟幀分析實踐

可以看到這這幾幀的卡頓可能都是BuildLazyItem方法耗時較長導致,可以大致推測,是列表懶載入時,Item繪製時間較長導致的。

同時在ArkUI Component泳道上,可以直觀的看到,自定義元件ArtileView的繪製頻率比較高且比較耗時,對於太過頻繁的繪製元件,可能也是影響應用丟幀的原因。

HarmonyOS:幀率和丟幀分析實踐

需要注意的是在Frame模板中,要想檢視ArkUI Component泳道需要在泳道錄製前進行手動勾選,如下圖所示:

HarmonyOS:幀率和丟幀分析實踐

2.3檢視ArkTS函式呼叫棧資訊,排查應用程式碼。

可以結合Frame Profiler工具,選擇ArkTS Callstack泳道檢視熱點函式,方便地跳回原始碼,定位具體是哪一個自定義元件繪製時間較長。如下圖所示,可以看到自定義元件ArticleCardView的繪製頻繁。下面以220#幀為例子,透過熱點函式可以看到其中initialRenderView 和__lazyForEachItemGenFunction這兩個方法比較耗時,佔比分別達到52.7%和22.9%,其中綠色的”ArkTS”表示雙擊該行可以跳轉到應用原始碼。

HarmonyOS:幀率和丟幀分析實踐

我們以initialRenderView函式的耗時為例進行分析,展開函式,可以看到主要是列表項ListItem的子元件ArticleCardView建立比較耗時。

HarmonyOS:幀率和丟幀分析實踐

展開其中一個元件函式呼叫鏈進行詳細分析,透過檢視函式呼叫,可以猜測是由於使用了@Prop變數,@Prop裝飾的變數會對父元件傳入狀態值進行深複製,如果@Prop裝飾器裝飾的變數為複雜Object、class或其型別陣列時,會增加狀態建立時間以及佔用大量記憶體。雙擊跳轉到原始碼,可以看到自定義元件ActionButtonView中確實使用了@Prop裝飾器變數。

HarmonyOS:幀率和丟幀分析實踐

其它函式耗時的詳細呼叫這裡不一一列舉。

第3步:選擇最佳化方案

選擇最佳化方案需要一些經驗的積累,開發者可以參考一些效能最佳化的最佳實踐,來選擇相應的最佳化方法。

下面我們對丟幀問題進行最佳化,針對前面的一些分析結果,我們可以從兩方面來入手解決卡頓問題:

  • 使用元件複用能力@Reusable來減少元件的頻繁建立。可複用元件從元件樹上移除時,會進入到一個回收快取區。後續建立新元件節點時,會複用快取區中的節點,節約元件重新建立的時間。
  • 簡化元件建立的邏輯,使用更高效的@Builder來構建列表項Item的子元件,替代原有@Component自定義元件的方式。此外使用@Builder以後,就不需要使用@Prop了,從而減少了資料的深複製耗時。

最佳化後示例程式碼如下:

  1. @Component
  2. struct DiscoverView {
  3. // ...
  4. build() {
  5. // 列表
  6. List() {
  7. LazyForEach(this.dataSource, (item: LearningResource) => {
  8. ListItem() {
  9. ArticleCardView() // 省略引數
  10. .reuseId('article')
  11. }
  12. }, (item: LearningResource) => item.id)
  13. }
  14. }
  15. }
  16. // 列表Item
  17. @Reusable
  18. @Component
  19. export struct ArticleCardView {
  20. // ...
  21. aboutToReuse(params: Record<string, Object>): void {
  22. // ...
  23. }
  24. Row() {
  25. ActionButtonBuilder() // 省略引數
  26. ActionButtonBuilder()
  27. ActionButtonBuilder()
  28. }
  29. build() {
  30. // ...
  31. }
  32. }
  33. // 使用@Builder構建子元件
  34. @Builder
  35. function ActionButtonBuilder() { // 省略引數
  36. // ...
  37. }

第4步:驗證最佳化效果

最後,我們可以使用步驟一的方式,來驗證最佳化後的結果。下面用Frame模板錄製後發現,丟幀情況得到明顯改善,列表快速滑動15.9s,丟幀率為0%,丟幀問題得到解決。如下圖:

HarmonyOS:幀率和丟幀分析實踐

如果此時問題仍未解決,可以再重新分析Trace定位問題,然後選擇最佳化方式。

常見丟幀問題

下面列舉了一些常見的丟幀問題以及對應的Trace,以及給出了一些最佳化方案,便於開發者遇到類似的問題,訪問識別和定位。

自定義動畫丟幀問題

在播放動畫或者生成動畫時,畫面產生停滯而導致幀率過低的現象,稱為動畫丟幀。

播放動畫時,系統需要在一個重新整理週期內完成動畫變化曲線的計算,完成元件佈局繪製等操作。建議使用系統提供的動畫介面,只需設定曲線型別、終點位置、時長等資訊,就能夠滿足常用的動畫功能,減少UI主執行緒的負載。

下面使用了自定義動畫,動畫曲線計算過程很容易引起UI執行緒高負載,易導致丟幀。

  1. @Entry
  2. @Component
  3. struct AnimationDemo1 {
  4. @State widthSize: number = 200;
  5. @State heightSize: number = 100;
  6. @State flag: boolean = true;
  7. computeSize() {
  8. let duration = 2000;
  9. let period = 16;
  10. let widthSizeEnd = 0;
  11. let heightSizeEnd = 0;
  12. if (this.flag) {
  13. widthSizeEnd = 100;
  14. heightSizeEnd = 50;
  15. } else {
  16. widthSizeEnd = 200;
  17. heightSizeEnd = 100;
  18. }
  19. let doTimes = duration / period;
  20. let deltaHeight = (heightSizeEnd - this.heightSize) / doTimes;
  21. let deltaWeight = (widthSizeEnd - this.widthSize) / doTimes;
  22. for (let i = 1; i <= doTimes; i++) {
  23. let t = period * (i);
  24. setTimeout(() => {
  25. this.heightSize = this.heightSize + deltaHeight;
  26. this.widthSize = this.widthSize + deltaWeight;
  27. }, t);
  28. }
  29. this.flag = !this.flag;
  30. }
  31. build() {
  32. Column() {
  33. Button('click me')
  34. .onClick(() => {
  35. let delay = 500;
  36. setTimeout(() => {
  37. this.computeSize();
  38. }, delay);
  39. })
  40. .width(this.widthSize)
  41. .height(this.heightSize)
  42. .backgroundColor(0x317aff)
  43. }.width('100%')
  44. .margin({ top: 5 })
  45. }
  46. }

使用Frame Profiler錄製Trace,可以看到動畫幀率只有63fps左右,而當前裝置是支援的裝置重新整理率是120Hz。

HarmonyOS:幀率和丟幀分析實踐

建議開發者透過系統提供的屬性動效API實現上述動效功能,使用屬性動畫或者顯式動畫,下面以屬性動畫實現上面的效果為例:

  1. @Entry
  2. @Component
  3. struct AnimationDemo2 {
  4. @State widthSize: number = 200;
  5. @State heightSize: number = 100;
  6. @State flag: boolean = true;
  7. build() {
  8. Column() {
  9. Button('click me')
  10. .onClick(() => {
  11. if (this.flag) {
  12. this.widthSize = 100;
  13. this.heightSize = 50;
  14. } else {
  15. this.widthSize = 200;
  16. this.heightSize = 100;
  17. }
  18. this.flag = !this.flag;
  19. })
  20. .width(this.widthSize)
  21. .height(this.heightSize)
  22. .backgroundColor(0x317aff)
  23. .animation({
  24. duration: 2000, // 動畫時長
  25. curve: Curve.Linear, // 動畫曲線
  26. delay: 500, // 動畫延遲
  27. iterations: 1, // 播放次數
  28. playMode: PlayMode.Normal // 動畫模式
  29. }) // 對Button元件的寬高屬性進行動畫配置
  30. }
  31. .width('100%')
  32. .margin({ top: 5 })
  33. }
  34. }

使用Frame Profiler錄製最佳化後的Trace,可以看到動畫幀率有了較大的提升,達到了116.9fps。

HarmonyOS:幀率和丟幀分析實踐

佈局巢狀過深

檢視的巢狀層次會影響應用的效能。在螢幕重新整理率為120Hz的裝置上,每8.3ms重新整理一幀,如果檢視的巢狀層次多,可能會導致沒法在8.3ms內完成一次螢幕重新整理,就會造成丟幀卡頓,影響使用者體驗。因此推薦開發者移除多餘的巢狀層次,使用相對佈局 (RelativeContainer),縮短元件重新整理耗時。

例如下面這個示例在列表中載入了2000條資料,同時子元件ChildComponent裡面的佈局巢狀了20層Stack元件。

  1. class MyDataSource implements IDataSource {
  2. private dataArray: string[] = [];
  3. public pushData(data: string): void {
  4. this.dataArray.push(data);
  5. }
  6. public totalCount(): number {
  7. return this.dataArray.length;
  8. }
  9. public getData(index: number): string {
  10. return this.dataArray[index];
  11. }
  12. registerDataChangeListener(listener: DataChangeListener): void {
  13. }
  14. unregisterDataChangeListener(listener: DataChangeListener): void {
  15. }
  16. }
  17. @Entry
  18. @Component
  19. struct StackDemo1 {
  20. // ... 此處省略LazyForEach資料初始化過程
  21. private data: MyDataSource = new MyDataSource();
  22. build() {
  23. List() {
  24. LazyForEach(this.data, (item: string) => {
  25. ListItem() {
  26. ChildComponent({ item: item })
  27. }
  28. .reuseId('child')
  29. }, (item: string) => item)
  30. }.cachedCount(5)
  31. }
  32. }
  33. @Reusable
  34. @Component
  35. struct ChildComponent {
  36. @State item: string = '';
  37. aboutToReuse(params: Record<string, Object>): void {
  38. this.item = params.item as string;
  39. }
  40. build() {
  41. Stack() {
  42. Stack() {
  43. // ... 此處省略Stack巢狀
  44. Text(this.item)
  45. .fontSize(50)
  46. .margin({ left: 10, right: 10 })
  47. }
  48. // ...
  49. }
  50. }
  51. }

使用Frame Profiler進行錄製,這裡我們直接看應用側的Trace資料,具體分析步驟可以看前面的丟幀問題分析思路章節

HarmonyOS:幀率和丟幀分析實踐

結合卡頓幀對應時間段的Trace資料,可以定位到FlushLayoutTask耗時過長,它的作用是重新測量和佈局所有的Item,其中Measure方法耗時比較久,因此卡頓原因可能是佈局處理邏輯過於複雜或低效。

開發者可以使用ArkUI Inspector,在DevEco Studio上檢視應用在真機上的UI顯示效果。利用ArkUI Inspector工具,開發者可以快速定位佈局問題或其他UI相關問題,效果圖如下:

HarmonyOS:幀率和丟幀分析實踐

可以直觀的看到Item的巢狀比較深,接下來我們可以減少不必要的巢狀來嘗試解決丟幀問題,示例程式碼如下:

  1. @Reusable
  2. @Component
  3. struct ChildComponent {
  4. @State item: string = '';
  5. aboutToReuse(params: Record<string, Object>): void {
  6. this.item = params.item as string;
  7. }
  8. build() {
  9. Stack() {
  10. Text(this.item)
  11. .fontSize(50)
  12. .margin({ left: 10, right: 10 })
  13. }
  14. }
  15. }

再次使用Frame Profiler進行錄製,可以看到丟幀問題已得到解決。

HarmonyOS:幀率和丟幀分析實踐

UI冗餘重新整理

自定義元件中的變數被狀態裝飾器(@State,@Prop等)裝飾後成為狀態變數,而狀態變數的改變會引起使用該變數的UI元件渲染重新整理。狀態變數的不合理使用可能會帶來冗餘重新整理等效能問題。開發者可以使用狀態變數元件定位工具(hidumper)獲取狀態管理相關資訊,例如自定義元件擁有的狀態變數、狀態變數的同步物件和關聯元件等,瞭解狀態變數影響UI的範圍,寫出高效能應用程式碼。

下面透過一個點選按鈕更改狀態變數引起元件重新整理的場景示例,結合hidumper工具,介紹狀態變數使用範圍不當,導致UI冗餘重新整理的問題定位。

在以下程式碼中,建立了自定義元件ComponentA、SpecialImage,每個元件都擁有一些狀態變數和UI元件。元件ComponentA中存在Move和Scale兩個按鈕,在按鈕的點選回撥中改變狀態變數的值重新整理相應的元件。

  1. // 常量宣告
  2. const animationDuration: number = 500; // move動畫時長
  3. const opacityChangeValue: number = 0.1; // opacity每次變化的值
  4. const opacityChangeRange: number = 1; // opacity變化的範圍
  5. const translateYChangeValue: number = 180; // translateY每次變化的值
  6. const translateYChangeRange: number = 250; // translateY變化的範圍
  7. const scaleXChangeValue: number = 0.6; // scaleX每次變化的值
  8. const scaleXChangeRange: number = 0.8; // scaleX每次變化的值
  9. // 樣式屬性類
  10. class UIStyle {
  11. public translateX: number = 0;
  12. public translateY: number = 0;
  13. public scaleX: number = 0.3;
  14. public scaleY: number = 0.3;
  15. }
  16. @Component
  17. struct ComponentA {
  18. @Link uiStyle: UIStyle; // uiStyle的屬性被多個元件使用
  19. build() {
  20. Column() {
  21. // 使用狀態變數的元件
  22. SpecialImage({ specialImageUiStyle: this.uiStyle })
  23. Stack() {
  24. Column() {
  25. Image($r('app.media.app_icon'))
  26. .height(78)
  27. .width(78)
  28. .scale({
  29. x: this.uiStyle.scaleX,
  30. y: this.uiStyle.scaleY
  31. })
  32. }
  33. Stack() {
  34. Text('Hello World')
  35. }
  36. }
  37. .translate({
  38. x: this.uiStyle.translateX,
  39. y: this.uiStyle.translateY
  40. })
  41. // 透過按鈕點選回撥修改狀態變數的值,引起相應的元件重新整理
  42. Column() {
  43. Button('Move')
  44. .onClick(() => {
  45. animateTo({ duration: animationDuration }, () => {
  46. this.uiStyle.translateY = (this.uiStyle.translateY + translateYChangeValue) % translateYChangeRange;
  47. })
  48. })
  49. Button('Scale')
  50. .onClick(() => {
  51. this.uiStyle.scaleX = (this.uiStyle.scaleX + scaleXChangeValue) % scaleXChangeRange;
  52. })
  53. }
  54. }
  55. }
  56. }
  57. @Component
  58. struct SpecialImage {
  59. @Link specialImageUiStyle: UIStyle;
  60. private opacityNum: number = 0.5; // 預設透明度
  61. private isRenderSpecialImage(): number {
  62. // Image每次渲染時透明度增加0.1, 在0-1之間迴圈
  63. this.opacityNum = (this.opacityNum + opacityChangeValue) % opacityChangeRange;
  64. return this.opacityNum;
  65. }
  66. build() {
  67. Column() {
  68. Image($r('app.media.app_icon'))
  69. .size({ width: 200, height: 200 })
  70. .scale({
  71. x: this.specialImageUiStyle.scaleX,
  72. y: this.specialImageUiStyle.scaleY
  73. })
  74. .opacity(this.isRenderSpecialImage())
  75. Text("SpecialImage")
  76. }
  77. }
  78. }
  79. @Entry
  80. @Component
  81. struct DFXStateBeforeOptimization {
  82. @State uiStyle: UIStyle = new UIStyle();
  83. build() {
  84. Column() {
  85. ComponentA({
  86. uiStyle: this.uiStyle
  87. })
  88. }
  89. .width('100%')
  90. .height('100%')
  91. }
  92. }

執行上述示例並分別點選按鈕,可以看到點選Move按鈕和Scale按鈕時元件SpecialImage都出現了重新整理,執行效果圖如下。

圖9 修改程式碼前點選Scale按鈕和Move按鈕時執行動圖
HarmonyOS:幀率和丟幀分析實踐

點選Move按鈕的時候SpecialImage元件卻發生了旋轉動畫,這就造成了冗餘重新整理。下面透過這個示例程式碼結合hidumper工具來介紹冗餘重新整理的問題定位。

1. 首先在裝置上開啟應用,進入ComponentA元件所在的頁面。

2. 使用以下命令獲取示例應用的視窗Id。當前執行的示例應用包名為performancelibrary,可以在輸出結果中找到對應視窗名performancelibrary0的WinId,即為應用的視窗Id。或者當應用正處於前臺執行時,Focus window的值就是應用的視窗Id。此處示例應用的視窗Id為11,後面的流程中使用的命令都需要指定視窗Id。

  1. hdc shell "hidumper -s WindowManagerService -a '-a'"
圖10 命令列獲取應用視窗Id執行介面

HarmonyOS:幀率和丟幀分析實踐

3. 基於上一步獲取的視窗Id 11,使用-viewHierarchy命令攜帶-r 引數遞迴列印應用的自定義元件樹。

  1. hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -viewHierarchy -r'"

列印應用的自定義元件樹結果如下:

  1. -----------------ViewPU Hierarchy-----------------
  2. [-viewHierarchy, viewId=4, isRecursive=true]
  3. |--Index[4]
  4. -----------------ViewPU Hierarchy-----------------
  5. [-viewHierarchy, viewId=53, isRecursive=true]
  6. |--DFXStateManagementPage[53]
  7. |--DFXStateManagementHome[55]
  8. -----------------ViewPU Hierarchy-----------------
  9. [-viewHierarchy, viewId=65, isRecursive=true]
  10. |--DFXStateBeforeOptimizationPage[65]
  11. |--DFXStateBeforeOptimization[67]
  12. |--ComponentA[70]
  13. |--SpecialImage[73]

從結果中找到目標元件ComponentA,後面括號中的內容即為元件ComponentA的節點Id 70。

4. 使用命令-stateVariables攜帶引數-viewId(引數的值為ComponentA的節點Id)獲取自定義元件ComponentA中的狀態變數資訊。

  1. hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -stateVariables -viewId=70'"

列印元件ComponentA的狀態變數資訊如下:

  1. --------------ViewPU State Variables--------------
  2. [-stateVariables, viewId=70, isRecursive=false]
  3. |--ComponentA[70]
  4. @Link/@Consume (class SynchedPropertyTwoWayPU) 'uiStyle'[71]
  5. |--Owned by @Component 'ComponentA'[70]
  6. |--Sync peers: {
  7. @Link/@Consume (class SynchedPropertyTwoWayPU) 'specialImageUiStyle'[74] <@Component 'SpecialImage'[73]>
  8. }
  9. |--Dependent components: 2 elmtIds: 'Stack[75]', 'Image[77]'

結果顯示ComponentA擁有@Link/@Consume型別的狀態變數uiStyle。每條狀態變數的詳細資訊都包含狀態變數的所屬元件、同步物件和關聯元件。

5. 以狀態變數uiStyle為例。

① Sync peers表示uiStyle在自定義元件SpecialImage中存在@Link/@Consume型別的狀態變數specialImageUiStyle訂閱資料變化。

② Dependent components表示在ComponentA元件中存在元件Stack[75]和Image[77]使用了狀態變數uiStyle,關聯元件的數量為2。

所以當uiStyle變化時,影響的元件範圍為自定義元件SpecialImage以及系統元件Stack[75]和Image[77]。

圖11 ComponentA的狀態變數資訊
HarmonyOS:幀率和丟幀分析實踐

示例中元件SpecialImage僅使用了uiStyle傳遞到specialImageUiStyle中的屬性scaleX、scaleY。但點選Move按鈕修改uiStyle中的屬性translateY時,引起的uiStyle變化也會導致元件SpecialImage的重新整理。所以,可以將uiStyle中的屬性scaleX、scaleY提取到狀態變數scaleStyle中,屬性translateX和translateY提取到狀態變數translateStyle中,僅傳遞scaleStyle給元件SpecialImage,避免不必要的重新整理。

由於提取後存在Class的巢狀,因此需要使用@Observed/@ObjectLink裝飾器裝飾相應的Class和狀態變數。修改後的部分程式碼如下:

  1. // 常量宣告
  2. const animationDuration: number = 500; // move動畫時長
  3. const opacityChangeValue: number = 0.1; // opacity每次變化的值
  4. const opacityChangeRange: number = 1; // opacity變化的範圍
  5. const translateYChangeValue: number = 180; // translateY每次變化的值
  6. const translateYChangeRange: number = 250; // translateY變化的範圍
  7. const scaleXChangeValue: number = 0.6; // scaleX每次變化的值
  8. const scaleXChangeRange: number = 0.8; // scaleX每次變化的值
  9. // 樣式屬性類,巢狀ScaleStyle, TranslateStyle
  10. @Observed
  11. class UIStyle {
  12. translateStyle: TranslateStyle = new TranslateStyle();
  13. scaleStyle: ScaleStyle = new ScaleStyle();
  14. }
  15. // 縮放屬性類
  16. @Observed
  17. class ScaleStyle {
  18. public scaleX: number = 0.3;
  19. public scaleY: number = 0.3;
  20. }
  21. // 位移屬性類
  22. @Observed
  23. class TranslateStyle {
  24. public translateX: number = 0;
  25. public translateY: number = 0;
  26. }
  27. @Component
  28. struct ComponentA {
  29. @ObjectLink scaleStyle: ScaleStyle;
  30. @ObjectLink translateStyle: TranslateStyle;
  31. build() {
  32. Column() {
  33. SpecialImage({
  34. specialImageScaleStyle: this.scaleStyle
  35. })
  36. // 其他UI元件
  37. Stack() {
  38. Column() {
  39. Image($r('app.media.app_icon'))
  40. .scale({
  41. x: this.scaleStyle.scaleX,
  42. y: this.scaleStyle.scaleY
  43. })
  44. }
  45. Stack() {
  46. Text('Hello World')
  47. }
  48. }
  49. .translate({
  50. x: this.translateStyle.translateX,
  51. y: this.translateStyle.translateY
  52. })
  53. // 透過按鈕點選回撥修改狀態變數的值,引起相應的元件重新整理
  54. Column() {
  55. Button('Move')
  56. .onClick(() => {
  57. animateTo({ duration: animationDuration }, () => {
  58. this.translateStyle.translateY =
  59. (this.translateStyle.translateY + translateYChangeValue) % translateYChangeRange;
  60. })
  61. })
  62. Button('Scale')
  63. .onClick(() => {
  64. this.scaleStyle.scaleX = (this.scaleStyle.scaleX + scaleXChangeValue) % scaleXChangeRange;
  65. })
  66. }
  67. }
  68. }
  69. }
  70. @Component
  71. struct SpecialImage {
  72. @Link specialImageScaleStyle: ScaleStyle;
  73. private opacityNum: number = 0.5; // 預設透明度
  74. // isRenderSpecialImage函式
  75. private isRenderSpecialImage(): number {
  76. // Image每次渲染時透明度增加0.1, 在0-1之間迴圈
  77. this.opacityNum = (this.opacityNum + opacityChangeValue) % opacityChangeRange;
  78. return this.opacityNum;
  79. }
  80. build() {
  81. Column() {
  82. Image($r('app.media.app_icon'))
  83. .scale({
  84. x: this.specialImageScaleStyle.scaleX,
  85. y: this.specialImageScaleStyle.scaleY
  86. })
  87. .opacity(this.isRenderSpecialImage())
  88. Text("SpecialImage")
  89. }
  90. }
  91. }
  92. @Entry
  93. @Component
  94. struct DFXStateAfterOptimization {
  95. @State uiStyle: UIStyle = new UIStyle();
  96. build() {
  97. Stack() {
  98. ComponentA({
  99. scaleStyle: this.uiStyle.scaleStyle,
  100. translateStyle: this.uiStyle.translateStyle,
  101. })
  102. }
  103. }
  104. }

修改後的示例執行效果圖如下,只有點選Scale按鈕時SpecialImage產生重新整理現象,點選Move按鈕時SpecialImage不會重新整理。

圖12 修改程式碼後點選Scale按鈕和Move按鈕時執行動圖
HarmonyOS:幀率和丟幀分析實踐

可以使用上文步驟再次獲取ComponentA元件的狀態變數資訊如下,可以看到ComponentA中狀態變數scaleStyle影響元件SpecialImage[74]和Image[78],狀態變數translateStyle影響元件Stack[76],translateStyle的變化不會再導致SpecialImage的重新整理。

  1. --------------ViewPU State Variables--------------
  2. [-stateVariables, viewId=70, isRecursive=false]
  3. |--ComponentA[70]
  4. @ObjectLink (class SynchedPropertyNestedObjectPU) 'scaleStyle'[71]
  5. |--Owned by @Component 'ComponentA'[70]
  6. |--Sync peers: {
  7. @Link/@Consume (class SynchedPropertyTwoWayPU) 'specialImageScaleStyle'[75] <@Component 'SpecialImage'[74]>
  8. }
  9. |--Dependent components: 1 elmtIds: 'Image[78]'
  10. @ObjectLink (class SynchedPropertyNestedObjectPU) 'translateStyle'[72]
  11. |--Owned by @Component 'ComponentA'[70]
  12. |--Sync peers: none
  13. |--Dependent components: 1 elmtIds: 'Stack[76]'

主執行緒中執行冗餘和耗時操作

應避免在主執行緒中執行冗餘與易耗時操作,否則可能會阻塞UI渲染,引發介面卡頓或掉幀現象,特別是在高頻回撥中執行耗時操作。具體可以參考:主執行緒耗時操作最佳化指導

丟幀問題最佳化建議

前面我們簡單介紹了圖形渲染的流程,瞭解到了影像渲染的兩個關鍵步驟:首先由應用側響應消費者的螢幕點選等輸入事件並且生成當前的介面描述資料結構,然後交給Render Service進行繪製。在這兩個步驟中分別會出現AppDeadlineMissed和RenderDeadlineMissed卡頓。前者可能是應用邏輯處理程式碼不夠高效導致的,可以結合Trace資料和熱點函式進行分析;後者可能是介面結構過於複雜或者GPU負載過大等原因導致的,可以使用佈局檢查器ArkUI Inspector工具和HiDumper命令列工具輔助分析定位。

針對一些常見的丟幀問題,下面列舉了一些最佳化建議:

  • 儘量減少佈局的巢狀層數,合理使用佈局,使用相對佈局 (RelativeContainer)來減少層級。
  • 使用元件複用減少元件的重複建立與渲染。
  • 合理管理狀態變數合理管理狀態變數,精準控制元件的更新範圍,避免冗餘重新整理,具體可以參考狀態管理最佳實踐
  • 使用LazyForEach載入長列表,長列表的最佳化可以參考長列表載入效能最佳化
  • 使用系統提供的動畫介面,減少動畫丟幀。
  • 最佳化主執行緒中冗餘和耗時操作,具體可以參考主執行緒耗時操作最佳化指導

相關文章