引言
從Xcode12開(kāi)始,Instrument更新了UI,新增了一個(gè)模板 Animation Hitches 用來(lái)分析用戶(hù)的 app 中的卡頓,并去除了 Core Animation 檢測(cè)方式。在 iPhone13Pro 之前 iPhone 屏幕最高刷新頻率仍為 60 HZ
,而在支持 PromotionDisplay 的設(shè)備上幀率可調(diào)整至 120 幀
,并且會(huì)根據(jù)當(dāng)前用戶(hù)手勢(shì)和設(shè)備狀態(tài)進(jìn)行動(dòng)態(tài)調(diào)整。此時(shí)再繼續(xù)使用幀率來(lái)判斷性能的好壞及流暢度將會(huì)是一個(gè)錯(cuò)誤的選擇。所以 Animation Hitches 主要用于代替幀率檢測(cè)
,并且提出 卡頓時(shí)間比
(Hitch Time Ratio) 的概念用于替代 FPS。
在 Hitch 提出之前,都是借助FPS
(Frames Per Second 幀率),即每秒繪制幀的數(shù)量來(lái)衡量頁(yè)面是否卡頓
- 滑動(dòng)屏幕時(shí),幀率理想值為60FPS
- 滑動(dòng)屏幕時(shí),幀率越高表示性能越好;幀率過(guò)低意味著屏幕可能出現(xiàn)卡頓,存在隨機(jī)丟幀的可能。
- 其中幀率
>=57為優(yōu)秀; >=55為良好; >=50為可接受;
蘋(píng)果于 20 年的 Session 中提出了 Hitch
的概念,用以衡量滑動(dòng)時(shí)的卡頓情況。Hitch 指的是 卡頓時(shí)間
(一幀延后出現(xiàn)的時(shí)間,ms)/ 總時(shí)間
(一般是 1 秒),簡(jiǎn)單來(lái)說(shuō) 卡頓時(shí)間比
就是一個(gè)區(qū)間內(nèi)的總卡頓時(shí)間除以它的持續(xù)時(shí)間。
-
低于 5 ms/s
說(shuō)明比較優(yōu)秀
,是最不易被用戶(hù)察覺(jué)到的 - 介于 5ms/s 和 10ms/s 之間,說(shuō)明發(fā)生了中等卡頓,用戶(hù)會(huì)察覺(jué)到一些中斷,但并不嚴(yán)重
-
高于 10 ms/s
說(shuō)明發(fā)生了較嚴(yán)重的卡頓
,已經(jīng)影響了用戶(hù)體驗(yàn)。
Hitch
卡頓
- 概念:任何時(shí)候屏幕上出現(xiàn)晚于預(yù)計(jì)的幀都屬于卡頓
-
簡(jiǎn)單來(lái)說(shuō)就是掉幀了,即沒(méi)有在規(guī)定時(shí)間內(nèi)渲染好一幀畫(huà)面,這就是卡頓一次
卡頓
如上圖所示,當(dāng)手指在屏幕上滑動(dòng)時(shí),滾動(dòng)視圖會(huì)隨著手勢(shì)做出響應(yīng),如果一幀一幀來(lái)看,就是每一幀都對(duì)應(yīng)手指位置的變化。當(dāng)卡頓發(fā)生時(shí),某一幀沒(méi)有跟隨手指變化,導(dǎo)致到下一幀時(shí),產(chǎn)生跳躍,打破了用戶(hù)和屏幕內(nèi)容的視覺(jué)連接感。圖中卡頓產(chǎn)生的原因就是第三幀重復(fù)了,主要是因?yàn)榈谒膸难舆t導(dǎo)致了第三幀占用了兩幀的時(shí)間,給用戶(hù)看到的就是卡頓掉幀的現(xiàn)象。
RenderLoop
-
概念:是一個(gè)連續(xù)的過(guò)程,通過(guò)用戶(hù)手勢(shì)等將事件傳給 App,接著 App 向操作系統(tǒng)傳遞事件并最終響應(yīng)事件,再將響應(yīng)傳遞給用戶(hù)的過(guò)程
RenderLoop
RenderLoop 的時(shí)間隨著設(shè)備刷新頻率,在 iPhone13 Pro(Max) 以下的 iPhone 設(shè)備最大均為 60 幀,而 iPhone13 Pro(Max) 及 iPadPro 則最高支持 120 幀,也就是最短僅需每 8.33 毫秒就可以顯示一個(gè)新幀。
RenderLoop刷新頻率
視圖渲染流程
在每一幀顯示的過(guò)程中,大概可以分為3個(gè)階段,如下所示
-
App
:進(jìn)行用戶(hù)事件處理 -
Render server
:負(fù)責(zé)將圖層樹(shù)轉(zhuǎn)換為可顯示的圖像(即用戶(hù)界面繪制) -
On the display
:顯示緩存的幀
注
1、App
和Render server
階段需要在下一個(gè) VSYNC 到來(lái)之前完成
2、這里運(yùn)用了雙緩存區(qū) + 垂直同步機(jī)制
,主要是用于解決 屏幕撕裂現(xiàn)象
3、整個(gè)渲染階段可以分為5個(gè)階段:
渲染階段細(xì)分
- 階段 1 + 2:App(Event + Commit)
- 階段 3 + 4:Render server(Prepare + Excute)
- 階段 5:Display
4、整體渲染流程如下
- 在
Event(事件階段)
通過(guò) touch、timer 等事件決定用戶(hù)界面是否需要改變- 在
Commit(提交階段)
,App會(huì)向 Render server(渲染服務(wù)器)提交渲染命令- 在
Prepare (準(zhǔn)備階段)
會(huì)為 GPU 的繪制組好準(zhǔn)備- 在
Excute(執(zhí)行階段)
會(huì)由 GPU 將用戶(hù)界面的圖像繪制出來(lái)- 在
Display(顯示階段)
會(huì)將緩沖區(qū)的幀交換到屏幕上顯示
下面以一個(gè)帶有陰影的渲染圖形為例,通過(guò)觀(guān)察 RenderLoop 中每一幀所做的工作,來(lái)分別介紹不同階段
App 階段
App 階段包含 2 個(gè)階段,分別是 Event 、Commit。其中Commit 又分為 4 個(gè)子階段,分別是
- Layout
- Display
- Prepare
- Commit
階段 1:Event 事件階段
-
事件階段 表示 App 接收到了事件(例如:touch、網(wǎng)絡(luò)請(qǐng)求回調(diào)、鍵盤(pán)、timer等)。
事件階段-Event -
在 App 中可以通過(guò)改變其層級(jí)結(jié)構(gòu),或者使用其他方式響應(yīng)事件。例如圖層顏色/大小/位置變化。當(dāng) App 更新了圖層時(shí), CoreAnimation 會(huì)同時(shí)調(diào)用
setNeedsLayout
方法,該方法能夠找出哪些圖層需要重新計(jì)算布局,且系統(tǒng)會(huì)合并這些重新計(jì)算的請(qǐng)求,并在 Commit 階段按需執(zhí)行,以此來(lái)減少重復(fù)工作
層級(jí)變化
階段 2: Commit 提交階段
提交階段還可以細(xì)分,主要分為4個(gè)子階段
-
layout (布局階段)
:layoutSubviews 會(huì)被所有需要布局的 View 調(diào)用 -
display(顯示階段)
:drawRect 會(huì)被每個(gè)需要被更新的 View 調(diào)用 -
prpare(準(zhǔn)備階段)
:未解碼圖像進(jìn)一步解碼(即需要優(yōu)化的常見(jiàn)的圖片主線(xiàn)程解碼操作) -
commit(提交階段)
:視圖樹(shù)將會(huì)被遞歸打包并發(fā)送到 RenderServer 中
Layout - 布局階段
在布局階段,layoutSubviews
會(huì)被所有需要布局的 View 調(diào)用。例如視圖布局(frame、bounds、tranform等)、增加/移除視圖、直接調(diào)用 setNeedsLayout/layoutIfNeesed 等
注:這些操作并不是立即執(zhí)行,系統(tǒng)會(huì)合并布局請(qǐng)求,在 Runloop 休眠前進(jìn)行統(tǒng)一處理
Display - 顯示階段
- 在顯示階段,
drawRect
會(huì)被每個(gè)需要被更新的 View 調(diào)用,例如 UILabel 等空間類(lèi)或者 任何重寫(xiě) drawRect 方法 的類(lèi),必須調(diào)用 調(diào)用setNeedsDisplay
用以支持 View 的更新。 - 非必須不要重寫(xiě) drawRect 方法,因?yàn)樵诶L制時(shí),每個(gè)自定義圖層都會(huì)接收到帶紋理的 CoreGraphics 的背景,會(huì)利用 CoreAnimation 進(jìn)行繪制,這些圖層就變成了圖片
- 導(dǎo)致內(nèi)存額外的開(kāi)銷(xiāo)以及bitmap的存儲(chǔ),對(duì)整體內(nèi)存壓力較大
- 由于是在 CPU上進(jìn)行繪制,還增加了整體主線(xiàn)程的占用
Prepare - 準(zhǔn)備階段
在準(zhǔn)備階段,主要是將還未解碼的圖像進(jìn)行進(jìn)一步解碼,這也是我們需要優(yōu)化的點(diǎn)(即優(yōu)化圖片主線(xiàn)程解碼操作
)。
因?yàn)閷?duì)于每個(gè)解碼的圖像,App可能會(huì)持續(xù)存在大量的內(nèi)存分配(與圖像大小成正比),當(dāng)App占用內(nèi)存越來(lái)越多時(shí),操作系統(tǒng)就會(huì)開(kāi)始?jí)嚎s物理內(nèi)存(physical memory),這整個(gè)過(guò)程都需要CPU參與,所以除了App會(huì)使用CPU,還增加了無(wú)法控制的全局 CPU 使用率,導(dǎo)致App消耗更多的物理內(nèi)存,此時(shí)操作系統(tǒng)會(huì)終止低優(yōu)先級(jí)的后臺(tái)進(jìn)程,從而釋放更多的物理內(nèi)存。但設(shè)備的物理內(nèi)存始終是有限的,當(dāng)App對(duì)內(nèi)存的消耗達(dá)到了臨界值時(shí),該App進(jìn)程就會(huì)被操作系統(tǒng)終止,這就是常說(shuō)的大圖導(dǎo)致的OOM
。
若某個(gè)圖像的顏色格式 GPU 無(wú)法直接使用,也會(huì)在這一步進(jìn)行格式轉(zhuǎn)換。這就要求對(duì)該圖像進(jìn)行 copy 操作,而不是直接使用指針,這樣會(huì)耗時(shí)更長(zhǎng)及占用更多的內(nèi)存。
Commit - 提交階段
在提交階段,視圖樹(shù)會(huì)被遞歸打包,并發(fā)送到 Render Server中,所以當(dāng)視圖圖層較復(fù)雜時(shí),這個(gè)過(guò)程的耗時(shí)也會(huì)相對(duì)較長(zhǎng),這也是我們經(jīng)常提及的優(yōu)化點(diǎn)(即盡量減輕視圖層級(jí)結(jié)構(gòu),不要跟套娃似的,無(wú)窮無(wú)盡
)。
Render Server 階段
Render Server(渲染服務(wù)器)主要負(fù)責(zé)將圖層樹(shù)轉(zhuǎn)換為真正顯示的圖像,分為兩個(gè)子階段
-
prepare
:圖層樹(shù)被編譯成一系列簡(jiǎn)單的指令,供 GPU 執(zhí)行,幀動(dòng)畫(huà)也在此處進(jìn)行處理 -
excute
:GPU 將 App 的圖層繪制成最終圖像
階段 3:Prepare 準(zhǔn)備階段
-
在 準(zhǔn)備階段,RenderServer 會(huì)廣度優(yōu)先遍歷 App 的圖層樹(shù),準(zhǔn)備一個(gè)線(xiàn)性管線(xiàn),這樣 GPU 就能按照順序執(zhí)行命令進(jìn)行繪制。
遍歷圖層樹(shù) -
從根圖層開(kāi)始逐層遍歷,最終才有了 GPU 可以在下一個(gè)執(zhí)行階段執(zhí)行的整個(gè)管線(xiàn)。
逐層遍歷
階段 4:Excute 執(zhí)行階段
在執(zhí)行階段,主要是由 GPU 根據(jù)前面 prepare 階段準(zhǔn)備好的圖層樹(shù)進(jìn)行頂點(diǎn)著色、形狀裝配、幾何著色、光柵化、片段著色與圖層混合。一旦 GPU 執(zhí)行完會(huì)將渲染好的圖像放入幀緩存區(qū)中等待下一個(gè) VSYNC 的到來(lái)并交換到屏幕上進(jìn)行顯示。
Display 階段
階段 5:Display 顯示階段
在顯示階段,主要是將幀緩存區(qū)中的內(nèi)容交換到顯示器上進(jìn)行最終顯示
視圖渲染流程總結(jié)
-
App
:進(jìn)行用戶(hù)事件的處理- Event:App接收到事件(touch、網(wǎng)絡(luò)請(qǐng)求、鍵盤(pán)、timer等)
- Commit
- layout (布局階段):layoutSubviews 會(huì)被所有需要布局的 View 調(diào)用
- display(顯示階段):drawRect 會(huì)被每個(gè)需要被更新的 View 調(diào)用
- prpare(準(zhǔn)備階段):未解碼圖像進(jìn)一步解碼(即需要優(yōu)化的常見(jiàn)的圖片主線(xiàn)程解碼操作)
- commit(提交階段):視圖樹(shù)將會(huì)被遞歸打包并發(fā)送到 RenderServer 中
-
RenderServer
:負(fù)責(zé)將圖層樹(shù)轉(zhuǎn)換為可顯示的圖像(即用戶(hù)界面繪制)- prepare:圖層樹(shù)被編譯成一系列簡(jiǎn)單的指令,供 GPU 執(zhí)行,幀動(dòng)畫(huà)也在此處進(jìn)行處理
- excute:GPU 將 App 的圖層繪制成最終圖像
-
Display
:將緩沖的幀顯示出來(lái)
想了解離屏渲染的同學(xué)請(qǐng)閱讀# 屏幕卡頓 及 iOS中的渲染流程解析
卡頓類(lèi)型
通過(guò)了解了視圖渲染的工作流程,其主要工作是在App 和 Render Server 中進(jìn)行的,所以總共涉及兩種卡頓類(lèi)型
-
提交卡頓
(App 階段) -
渲染卡頓
(Render Server 階段)
卡頓類(lèi)型
提交卡頓
- 提交卡頓:是指 App 話(huà)費(fèi)過(guò)長(zhǎng)的時(shí)間來(lái)處理/提交事件
提交卡頓
如上圖所示,在提交階段耗時(shí)過(guò)長(zhǎng),從而導(dǎo)致錯(cuò)過(guò)了截止時(shí)間,所以在下一個(gè) VSYNC 中 Render Server 沒(méi)有需要處理的事情,必須要等待下一個(gè) VSYNC 到了后才開(kāi)始渲染。簡(jiǎn)單來(lái)說(shuō)就是把幀傳送的時(shí)間延遲了一幀(即 16.67ms),這個(gè)延遲時(shí)間 即為 卡頓時(shí)間(Hitch time)
。
如何避免提交卡頓?
主要有以下幾種方式
- 保持視圖輕量
- 避免復(fù)雜布局
- 合理運(yùn)用多線(xiàn)程能力
下面進(jìn)行詳細(xì)說(shuō)明
保持視圖的輕量
- 盡可能利用
CALayer
上GPU 加速的可用屬性,如非必要避免使用CPU進(jìn)行自定義繪制 - 非必要情況下,避免重寫(xiě)
drawRect
方法,因?yàn)闀?huì)導(dǎo)致額外的內(nèi)存開(kāi)銷(xiāo)。
針對(duì)于文本、圖片等原本就在 CPU 上進(jìn)行繪制的系統(tǒng)控件,我們可以嘗試使用其更底層線(xiàn)程安全的 CoreGraphics 能力,比如 TextKit、CoreText 等搭配多線(xiàn)程異步繪制減輕主線(xiàn)程壓力。
- 盡量復(fù)用視圖,避免重復(fù)的添加/移除視圖
- 如果需要將某一個(gè)視圖從某一個(gè)動(dòng)畫(huà)中移除,盡量使用
hidden
屬性 - 對(duì)于 Prepare 階段,當(dāng)我們的 UIImage 容器視圖的大小小于圖片本身時(shí),我們通常可以使用 下采樣技術(shù)(downsampling) 來(lái)進(jìn)行縮略圖的創(chuàng)建以節(jié)省部分內(nèi)存空間。
避免復(fù)雜布局
- 減少代價(jià)過(guò)高且重復(fù)的布局,在需要更新布局時(shí)盡量只使用
setNeedsLayout
。
layoutIfNeeded 會(huì)消耗當(dāng)前事務(wù)的生命周期也會(huì)造成卡頓,大多數(shù)時(shí)候你可以等到下一次 Runloop 執(zhí)行時(shí)再更新你的布局。
- 避免復(fù)雜布局約束,嘗試使用最少的約束來(lái)完成布局
- 避免遞歸布局,即視圖應(yīng)該只能使自己或自己的子視圖無(wú)效,而不能使其同級(jí)視圖或父視圖無(wú)效
- 避免非必要的視圖層級(jí),復(fù)雜的視圖層級(jí)會(huì)增加提交階段的整體耗時(shí)
合理運(yùn)用多線(xiàn)程能力
- 利用GCD的多線(xiàn)程能力,充分利用 CPU 多核優(yōu)勢(shì),提前在子線(xiàn)程進(jìn)行布局等 UI 無(wú)關(guān)操作,避免主線(xiàn)程掛起(hang)。
- 避免主線(xiàn)程 IO 等磁盤(pán)相關(guān)操作
- 針對(duì)于常見(jiàn)的主線(xiàn)程解碼操作,
- 在 iOS15 之前,我們通常都是自己封裝或是利用最常見(jiàn)的第三方庫(kù) SDWebImage 替我們?cè)谧泳€(xiàn)程進(jìn)行解碼操作。
- 在 iOS15 中,Apple 終于提供了官方的解決方案以解決該問(wèn)題:UIImage 的
prepareThumbnailOfSize:completionHandler:
等新接口。
- 針對(duì)于必須在 CPU 上進(jìn)行繪制的組件,嘗試結(jié)合多線(xiàn)程使用
異步繪制
能力減輕主線(xiàn)程壓力。
渲染卡頓
-
渲染卡頓:是指 Render Server 無(wú)法按時(shí)準(zhǔn)備/執(zhí)行圖層樹(shù)的出現(xiàn),即 Excute 階段耗時(shí)超過(guò)了 VSYNC 的界限,導(dǎo)致本來(lái)應(yīng)該渲染的幀為準(zhǔn)備好。
渲染卡頓
如上圖所示,綠色的畫(huà)面比預(yù)期的晚了一幀于是有了 16 毫秒的卡頓。
如何避免渲染卡頓?
Prepare 階段對(duì)卡頓的影響較少,主要還是在 Excute 階段的離屏渲染。針對(duì)離屏渲染的優(yōu)化,請(qǐng)閱讀
# iOS 常見(jiàn)觸發(fā)離屏渲染場(chǎng)景及優(yōu)化方案總結(jié)
- 對(duì)于陰影來(lái)說(shuō),在設(shè)置陰影時(shí)確保設(shè)置
shadowPath
以減少大量離屏通道 - 在圓化矩形時(shí),使用
cornerRadius
和cornerCurve
屬性避免用蒙版或角內(nèi)容來(lái)構(gòu)成圓角矩形。 - 優(yōu)化整個(gè) App 的 Mask。
使用 masksToBounds 遮蔽為矩形圓角矩形或橢圓形的性能比自定義蒙版圖層好得多
- 合理并謹(jǐn)慎的使用 shouldRasterize 屬性,
它會(huì)對(duì)一塊圖層進(jìn)行光柵化操作并進(jìn)行緩存。若針對(duì)于需要頻繁刷新的圖層使用該屬性反而對(duì)性能有著負(fù)面影響。
- 盡量使用非透明的圖層
- 盡量減少圖層混合
- 重要的是用
Instruments
來(lái)對(duì) App 進(jìn)行分析并檢查圖層樹(shù)以獲得重要的技巧從而降低整體離屏計(jì)數(shù)。
下面就主要介紹 Instrument 中 Animation Hitches
的使用
使用
-
選中 Instrument 中的
Animation Hitches
Animation Hitches -
啟動(dòng)程序,會(huì)顯示recording 此時(shí)操作界面卡頓的位置工具會(huì)記錄
記錄 -
然后再次點(diǎn)擊關(guān)閉等待Analyze分析完成后顯示如下界面,找出耗時(shí)的函數(shù)
耗時(shí)函數(shù) 最后分析,并根據(jù)實(shí)際情況解決問(wèn)題
參考文章
Tech Talk - Hitches 與 渲染循環(huán)
iOS 性能檢測(cè)新方式——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)化:從理論到實(shí)踐全面解析 -字節(jié)
WWDC20 10077 - 使用 XCTest 消除動(dòng)畫(huà)卡頓
# APP 性能優(yōu)化終極求生指南
# iOS性能優(yōu)化之界面卡頓監(jiān)測(cè)
# iOS 高刷屏監(jiān)控 + 優(yōu)化:從理論到實(shí)踐全面解析
## 精確定位頁(yè)面滑動(dòng)幀率瓶頸及優(yōu)化參考