引言
從Xcode12開始沐扳,Instrument更新了UI,新增了一個模板 Animation Hitches 用來分析用戶的 app 中的卡頓俯树,并去除了 Core Animation 檢測方式。在 iPhone13Pro 之前 iPhone 屏幕最高刷新頻率仍為 60 HZ
才避,而在支持 PromotionDisplay 的設(shè)備上幀率可調(diào)整至 120 幀
,并且會根據(jù)當(dāng)前用戶手勢和設(shè)備狀態(tài)進行動態(tài)調(diào)整。此時再繼續(xù)使用幀率來判斷性能的好壞及流暢度將會是一個錯誤的選擇。所以 Animation Hitches 主要用于代替幀率檢測
烘嘱,并且提出 卡頓時間比
(Hitch Time Ratio) 的概念用于替代 FPS。
在 Hitch 提出之前蝗蛙,都是借助FPS
(Frames Per Second 幀率)蝇庭,即每秒繪制幀的數(shù)量來衡量頁面是否卡頓
- 滑動屏幕時,幀率理想值為60FPS
- 滑動屏幕時捡硅,幀率越高表示性能越好哮内;幀率過低意味著屏幕可能出現(xiàn)卡頓,存在隨機丟幀的可能壮韭。
- 其中幀率
>=57為優(yōu)秀; >=55為良好; >=50為可接受;
蘋果于 20 年的 Session 中提出了 Hitch
的概念北发,用以衡量滑動時的卡頓情況。Hitch 指的是 卡頓時間
(一幀延后出現(xiàn)的時間喷屋,ms)/ 總時間
(一般是 1 秒)琳拨,簡單來說 卡頓時間比
就是一個區(qū)間內(nèi)的總卡頓時間除以它的持續(xù)時間。
-
低于 5 ms/s
說明比較優(yōu)秀
屯曹,是最不易被用戶察覺到的 - 介于 5ms/s 和 10ms/s 之間狱庇,說明發(fā)生了中等卡頓,用戶會察覺到一些中斷恶耽,但并不嚴重
-
高于 10 ms/s
說明發(fā)生了較嚴重的卡頓
密任,已經(jīng)影響了用戶體驗。
Hitch
卡頓
- 概念:任何時候屏幕上出現(xiàn)晚于預(yù)計的幀都屬于卡頓
-
簡單來說就是掉幀了偷俭,即沒有在規(guī)定時間內(nèi)渲染好一幀畫面浪讳,這就是卡頓一次
卡頓
如上圖所示,當(dāng)手指在屏幕上滑動時涌萤,滾動視圖會隨著手勢做出響應(yīng)淹遵,如果一幀一幀來看,就是每一幀都對應(yīng)手指位置的變化负溪。當(dāng)卡頓發(fā)生時合呐,某一幀沒有跟隨手指變化,導(dǎo)致到下一幀時笙以,產(chǎn)生跳躍淌实,打破了用戶和屏幕內(nèi)容的視覺連接感。圖中卡頓產(chǎn)生的原因就是第三幀重復(fù)了猖腕,主要是因為第四幀的延遲導(dǎo)致了第三幀占用了兩幀的時間拆祈,給用戶看到的就是卡頓掉幀的現(xiàn)象。
RenderLoop
-
概念:是一個連續(xù)的過程倘感,通過用戶手勢等將事件傳給 App放坏,接著 App 向操作系統(tǒng)傳遞事件并最終響應(yīng)事件,再將響應(yīng)傳遞給用戶的過程
RenderLoop
RenderLoop 的時間隨著設(shè)備刷新頻率老玛,在 iPhone13 Pro(Max) 以下的 iPhone 設(shè)備最大均為 60 幀淤年,而 iPhone13 Pro(Max) 及 iPadPro 則最高支持 120 幀钧敞,也就是最短僅需每 8.33 毫秒就可以顯示一個新幀。
RenderLoop刷新頻率
視圖渲染流程
在每一幀顯示的過程中麸粮,大概可以分為3個階段溉苛,如下所示
-
App
:進行用戶事件處理 -
Render server
:負責(zé)將圖層樹轉(zhuǎn)換為可顯示的圖像(即用戶界面繪制) -
On the display
:顯示緩存的幀
注
1、App
和Render server
階段需要在下一個 VSYNC 到來之前完成
2弄诲、這里運用了雙緩存區(qū) + 垂直同步機制
愚战,主要是用于解決 屏幕撕裂現(xiàn)象
3、整個渲染階段可以分為5個階段:
渲染階段細分
- 階段 1 + 2:App(Event + Commit)
- 階段 3 + 4:Render server(Prepare + Excute)
- 階段 5:Display
4齐遵、整體渲染流程如下
- 在
Event(事件階段)
通過 touch寂玲、timer 等事件決定用戶界面是否需要改變- 在
Commit(提交階段)
,App會向 Render server(渲染服務(wù)器)提交渲染命令- 在
Prepare (準備階段)
會為 GPU 的繪制組好準備- 在
Excute(執(zhí)行階段)
會由 GPU 將用戶界面的圖像繪制出來- 在
Display(顯示階段)
會將緩沖區(qū)的幀交換到屏幕上顯示
下面以一個帶有陰影的渲染圖形為例梗摇,通過觀察 RenderLoop 中每一幀所做的工作拓哟,來分別介紹不同階段
App 階段
App 階段包含 2 個階段,分別是 Event 伶授、Commit彰檬。其中Commit 又分為 4 個子階段,分別是
- Layout
- Display
- Prepare
- Commit
階段 1:Event 事件階段
-
事件階段 表示 App 接收到了事件(例如:touch谎砾、網(wǎng)絡(luò)請求回調(diào)逢倍、鍵盤、timer等)景图。
事件階段-Event -
在 App 中可以通過改變其層級結(jié)構(gòu)较雕,或者使用其他方式響應(yīng)事件。例如圖層顏色/大小/位置變化挚币。當(dāng) App 更新了圖層時亮蒋, CoreAnimation 會同時調(diào)用
setNeedsLayout
方法,該方法能夠找出哪些圖層需要重新計算布局妆毕,且系統(tǒng)會合并這些重新計算的請求慎玖,并在 Commit 階段按需執(zhí)行,以此來減少重復(fù)工作
層級變化
階段 2: Commit 提交階段
提交階段還可以細分笛粘,主要分為4個子階段
-
layout (布局階段)
:layoutSubviews 會被所有需要布局的 View 調(diào)用 -
display(顯示階段)
:drawRect 會被每個需要被更新的 View 調(diào)用 -
prpare(準備階段)
:未解碼圖像進一步解碼(即需要優(yōu)化的常見的圖片主線程解碼操作) -
commit(提交階段)
:視圖樹將會被遞歸打包并發(fā)送到 RenderServer 中
Layout - 布局階段
在布局階段趁怔,layoutSubviews
會被所有需要布局的 View 調(diào)用。例如視圖布局(frame薪前、bounds润努、tranform等)、增加/移除視圖示括、直接調(diào)用 setNeedsLayout/layoutIfNeesed 等
注:這些操作并不是立即執(zhí)行铺浇,系統(tǒng)會合并布局請求,在 Runloop 休眠前進行統(tǒng)一處理
Display - 顯示階段
- 在顯示階段垛膝,
drawRect
會被每個需要被更新的 View 調(diào)用鳍侣,例如 UILabel 等空間類或者 任何重寫 drawRect 方法 的類丁稀,必須調(diào)用 調(diào)用setNeedsDisplay
用以支持 View 的更新。 - 非必須不要重寫 drawRect 方法倚聚,因為在繪制時线衫,每個自定義圖層都會接收到帶紋理的 CoreGraphics 的背景,會利用 CoreAnimation 進行繪制秉沼,這些圖層就變成了圖片
- 導(dǎo)致內(nèi)存額外的開銷以及bitmap的存儲桶雀,對整體內(nèi)存壓力較大
- 由于是在 CPU上進行繪制矿酵,還增加了整體主線程的占用
Prepare - 準備階段
在準備階段唬复,主要是將還未解碼的圖像進行進一步解碼,這也是我們需要優(yōu)化的點(即優(yōu)化圖片主線程解碼操作
)全肮。
因為對于每個解碼的圖像敞咧,App可能會持續(xù)存在大量的內(nèi)存分配(與圖像大小成正比),當(dāng)App占用內(nèi)存越來越多時辜腺,操作系統(tǒng)就會開始壓縮物理內(nèi)存(physical memory)休建,這整個過程都需要CPU參與,所以除了App會使用CPU评疗,還增加了無法控制的全局 CPU 使用率测砂,導(dǎo)致App消耗更多的物理內(nèi)存,此時操作系統(tǒng)會終止低優(yōu)先級的后臺進程百匆,從而釋放更多的物理內(nèi)存砌些。但設(shè)備的物理內(nèi)存始終是有限的,當(dāng)App對內(nèi)存的消耗達到了臨界值時加匈,該App進程就會被操作系統(tǒng)終止存璃,這就是常說的大圖導(dǎo)致的OOM
。
若某個圖像的顏色格式 GPU 無法直接使用雕拼,也會在這一步進行格式轉(zhuǎn)換纵东。這就要求對該圖像進行 copy 操作,而不是直接使用指針啥寇,這樣會耗時更長及占用更多的內(nèi)存偎球。
Commit - 提交階段
在提交階段,視圖樹會被遞歸打包辑甜,并發(fā)送到 Render Server中甜橱,所以當(dāng)視圖圖層較復(fù)雜時,這個過程的耗時也會相對較長栈戳,這也是我們經(jīng)常提及的優(yōu)化點(即盡量減輕視圖層級結(jié)構(gòu)岂傲,不要跟套娃似的,無窮無盡
)子檀。
Render Server 階段
Render Server(渲染服務(wù)器)主要負責(zé)將圖層樹轉(zhuǎn)換為真正顯示的圖像镊掖,分為兩個子階段
-
prepare
:圖層樹被編譯成一系列簡單的指令乃戈,供 GPU 執(zhí)行,幀動畫也在此處進行處理 -
excute
:GPU 將 App 的圖層繪制成最終圖像
階段 3:Prepare 準備階段
-
在 準備階段亩进,RenderServer 會廣度優(yōu)先遍歷 App 的圖層樹症虑,準備一個線性管線,這樣 GPU 就能按照順序執(zhí)行命令進行繪制归薛。
遍歷圖層樹 -
從根圖層開始逐層遍歷谍憔,最終才有了 GPU 可以在下一個執(zhí)行階段執(zhí)行的整個管線。
逐層遍歷
階段 4:Excute 執(zhí)行階段
在執(zhí)行階段主籍,主要是由 GPU 根據(jù)前面 prepare 階段準備好的圖層樹進行頂點著色习贫、形狀裝配、幾何著色千元、光柵化苫昌、片段著色與圖層混合。一旦 GPU 執(zhí)行完會將渲染好的圖像放入幀緩存區(qū)中等待下一個 VSYNC 的到來并交換到屏幕上進行顯示幸海。
Display 階段
階段 5:Display 顯示階段
在顯示階段祟身,主要是將幀緩存區(qū)中的內(nèi)容交換到顯示器上進行最終顯示
視圖渲染流程總結(jié)
-
App
:進行用戶事件的處理- Event:App接收到事件(touch、網(wǎng)絡(luò)請求物独、鍵盤袜硫、timer等)
- Commit
- layout (布局階段):layoutSubviews 會被所有需要布局的 View 調(diào)用
- display(顯示階段):drawRect 會被每個需要被更新的 View 調(diào)用
- prpare(準備階段):未解碼圖像進一步解碼(即需要優(yōu)化的常見的圖片主線程解碼操作)
- commit(提交階段):視圖樹將會被遞歸打包并發(fā)送到 RenderServer 中
-
RenderServer
:負責(zé)將圖層樹轉(zhuǎn)換為可顯示的圖像(即用戶界面繪制)- prepare:圖層樹被編譯成一系列簡單的指令,供 GPU 執(zhí)行挡篓,幀動畫也在此處進行處理
- excute:GPU 將 App 的圖層繪制成最終圖像
-
Display
:將緩沖的幀顯示出來
想了解離屏渲染的同學(xué)請閱讀# 屏幕卡頓 及 iOS中的渲染流程解析
卡頓類型
通過了解了視圖渲染的工作流程婉陷,其主要工作是在App 和 Render Server 中進行的,所以總共涉及兩種卡頓類型
-
提交卡頓
(App 階段) -
渲染卡頓
(Render Server 階段)
卡頓類型
提交卡頓
- 提交卡頓:是指 App 話費過長的時間來處理/提交事件
提交卡頓
如上圖所示瞻凤,在提交階段耗時過長憨攒,從而導(dǎo)致錯過了截止時間,所以在下一個 VSYNC 中 Render Server 沒有需要處理的事情阀参,必須要等待下一個 VSYNC 到了后才開始渲染肝集。簡單來說就是把幀傳送的時間延遲了一幀(即 16.67ms),這個延遲時間 即為 卡頓時間(Hitch time)
蛛壳。
如何避免提交卡頓杏瞻?
主要有以下幾種方式
- 保持視圖輕量
- 避免復(fù)雜布局
- 合理運用多線程能力
下面進行詳細說明
保持視圖的輕量
- 盡可能利用
CALayer
上GPU 加速的可用屬性,如非必要避免使用CPU進行自定義繪制 - 非必要情況下衙荐,避免重寫
drawRect
方法捞挥,因為會導(dǎo)致額外的內(nèi)存開銷。
針對于文本忧吟、圖片等原本就在 CPU 上進行繪制的系統(tǒng)控件砌函,我們可以嘗試使用其更底層線程安全的 CoreGraphics 能力,比如 TextKit、CoreText 等搭配多線程異步繪制減輕主線程壓力讹俊。
- 盡量復(fù)用視圖垦沉,避免重復(fù)的添加/移除視圖
- 如果需要將某一個視圖從某一個動畫中移除,盡量使用
hidden
屬性 - 對于 Prepare 階段仍劈,當(dāng)我們的 UIImage 容器視圖的大小小于圖片本身時厕倍,我們通常可以使用 下采樣技術(shù)(downsampling) 來進行縮略圖的創(chuàng)建以節(jié)省部分內(nèi)存空間贩疙。
避免復(fù)雜布局
- 減少代價過高且重復(fù)的布局讹弯,在需要更新布局時盡量只使用
setNeedsLayout
。
layoutIfNeeded 會消耗當(dāng)前事務(wù)的生命周期也會造成卡頓这溅,大多數(shù)時候你可以等到下一次 Runloop 執(zhí)行時再更新你的布局组民。
- 避免復(fù)雜布局約束,嘗試使用最少的約束來完成布局
- 避免遞歸布局芍躏,即視圖應(yīng)該只能使自己或自己的子視圖無效邪乍,而不能使其同級視圖或父視圖無效
- 避免非必要的視圖層級降狠,復(fù)雜的視圖層級會增加提交階段的整體耗時
合理運用多線程能力
- 利用GCD的多線程能力对竣,充分利用 CPU 多核優(yōu)勢,提前在子線程進行布局等 UI 無關(guān)操作榜配,避免主線程掛起(hang)否纬。
- 避免主線程 IO 等磁盤相關(guān)操作
- 針對于常見的主線程解碼操作,
- 在 iOS15 之前蛋褥,我們通常都是自己封裝或是利用最常見的第三方庫 SDWebImage 替我們在子線程進行解碼操作临燃。
- 在 iOS15 中,Apple 終于提供了官方的解決方案以解決該問題:UIImage 的
prepareThumbnailOfSize:completionHandler:
等新接口烙心。
- 針對于必須在 CPU 上進行繪制的組件膜廊,嘗試結(jié)合多線程使用
異步繪制
能力減輕主線程壓力。
渲染卡頓
-
渲染卡頓:是指 Render Server 無法按時準備/執(zhí)行圖層樹的出現(xiàn)淫茵,即 Excute 階段耗時超過了 VSYNC 的界限爪瓜,導(dǎo)致本來應(yīng)該渲染的幀為準備好。
渲染卡頓
如上圖所示匙瘪,綠色的畫面比預(yù)期的晚了一幀于是有了 16 毫秒的卡頓铆铆。
如何避免渲染卡頓?
Prepare 階段對卡頓的影響較少丹喻,主要還是在 Excute 階段的離屏渲染薄货。針對離屏渲染的優(yōu)化,請閱讀
# iOS 常見觸發(fā)離屏渲染場景及優(yōu)化方案總結(jié)
- 對于陰影來說碍论,在設(shè)置陰影時確保設(shè)置
shadowPath
以減少大量離屏通道 - 在圓化矩形時谅猾,使用
cornerRadius
和cornerCurve
屬性避免用蒙版或角內(nèi)容來構(gòu)成圓角矩形。 - 優(yōu)化整個 App 的 Mask。
使用 masksToBounds 遮蔽為矩形圓角矩形或橢圓形的性能比自定義蒙版圖層好得多
- 合理并謹慎的使用 shouldRasterize 屬性税娜,
它會對一塊圖層進行光柵化操作并進行緩存先煎。若針對于需要頻繁刷新的圖層使用該屬性反而對性能有著負面影響。
- 盡量使用非透明的圖層
- 盡量減少圖層混合
- 重要的是用
Instruments
來對 App 進行分析并檢查圖層樹以獲得重要的技巧從而降低整體離屏計數(shù)巧涧。
下面就主要介紹 Instrument 中 Animation Hitches
的使用
使用
-
選中 Instrument 中的
Animation Hitches
Animation Hitches -
啟動程序薯蝎,會顯示recording 此時操作界面卡頓的位置工具會記錄
記錄 -
然后再次點擊關(guān)閉等待Analyze分析完成后顯示如下界面,找出耗時的函數(shù)
耗時函數(shù) 最后分析谤绳,并根據(jù)實際情況解決問題
參考文章
Tech Talk - Hitches 與 渲染循環(huán)
iOS 性能檢測新方式——AnimationHitches
Animation Hitches in iOS Development
Explore UI animation hitches and the render loop - Tech Talks - Videos - Apple Developer
Find and fix hitches in the commit phase - Tech Talks - Videos
iOS 性能分析-阿里
iOS 高刷屏監(jiān)控 + 優(yōu)化:從理論到實踐全面解析 -字節(jié)
WWDC20 10077 - 使用 XCTest 消除動畫卡頓
# APP 性能優(yōu)化終極求生指南
# iOS性能優(yōu)化之界面卡頓監(jiān)測
# iOS 高刷屏監(jiān)控 + 優(yōu)化:從理論到實踐全面解析
## 精確定位頁面滑動幀率瓶頸及優(yōu)化參考