扣丁書屋

iOS 性能檢測新方式?——AnimationHitches

AnimationHitches 的運行原理

背景

在 Xcode12 中,Instrument 新增 AnimationHitches 檢測類型用以檢測卡頓,并去除 CoreAnimation 檢測方式。在支持 PromotionDisplay 的設備上幀率可調整至 120 幀,并且會根據當前用戶手勢和設備狀態進行動態調整。此時再繼續使用幀率來判斷性能的好壞及流暢度將會是一個錯誤的選擇。所以 AnimationHitches 主要用于代替幀率檢測,并且提出 卡頓時間比(Hitch Time Ratio) 的概念用于替代 FPS。由于目前關于 Hitch 相關的資料很少,而在 iPhone13Pro 之前 iPhone 屏幕最高刷新頻率仍為 60 HZ,所以很多同學都還未關注到該能力。所以本篇將主要介紹 Hitch(卡頓) 的概念、RenderLoop(渲染循環) 的整體流程,卡頓類型及如何避免卡頓。

什么是卡頓?

? 概念

任何時候屏幕上出現晚于預計的幀都屬于卡頓。

? 實例

例如 滾動動畫(Scroll)、點擊動畫(Animation)、轉場動畫(Transition),這些流暢的動畫構建了一種用戶和屏幕內容的視覺連接感,而如果動畫卡頓會導致動畫畫面跳躍,打破這種連接感,用戶體驗會變得很差。

一個常見的例子,當用戶在操作一個滾動視圖上下滾動時,發生了卡頓,這是因為第四幀的延遲導致了第三幀占用了兩幀的時間,給用戶看到的就是卡頓掉幀的現象。

RenderLoop

? 概念

RenderLoop 是一個連續的過程,通過用戶手勢等將事件傳給 App,接著 App 向操作系統傳遞事件并最終響應事件,再將響應傳遞給用戶的過程。

RenderLoop 的時間隨著設備刷新頻率,在 iPhone13 Pro(Max) 以下的 iPhone 設備最大均為 60 幀,而 iPhone13 Pro(Max) 及 iPadPro 則最高支持 120 幀,也就是最短僅需每 8.33 毫秒就可以顯示一個新幀。

? 幀準備階段

在準備每一幀的過程中,可以總體分為三個階段。App、RenderServer 和 Display。其中 App 中主要進行一些用戶事件的處理,而 RenderServer 會進行真正的用戶界面繪制,這兩個階段都需要在下一個 VSNYC 到來前完成。最終到 Display 階段會將緩沖的幀展示出來。對這一幀進行雙幀處理我們把這稱之為雙緩沖,由于顯示器是逐行掃描進行畫面顯示,雙緩沖和垂直同步機制避免了屏幕撕裂的現象。

當然,系統也提供備用的三緩沖機制,為 RenderServer 提供額外的一幀進行渲染,該機制通常情況下不會開啟。

? 階段細節

整個渲染循環可細分為 5 個階段,其中在我們 App 中的為 Event,Commit 階段,而 Commit 階段可進一步細分為 Layout、Display、Prepare 和 Commit。

  1. 在事件階段通過 touch,timer 等事件決定用戶界面是否需要改變;
  2. 而在 Commit 階段 App 會向渲染服務器 RenderServer 提交渲染命令;
  3. RenderServer 中的 Prepare,Execute 階段,在 Prepare 階段會為 GPU 的繪制做好準備,而在 Execute 階段會由 GPU 將用戶界面的圖像繪制出來;
  4. 最后的 Display 階段會將緩沖幀交換到屏幕上顯示。

以一個帶有陰影的渲染圖形為例,觀察下 RenderLoop 中每一幀所做的工作

- App

Event

在該階段表示 App 接收到了事件,比如 touch 事件、網絡請求回調、鍵盤和 Timer 。一個 App 可以通過改變其層級結構或是用任何其他方式響應這些事件。

例如 App 能改變圖層的背景顏色,甚至能改變圖層的大小和位置。當 App 更新了圖層的限制范圍時, CoreAnimation 會同時會調用 setNeedsLayout。它能夠分辨哪些圖層必須要重新計算布局,系統會合并這些需要布局的請求并在 Commit 階段按順序執行,用以減少重復工作。

Commit

在一次事務的提交中共涉及四個不同的階段:布局階段、顯示階段、準備階段和最后的提交階段。

布局階段

在布局階段, layoutSubviews 會被所有需要布局的 View 調用。比如布局視圖(frame、bounds、transform),增加或移除視圖,亦或是直接調用 setNeedsLayout。注意這些布局操作并非立即執行,系統會合并這些布局請求,在 Runloop 休眠前統一執行這些操作。

顯示階段

在顯示階段,drawRect 會被每個需要被更新的 View 調用。比如 UILabel、UIImageView 或者只是任何重寫 drawRect 方法的類。他們必須調用 setNeedsDisplay 用以支持 View 的更新。在繪制時每個自定義的繪圖圖層都會接收到帶紋理的 CoreGraphics 的背景。他們將利用 CoreAnimation 進行繪制,這些圖層就變成了圖片。所以如果沒有必要則不要重寫 drawRect 方法,其不僅會額外開辟一塊內存用以存儲 bitmap,還會在 CPU 上進行繪制,增加了整體主線程時間占用,當自定義 drawRect 視圖較多時,對整體的內存壓力也比較大。

準備階段

在 Prepare 階段還沒有解碼的圖像將會在這一步進行解碼,也就是我們需要優化的常見的圖片主線程解碼操作。

對于每個被解碼的圖像, App 可能會持續存在大量的內存分配。這種內存分配與輸入圖像的大小成正比,而與 FrameBuffer 中實際渲染的圖像視圖的大小沒有必然聯系。當 App 占用越來越多的內存時,操作系統將會開始壓縮物理內存(physical memory)。整個過程都需要 CPU 的參與,所以除了我們自己的 App 對 CPU 的使用外,還可能會增加無法控制的全局 CPU 使用率。最終,我們的 App 可能會消耗更多的物理內存,以至于操作系統需要啟動終止進程,它將從低優先級的后臺進程開始。如果我們的 App 對內存的消耗了達到了特定數量,可能會被終止,這也就是為什么經常會因為大圖的原因產生 OOM。

若某個圖像的顏色格式 GPU 無法直接使用,也會在這一步進行格式轉換。這就要求對該圖像進行 copy 操作,而不是直接使用指針,這樣會耗時更長及占用更多的內存。

提交階段

在提交階段中,視圖樹將會被遞歸打包并發送到 RenderServer 中,所以當視圖層級較為復雜時,這個過程耗費的時間也會更長一些,所以需要盡量減輕視圖層級結構。

- RenderServer

RenderServer 負責將我們的圖層樹轉換為真正可顯示的圖像。RenderServer 有兩個階段:Prepare 和 Execute 。在 Prepare 階段我們的圖層樹被編譯成一系列簡單的指令,供 GPU 執行,幀動畫也在此處進行處理。在渲染執行階段 GPU 將 App 的圖層繪制成最終圖像。

下面來一個渲染實例。在下面這個實例中,圓形和長條周圍都有陰影。

Prepare

在準備階段, RenderServer 會廣度優先遍歷 App 的圖層樹,準備一個線性管線,這樣 GPU 就能按照順序執行命令進行繪制。從根圖層開始逐層遍歷,最終才有了 GPU 可以在下一個執行階段執行的整個管線。

Execute

執行階段主要是由 GPU 根據前面 prepare 階段準備好的圖層樹進行頂點著色、形狀裝配、幾何著色、光柵化、片段著色與圖層混合。一旦 GPU 執行完會將渲染好的圖像放入幀緩存區中等待下一個 VSYNC 的到來并交換到屏幕上進行顯示。

在該例中, GPU 的工作就是利用該管線將每一步都繪制成紋理并最終合成,最終在顯示階段會在屏幕上顯示該紋理。

從第一個藍色的圖層開始,它在指定的邊界內繪制顏色。然后深藍色被繪制在其邊界內,但是當前圓形和矩形中都有陰影,所以現在 GPU 必須先去繪制陰影。而陰影的形狀由還未繪制的兩層定義,所以需要先繪制圓形和矩形,為了避免這兩圖層被陰影遮擋,所以需要切換到不同的紋理先繪制陰影,對于這種情況我們稱之為“離屏渲染”。在這里需要額外開辟一塊內存用以繪制圓形和矩形,然后將該圖層變為黑色并且模糊來實現陰影的效果。

然后 GPU 可以將陰影的離屏渲染紋理復制到最終的紋理中。陰影圖層就完成了,下一步是再次繪制圓形和矩形??梢宰⒁獾降氖?,這里不僅開辟了一塊額外的存儲空間用以渲染陰影,圓形和矩形也被渲染了兩次,對性能損害極大。

而最后的文本是在 CPU 上完成繪制的, GPU 會通過復制 CPU 繪制的文本圖像來完成。完成上述流程后,幀已經準備好進行顯示了。

需要注意在這個過程中我們不得不用離屏渲染來渲染陰影,導致渲染需要更長的時間。

離屏渲染

離屏渲染通道指的是 GPU 必須先在其它地方開辟一塊內存進行圖層渲染,然后再將其復制回來。就陰影而言,它必須繪制圖層,以確定最終形狀。

偶爾的離屏渲染對性能影響并不大,但離屏通道可能會積少成多,導致渲染出現卡頓。因此需要在 App 中監控并盡量避免。主要有四種主要類型的離屏通道可以優化:陰影、蒙版、圓角和毛玻璃。

Shadow:比如在實例中,如果不先繪制附加到圖形上面的陰影,GPU 就沒有足夠的信息來繪制陰影。

Mask:當圖層或圖層樹需要被遮蔽時,GPU 需要渲染被遮蔽的子樹,它也需要避免覆蓋被遮蔽形狀外的像素。因此它只會把最終需要顯示的像素復制回最終紋理,由于最終結果可能由多層渲染結果疊加,所以必須要利用額外的內存空間對中間的渲染結果進行緩存,因此系統會默認來觸發離屏渲染,這種離屏渲染可能會導致渲染了許多用戶永遠不會看到的像素。

CornerRadius:由于 GPU 繪制時會先從根節點開始繪制,所以如果根節點上設置了圓角,并且設置了 maskToBounds 裁剪屬性,那就會需要一個額外的離屏渲染 buffer 用以緩存中間的裁剪結果,并最終將圓角內的像素復制回來,組透明度等屬性都可能會觸發離屏渲染。

iOS8 中開始支持 UIBlurEffectView 控件用以支持模糊化和鮮亮化,要應用這些效果,GPU 必須用離屏通道將內容復制到另一個紋理中,然后對其進行模糊、縮放疊加等操作并將最終結果復制回來。

- Display

Display 的過程實際上就是將幀緩存區中的內容交換到顯示器上進行最終顯示,這一過程我們參與不多。

? 總結

為了達到目標幀速率并且保持低輸入延遲,RenderLoop 的整個過程實際上是在每一幀中并行進行的,這樣管線就成了并行的。在系統渲染前一幀的同時 CPU 可以準備一個新幀,所以每幀的截止期都很重要。

卡頓類型

上面已經描述了 RenderLoop 的整個工作流程,實際上主要是在 App 和 RenderServer 中進行,所以總共有兩種主要類型的卡頓:提交卡頓(發生在 App 中),渲染卡頓(發生在 RenderServer 中)。

? 提交卡頓

  • 概念

提交卡頓指的是 App 花費過長時間來處理或提交事件。

在提交中用了太長的時間而錯過了截止期,所以在下一個 VSYNC 中 RenderServer 沒有事情可以處理,必須等待下一個 VSYNC 到來后才能開始渲染?,F在已經把幀傳送的時間推遲了一幀,以毫秒計時這將是 iPhone(60hz) 或 iPad 上的 16.67 毫秒。這個延遲時間就被稱為“卡頓時間(Hitch Time)”。如果提交工作花了更長的時間,比如通過了下一個 VSYNC 的起始時間,那么這一幀就晚了兩幀或者說是 33.34 毫秒,在這 33.34 毫秒中用戶都無法得到順暢的滾動。

  • 如何避免卡頓

保持視圖的輕量

  1. 為了保持視圖的輕量盡可能地利用CALayer 上 GPU 加速的可用屬性,如非必要需要避免使用 CPU 進行自定義繪制。
  2. 若非必要情況下不要重寫 drawRect 屬性,因為其會開辟額外的內存空間進行 CPU 繪制,并且在 CPU 上繪制會耗費更多的時間主線程。針對于文本、圖片等原本就在 CPU 上進行繪制的系統控件,我們可以嘗試使用其更底層線程安全的 CoreGraphics 能力,比如 TextKit、CoreText 等搭配多線程異步繪制減輕主線程壓力。
  3. 盡量復用視圖而不是頻繁的添加或移除視圖。
  4. 如果要把某一視圖從某一動畫中移除,盡量使用 hidden 屬性。
  5. 對于 Prepare 階段,當我們的 UIImage 容器視圖的大小小于圖片本身時,我們通??梢允褂?下采樣技術(downsampling) 來進行縮略圖的創建以節省部分內存空間。

避免復雜布局

  1. 減少代價過高且重復的布局,在需要更新布局時盡量只使用 setNeedsLayout。layoutIfNeeded 會消耗當前事務的生命周期也會造成卡頓,大多數時候你可以等到下一次 Runloop 執行時再更新你的布局。
  2. 嘗試使用最少的約束來完成布局。
  3. 視圖應該只能使自己或自己的子視圖無效,而不能使其同級視圖或父視圖無效,避免遞歸布局。
  4. 避免非必要的視圖層級創建,復雜的視圖層級會增加提交階段的整體耗時

合理多線程能力

  1. 學會利用 GCD 的多線程能力,充分利用 CPU 多核優勢,提前在子線程進行布局等 UI 無關操作,避免主線程掛起(hang)。
  2. 避免主線程 IO 等磁盤相關操作。
  3. 而針對于常見的主線程解碼操作,在 iOS15 之前,我們通常都是自己封裝或是利用最常見的第三方庫 SDWebImage 替我們在子線程進行解碼操作。而在 iOS15 中,Apple 終于提供了官方的解決方案以解決該問題:UIImage 的 prepareThumbnailOfSize:completionHandler: 等新接口。
  4. 針對于必須在 CPU 上進行繪制的組件,嘗試結合多線程使用異步繪制能力減輕主線程壓力。

? 渲染 卡頓

  • 概念

渲染卡頓會在渲染服務器無法按時準備或者執行圖層樹時出現。這里顯然 Execute 的時長超過了 VSYNC 的界限,因此這一幀無法按時準備好。綠色的畫面比預期的晚了一幀于是有了 16 毫秒的卡頓

  • 如何避免卡頓

準備階段我們影響較少,通常主要影響在于執行階段的離屏渲染。對于陰影來說,在設置陰影時,確保設置 shadowPath 以減少大量離屏通道。在圓化矩形時,使用 cornerRadius 和 cornerCurve 屬性避免用蒙版或角內容來構成圓角矩形。

優化整個 App 的 Mask。使用 masksToBounds 遮蔽為矩形圓角矩形或橢圓形的性能比自定義蒙版圖層好得多。重要的是用 Instruments 來對 App 進行分析并檢查圖層樹以獲得重要的技巧從而降低整體離屏計數。

合理并謹慎的使用 shouldRasterize 屬性,它會對一塊圖層進行光柵化操作并進行緩存。若針對于需要頻繁刷新的圖層使用該屬性反而對性能有著負面影響。

盡量使用非透明的圖層以盡量減少圖層混合。

檢測卡頓

當只著眼于一個卡頓或幾個卡頓時,卡頓時間是很有用的,但在像在滾動、動畫或者是轉場等時長更長的事件時會變得很難處理。除非每次滾動或者動畫用的都是完全相同的時間,這樣就會有相同的幀數。并且 iOS 設備并不總是更新屏幕,如果沒有事務發送到 RenderServer 上,新的一幀就不會被提交。通過測試來比較卡頓時間就更難了。所以 Apple 提供了一種叫 “卡頓時間比(Hitch Time Ratio)” 的指標來衡量一段時間內的卡頓情況。

卡頓時間比就是一個區間內的總卡頓時間除以它的持續時間。因為它標準化為總時間,我們就能在不同的實踐中交叉比較。它是由每秒中的卡頓毫秒時間來測定的。所以代表著設備在每秒內出現卡頓的毫秒數。

一個實例如下,在一臺 iPhone(60HZ) 上這是半秒的工作量,每一幀都在 VSYNC 到來前準備好了,所以用戶看不到卡頓,卡頓時間為 0,卡頓時間比也為 0。

第二個例子如下,在該例中有時是在提交階段的卡頓,有時是在 RenderServer 中造成了卡頓。將卡頓時間加起來結果就是 100.02 ms 半秒。我們就得到了每秒 200.04 ms 的卡頓時間比。

以下是蘋果建議的卡頓時間比目標。目標是 5 ms/s 以下的卡頓,是最不易被用戶察覺到的。5~10 ms/s 的卡頓用戶就會察覺到一些中斷。超過 10 ms/s 就會嚴重影響用戶體驗。

總結

本篇主要討論了 RenderLoop 以及新的一幀展現給用戶的整個流程,并且著眼于什么是卡頓,以及它的兩種類型:提交卡頓以及渲染卡頓。并最終定義了卡頓時間比用以測量當前 App 的卡頓程度和性能。相信大家對整個渲染循環和卡頓類型有了更清晰的認識,在日常編碼中也可以盡量避免這些問題。

本篇主要介紹了一些原理相關的概念,那么具體的卡頓應該如何測量?下一篇將會通過實踐結合 Instrument 的 AnimationHitches 能力分析 DXSDK 作為卡片層面在日常信息流的使用過程中在性能方面存在的一些問題,以及 DXSDK 上半年做的一些性能優化改進。


https://mp.weixin.qq.com/s/SQgxvBztLQFZ6QV43iN2Vg

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 8月以前  |  18061次閱讀
快速配置 Sign In with Apple 2年以前  |  5484次閱讀
APP適配iOS11 3年以前  |  4436次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4262次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4184次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  3916次閱讀
開篇 關于iOS越獄開發 3年以前  |  3794次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3719次閱讀
給數組NSMutableArray排序 3年以前  |  3642次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3346次閱讀
UITableViewCell高亮效果實現 3年以前  |  3344次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3242次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3083次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2857次閱讀
使用最高權限操作iPhone手機 3年以前  |  2828次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>