UIView 動(dòng)畫(huà)降幀探究
作者丨Rickey王小吉
來(lái)源丨一瓜技術(shù)(ID:tech_gua)
目錄

為什么要降幀

首先要說(shuō)明一件事,那就是為什么要對(duì)動(dòng)畫(huà)降幀?
眾所周知,刷新頻率越高體驗(yàn)越好,對(duì)于 iOS app 的刷新頻率應(yīng)該是越接近越 60fps 越好,這里主動(dòng)給動(dòng)畫(huà)降幀,肯定會(huì)影響動(dòng)畫(huà)的體驗(yàn)。但是另一方面,我們也知道動(dòng)畫(huà)渲染的過(guò)程中需要消耗大量的 GPU 資源,所以給動(dòng)畫(huà)降幀則可以給 GPU 減負(fù),降低 GPU 使用率峰值。
所以給動(dòng)畫(huà)降幀,實(shí)際上是一種用體驗(yàn)換性能的決策,在動(dòng)畫(huà)不復(fù)雜但是數(shù)量很多的情況下(比如一些彈幕動(dòng)畫(huà)、點(diǎn)贊動(dòng)畫(huà)),給動(dòng)畫(huà)降幀并不會(huì)影響動(dòng)畫(huà)效果,此時(shí)降幀就能累計(jì)節(jié)約大量的 GPU 性能。
動(dòng)畫(huà)渲染對(duì)性能的消耗
iOS 中的屏幕渲染原理可以參看之前的文章:iOS 渲染全解析,文中會(huì)講解整個(gè)屏幕渲染的過(guò)程,詳細(xì)說(shuō)明了 Core Animation 渲染流水線的整個(gè)原理,為什么渲染過(guò)程會(huì)對(duì) GPU 有較大的消耗。

下圖是 Core Animation 的單次渲染流水線,也就是一幀動(dòng)畫(huà)的渲染過(guò)程:
Handle Events:這個(gè)過(guò)程中會(huì)先處理點(diǎn)擊事件,這個(gè)過(guò)程中有可能會(huì)需要改變頁(yè)面的布局和界面層次。 Commit Transaction:此時(shí) app 會(huì)通過(guò) CPU 處理顯示內(nèi)容的前置計(jì)算,比如布局計(jì)算、圖片解碼等任務(wù),接下來(lái)會(huì)進(jìn)行詳細(xì)的講解。之后將計(jì)算好的圖層進(jìn)行打包發(fā)給 Render Server。 Render Server - Decode:打包好的圖層被傳輸?shù)?Render Server 之后,首先會(huì)進(jìn)行解碼。注意完成解碼之后需要等待下一個(gè) RunLoop 才會(huì)執(zhí)行下一步 Draw Calls。 Render Server - Draw Calls:解碼完成后,Core Animation 會(huì)調(diào)用下層渲染框架的方法進(jìn)行繪制,進(jìn)而調(diào)用到 GPU。 GPU - Render:這一階段主要由 GPU 進(jìn)行渲染。 Display:顯示階段,需要等 render 結(jié)束的下一個(gè) RunLoop 觸發(fā)顯示。
總而言之,每一幀動(dòng)畫(huà)的渲染對(duì)于 CPU 和 GPU 都有一定的消耗,尤其是對(duì) GPU 的性能占用較大。
屏幕刷新 FPS vs CoreAnimation FPS
vSync 垂直信號(hào)刷新屏幕的原理我們都知道,但是在 iOS 中并不止有一種 FPS。
屏幕刷新FPS
屏幕刷新幀率就是我們通常說(shuō)的 FPS,由于人眼的視覺(jué)暫留效應(yīng),當(dāng)屏幕刷新頻率足夠高時(shí)(FPS 通常是 50 到 60 左右),就能讓畫(huà)面看起來(lái)是連續(xù)而流暢的。當(dāng)一次渲染時(shí)間過(guò)長(zhǎng),就會(huì)發(fā)生掉幀的現(xiàn)象,此時(shí) FPS 下降,用戶就能直觀地感受到卡頓。
對(duì)于 iOS 用戶來(lái)說(shuō), 屏幕刷新幀率直接反應(yīng)了流暢度體驗(yàn),顯然 FPS 越高、越接近 60 越好。
CoreAnimation FPS
CoreAnimation FPS 指的是 CoreAnimation Render Server 的運(yùn)行幀率,對(duì)應(yīng)前面渲染流水線中非常重要的 GPU render 階段。

可以發(fā)現(xiàn),每一次渲染流水線都一定會(huì)有 Render Server 參與的過(guò)程,所以 Render Server 運(yùn)行的頻率直接反應(yīng)了 GPU 被調(diào)用的頻率。CoreAnimation FPS 越高,意味著 GPU 被渲染流水線使用的越頻繁,那么相應(yīng)的 GPU 使用率就會(huì)越高。
所以簡(jiǎn)單來(lái)說(shuō) CoreAnimation FPS 直接影響了 GPU 的使用率,一般來(lái)說(shuō) CoreAnimation FPS 越低越好。
正常情況下,如果界面沒(méi)有頻繁的 UI 變更,不需要頻繁的重新渲染,那么 CoreAnimation FPS 應(yīng)該是非常低的。但是如果使用了高幀率動(dòng)畫(huà),由于需要快速更新動(dòng)畫(huà)效果,必然會(huì)引起 CoreAnimation FPS 升高。

我們使用 Instrument 中 CoreAnimation FPS 選項(xiàng)測(cè)出的 FPS 就是 CoreAnimation FPS,如圖可以看到,能夠通過(guò) Instrument 監(jiān)測(cè)到 avg CoreAnimation FPS,以及 GPU 使用率的情況,可以將這些指標(biāo)作為幀率優(yōu)化的結(jié)果指標(biāo)。
降幀方案
在調(diào)查降幀方案之前,先回顧一下我們的最終目的:調(diào)研多種動(dòng)畫(huà)實(shí)現(xiàn)方法,選擇可以控制或者降低渲染幀率的方式,重新實(shí)現(xiàn)已有動(dòng)畫(huà)。進(jìn)而達(dá)到降低 GPU 使用率的效果。
重寫(xiě) DrawRect:
一種常見(jiàn)自定義動(dòng)畫(huà)的方案是通過(guò)重寫(xiě) drawRect: 方法實(shí)現(xiàn):改變 view 屬性 -> 觸發(fā) drawRect: 進(jìn)行重繪 -> 改變 view 的展示。
在前文提到的渲染流水線的 Commit Transaction 這個(gè)階段中,其中 Display 步驟會(huì)通過(guò) Core Graphics 進(jìn)行視圖的繪制,注意不是真正的顯示,而是得到圖元 primitives 數(shù)據(jù)。
注意正常情況下 Display 階段只會(huì)得到圖元 primitives 信息,而位圖 bitmap 是在 GPU 中根據(jù)圖元信息繪制得到的。但是如果重寫(xiě)了 drawRect: 方法,這個(gè)方法會(huì)直接調(diào)用 Core Graphics 繪制方法得到 bitmap 數(shù)據(jù),同時(shí)系統(tǒng)會(huì)額外申請(qǐng)一塊內(nèi)存,用于暫存繪制好的 bitmap。
由于重寫(xiě)了 drawRect: 方法,導(dǎo)致繪制過(guò)程從 GPU 轉(zhuǎn)移到了 CPU,這就導(dǎo)致了一定的效率損失。與此同時(shí),這個(gè)過(guò)程會(huì)額外使用 CPU 和內(nèi)存,因此如果繪制效率不夠高,很容易造成 CPU 卡頓或者內(nèi)存爆炸。
顯而易見(jiàn),通過(guò)重寫(xiě) DrawRect: 方法來(lái)實(shí)現(xiàn)的動(dòng)畫(huà)并不適合來(lái)做降幀優(yōu)化的方案。
Core Animation 動(dòng)畫(huà)

由于 CALayer 保存的是一個(gè)靜態(tài)的 bitmap 以及一些狀態(tài)信息(如透明度、旋轉(zhuǎn)角度等),對(duì)于一個(gè)動(dòng)畫(huà)過(guò)程,實(shí)際上改變的是 layer 的狀態(tài),而不是靜態(tài)內(nèi)容。這也就意味著當(dāng)動(dòng)畫(huà)發(fā)生時(shí),Core Animation 會(huì)將靜態(tài) bitmap 以及改變后的狀態(tài)傳遞給 GPU,GPU 根據(jù) bitmap 及新的狀態(tài),將新的樣式繪制在屏幕上。
而對(duì)比更傳統(tǒng)的基于 UIView 重寫(xiě) drawRect: 的動(dòng)畫(huà)實(shí)現(xiàn)方式,drawRect: 每一次動(dòng)畫(huà)改變都需要依賴新參數(shù)進(jìn)行重繪,這就導(dǎo)致了主線程昂貴的 CPU 消耗?;?CALayer 的動(dòng)畫(huà)就能避免這些消耗,因?yàn)?GPU 是直接根據(jù) bitmap 進(jìn)行繪制,GPU 會(huì)對(duì) bitmap 進(jìn)行緩存,這樣能極大地節(jié)約 CPU 的性能消耗,提高效率。
通常 Core Animation 動(dòng)畫(huà)是實(shí)現(xiàn)復(fù)雜動(dòng)畫(huà)的首選,但是在降幀目的下,無(wú)法通過(guò) Core Animation 將渲染幀率下降到 60FPS 以下。
UIView animation block
UIView animation block 是根據(jù) Core Animation 動(dòng)畫(huà)的封裝,使用起來(lái)更加簡(jiǎn)潔。同時(shí),UIView 提供 UIViewAnimationOptionPreferredFramesPerSecond30 屬性,可以支持指定動(dòng)畫(huà)刷新頻率為 30fps。
注:實(shí)際上 CoreAnimation 動(dòng)畫(huà)通過(guò)設(shè)置 CAAnimation 私有屬性 preferredFramesPerSecond,也可以達(dá)到降幀的效果,具體可查看源代碼 iOS-Runtime-Headers(https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/QuartzCore.framework/CAAnimation.h) 中的相關(guān)屬性。
通過(guò)這個(gè)屬性,可以很方便地實(shí)現(xiàn)降幀的目的,但是這個(gè)方案也并不是完美的,由于只支持了直線位移,所以當(dāng)涉及到貝塞爾曲線位移的時(shí)候,需要手動(dòng)計(jì)算貝塞爾曲線上的點(diǎn),進(jìn)行近似的位移。
利用 CADisplayLink 進(jìn)行逐幀動(dòng)畫(huà)
CADisplayLink 是一個(gè)能讓我們以和屏幕刷新率相同的頻率將內(nèi)容畫(huà)到屏幕上的定時(shí)器,每次屏幕內(nèi)容刷新結(jié)束時(shí),runloop 就會(huì)向?qū)?yīng)的 target 發(fā)送一次 selector 方法,selector 就會(huì)被調(diào)用一次。
相比另外兩種定時(shí)器, NSTimer 精度稍低,并且延遲時(shí)間會(huì)逐漸累積,當(dāng) runloop 處于阻塞狀態(tài),NSTimer 的操作就會(huì)被推遲到下一個(gè) runloop,很容易造成動(dòng)畫(huà)失控;而基于 dispatch_source_t 的定時(shí)器同樣也不是百分百精確,如果 GCD 內(nèi)部管理的所有線程都被占用時(shí),其觸發(fā)事件也將被延遲。
實(shí)際上使用 CADisplayLink 是一種終極方案,最終可以通過(guò)設(shè)置 CADisplayLink 的計(jì)時(shí)幀率來(lái)控制動(dòng)畫(huà)的幀率,不再只有 30 幀和 60 幀兩個(gè)選項(xiàng),能更好的適應(yīng)多種情況,更好地平衡 GPU 占用率與用戶體驗(yàn)。

另一種基于 CADisplayLink 的情況的好處在于可以精準(zhǔn)控制多個(gè)動(dòng)畫(huà)觸發(fā)時(shí)間。如果有多個(gè)低幀動(dòng)畫(huà)同時(shí)進(jìn)行,但是這些低幀動(dòng)畫(huà)的刷新時(shí)刻不一定是同時(shí)觸發(fā)的,那么整體來(lái)看有可能就會(huì)造成降幀效果不佳的情況(極端情況比如上圖,兩個(gè) 30 幀動(dòng)畫(huà)同時(shí)進(jìn)行卻剛好錯(cuò)開(kāi),造成 60 幀的效果)。這時(shí)通過(guò) CADisplayLink 就能夠精準(zhǔn)地控制多個(gè)動(dòng)畫(huà)的刷新時(shí)刻,保證降幀的效果。
但是基于 CADisplayLink 實(shí)現(xiàn)動(dòng)畫(huà)需要重寫(xiě)大量代碼,工作量很大,具體可以參考 Facebook - pop (https://github.com/facebookarchive/pop),該庫(kù)雖然不支持自定義幀率,但是已經(jīng)完整實(shí)現(xiàn)了基于 CADisplayLink 的自定義動(dòng)畫(huà)。
測(cè)試方案與結(jié)論
最終方案
基于上面的各種原因,這次選用了 UIView animation block + UIViewAnimationOptionPreferredFramesPerSecond30 屬性進(jìn)行動(dòng)畫(huà)降幀的方案,可以較少改動(dòng)代碼而實(shí)現(xiàn)降幀的目的。
在這種方案下,如果本身動(dòng)畫(huà)使用的就是 UIView animation block,那么直接加上 UIViewAnimationOptionPreferredFramesPerSecond30 屬性就可以了;如果基于 CoreAnimation 實(shí)現(xiàn)的動(dòng)畫(huà),也能快捷地改造為 UIView animation block。
下面我們以進(jìn)入抖音直播、在直播間內(nèi)觸發(fā)點(diǎn)贊動(dòng)畫(huà)為例子,進(jìn)行對(duì)比測(cè)試:

原本方案(60 FPS):基于 Core Animation 的動(dòng)畫(huà) 降幀方案(30 FPS):修改為 UIView animation block + UIViewAnimationOptionPreferredFramesPerSecond30
對(duì)幀率的影響
這張圖是沒(méi)做改動(dòng)的情況,可以看到在直播間內(nèi)瘋狂觸發(fā)動(dòng)畫(huà)時(shí),會(huì)將 Core Animation FPS 打滿,始終保持在 59 - 60 FPS。

采用降幀方案后,可以看到幀率明顯下降,整個(gè) App 的 Core Animation FPS 能降低到 40 FPS 左右。

對(duì) CPU & GPU 的影響
經(jīng)過(guò)多次測(cè)試,將數(shù)據(jù)結(jié)論總結(jié)如下:
動(dòng)畫(huà)觸發(fā)頻率較低時(shí):降幀方案能有效降低 GPU 占用率(下降 10%-20%),略微降低 CPU 占用率。 高頻觸發(fā)動(dòng)畫(huà) or 長(zhǎng)時(shí)間連續(xù)觸發(fā)動(dòng)畫(huà),超過(guò)一定臨界時(shí)間之后:降幀方案下 GPU、CPU 占用率反而升高。 臨界點(diǎn)有機(jī)器性能決定,低端機(jī)的臨界時(shí)間也更短;對(duì)于較新機(jī)型,很難觸及到臨界時(shí)間,但對(duì)于 iPhone 6 來(lái)說(shuō),復(fù)雜的動(dòng)畫(huà)如果頻率稍高一些,就會(huì)導(dǎo)致 GPU、CPU 占用率很快升高。
臨界時(shí)間現(xiàn)象解釋
從上面的數(shù)據(jù)結(jié)論可以發(fā)現(xiàn),改造為 UIView animation block 方案之后,容易遭遇 CPU 瓶頸。
當(dāng) animation block 過(guò)多(每個(gè)動(dòng)畫(huà)需要的 block 過(guò)多?? 每秒觸發(fā)的動(dòng)畫(huà)數(shù)過(guò)多 ?? 持續(xù)時(shí)間過(guò)長(zhǎng))而無(wú)法被消化時(shí),會(huì)遭遇 CPU 瓶頸,導(dǎo)致 CPU、GPU 占用率反而升高。
對(duì)于這個(gè)現(xiàn)象暫時(shí)也沒(méi)有發(fā)現(xiàn)具體的解釋,不過(guò)我大概有如下的猜測(cè):
Block-based animation 的每一個(gè) block 都會(huì)返回一個(gè) UIViewAdditiveAnimationAction 類,用于 CALayer 動(dòng)畫(huà)的 actionForKey: 方法的回調(diào)。 與此同時(shí),根據(jù) block 中的具體內(nèi)容生成對(duì)應(yīng)的類,需要換算 fromValue 以及 toValue 等內(nèi)容(不清楚換算是否在主線程實(shí)現(xiàn),否則也可能造成線程爆炸),比起直接基于 CoreAnimation keyFrames 的動(dòng)畫(huà)需要更多的 CPU 計(jì)算量。 因此,當(dāng) block 堆積過(guò)多,就可能會(huì)造成 CPU 負(fù)擔(dān)過(guò)重。
總結(jié)
通過(guò) PreferredFramesPerSecond30 屬性能有效降低刷新幀率,從而降低 CPU、GPU 占用率 降低刷新幀率后,GPU 占用率下降比較明顯,下降能達(dá)到 10%-20% 當(dāng) animation block 過(guò)多而無(wú)法被消化時(shí),會(huì)遭遇 CPU 瓶頸,導(dǎo)致 CPU、GPU 占用率反而升高。且低端機(jī)更容易遭遇瓶頸
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取