原文連接html
隱式動畫
實現的背後體現了核心動畫精心設計的許多機制。在layer
的屬性發生改變以後,會向它的代理方請求一個CAAction
行爲來完成後續的工做,系統容許代理方返回nil
指針。一旦這麼作,修改屬性的工做最終移交給CATransaction
處理,由修改的屬性值決定是否自動生成一個CABasicAnimation
。若是知足,此時隱式動畫將被觸發。併發
在覈心動畫中,每一個圖層的修改都是事務CATransaction
的一部分,它能夠同時對多個layer
的屬性進行修改,而後成批的將將多個圖層樹包裝起來,一次性發送到渲染服務進程。CATransaction
事務對象被分爲implicit
和explicit
兩種類型,分別對應隱式
和顯式
。implicit transaction
會被投遞到線程的下一個runloop
完成處理:app
Core Animation supports two types of transactions: implicit transactions and explicit transactions. Implicit transactions are created automatically when the layer tree is modified by a thread without an active transaction and are committed automatically when the thread's runloop next iterates.dom
默認狀況下,CATransaction
會在背後獨立完成圖層樹屬性計算的工做。系統提供API
來顯式的使用事務類,而且手動提交給渲染服務進程,這種作法被稱做推動過渡
。推動過渡
會生成一個默認時長爲0.25s
時長的動畫效果來完成屬性值的修改。下面代碼會在0.25s
內將圖層放大一倍:異步
[CATransaction begin];
self.circle.transform = CATransform3DScale(CATransform3DIdentity, 2, 2, 1);
[CATransaction commit];
複製代碼
圖層屬性被修改時,會朝着本身的代理對象請求一個CAAction
行爲來幫助本身完成屬性修改的行爲。代理方法actionForLayer:forKey:
容許三種返回的數據格式來完成不一樣的修改動做:函數
空對象oop
UIView
在響應代理時默認會返回一個NSNull
對象,表示屬性修改後,不實現任何的動做,根據修改後的屬性值直接更新視圖。但UIView
不老是會返回空對象,若是layer
的修改發生在[UIView animatedXXX]
接口的block
中,每個修改的屬性值UIView
都會返回對應的CABasicAnimation
對象來進行動畫修改性能
nil
學習
手動建立並添加到視圖上的CALayer
或其子類在屬性修改時,沒有獲取到具體的修改行爲。此時被修改的屬性會被CATransaction
記錄,最終在下一個runloop
的回調中生成動畫來響應本次屬性修改。因爲這個過程非開發者主動完成的,所以這種動畫被稱做隱式動畫
測試
CAAction
的子類
若是返回的是CAAction
對象,會直接開始動畫來響應圖層屬性的修改。通常返回的對象多爲CABasicAnimation
類型,對象中包裝了動畫時長
、動畫初始/結束狀態
、動畫時間曲線
等關鍵信息。當CAAction
對象被返回時,會馬上執行動做來響應本次屬性修改
首先,隱式動畫
是相對於顯式動畫
而言的,屬於被動實現。因爲顯式動畫
是主動實現的,所以在實現這些動畫的時候,咱們會去考慮動畫是否流暢,動畫先後是否會有卡幀,也會不斷的運行來保證動畫效果如預期完成。而隱式動畫
多屬於系統本身完成的動畫效果,提供給咱們的可調試空間也很小,這也致使了開發者對它的重視不夠,從而阻礙了進一步深刻學習的可能性。
其次,和用戶直接進行交互的就是UI
元素。在發生卡幀、掉幀的性能問題時,用戶對靜止界面和動畫的感知是徹底不一樣的。即使只有1
幀頁面丟失,在動畫中也能輕易的被用戶捕捉。舉個例子,當用戶按下按鈕,應用推遲了一、2
幀纔開始跳轉。又或者是在界面跳轉時丟失幀數據,具體表現爲卡幀,此時用戶對於卡頓的感知是遠遠大於日常的,所以瞭解隱式動畫
過程當中如何發生卡頓是頗有必要的。
隱式動畫的修改最終由CATransaction
事務完成,它在主線程的runloop
註冊了一個監聽者,具體回調發生在before waiting
階段。在回調中會將全部implicit transactions
以動畫的形式展現。雖然蘋果文檔沒有明說具體的回調時機,但經過簡單的測試能夠定位transaction
的回調時間:經過註冊兩個runloop
監聽者,回調優先級分別設爲NSIntegerMax
和NSIntegerMin
,監控最先和最晚的回調階段,而且在對應位置添加斷點,查看斷點先後圖層是否更新:
- (void)viewDidLoad {
[super viewDidLoad];
CFRunLoopObserverContext ctx = { 0, (__bridge void *)self, NULL, NULL };
CFRunLoopObserverRef allActivitiesObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, NSIntegerMin, &__runloop_callback, &ctx);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), allActivitiesObserver, kCFRunLoopCommonModes);
CFRunLoopObserverRef beforeWaitingObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, NSIntegerMax, &__runloop_before_waiting_callback, &ctx);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), beforeWaitingObserver, kCFRunLoopCommonModes);
}
複製代碼
手動建立的CALayer
在屬性修改會產生隱式動畫,將layer
增長到視圖層級上後,點擊按鈕來修改它的transform
屬性,而且觀察斷點先後的效果:
self.circle = [CAShapeLayer layer];
self.circle.delegate = self;
self.circle.anchorPoint = CGPointMake(0.5, 0.5);
self.circle.fillColor = [UIColor orangeColor].CGColor;
self.circle.path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(CGRectGetMidX([UIScreen mainScreen].bounds) - 50, 80, 100, 100)].CGPath;
[self.view.layer addSublayer: self.circle];
複製代碼
經過斷點和界面顯示能夠看到在Before Waiting
階段的兩次回調之間,transaction
完成了屬性修改的渲染任務(在DEBUG+斷點
狀態下,隱式動畫不能很好的完成動畫效果):
經過上面的測試能夠肯定transaction
的事務處理確實發生在before waiting
階段。但因爲註冊observer
時傳入的優先級能夠影響回調順序,爲了排除回調順序可能對測試的干擾,能夠經過hook
掉CFRunLoopAddObserver
這一註冊函數,來獲取已有的全部註冊before waiting
的回調信息:
void new_runloopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode) {
CFOptionFlags activities = CFRunLoopObserverGetActivities(observer);
if (activities & kCFRunLoopBeforeWaiting) {
CFRunLoopObserverContext context;
CFRunLoopObserverGetContext(observer, &context);
void *info = context.info;
NSLog(@"%d, %@", CFRunLoopObserverGetOrder(observer), (__bridge id)info);
}
origin_CFRunLoopAddObserver(rl, observer, mode);
}
複製代碼
運行後應用註冊了5
個包含before waiting
狀態的observer
,優先級分別是最小爲0
,最大爲2147483647
,也就是0 ~ 2^31-1
,處於NSIntegerMin
和NSIntergerMax
之間,足以肯定測試的正確性。
經過上面的測試,可知layer
的隱式動畫發生在before waiting
這一階段。那麼理論上來講,假如在兩個監聽回調之間發生了卡頓,應該會對動畫效果形成影響。另外,卡頓的時機可能也會影響動畫的效果。分別在transaction
的回調讓主線程進入休眠來測試不一樣時機的卡頓對動畫形成的效果,上面的測試證實了註冊的兩個已有回調能夠用於製做不一樣時機的卡頓:
NSLog(@"ready sleep");
[NSThread sleepForTimeInterval: 1];
NSLog(@"after sleep");
複製代碼
先於CATransaction
回調發生卡頓。點擊按鈕後,界面卡頓1s
,而後纔開始執行動畫。期間屢次點擊按鈕無效:
後於CATransaction
回調發生卡頓。點擊按鈕後,動畫馬上開始執行。界面會中止響應1s
,一樣卡頓期間不響應點擊。動畫存在卡幀現象,但不嚴重:
在transaction
先後製做卡頓確實產生了不一樣的效果,可是即使更換卡頓的時機,動畫效果還是比較流暢的,這證實了渲染、展現過程和主線程多是併發執行的。實際上在WWDC2014
的視頻中有對圖層渲染過程的詳細講述,隱式動畫
遵循這樣的渲染過程。圖層渲染過程分爲三個階段:
Commit Transaction + Decode
transaction
此時會將被修改的一個或者多個layer
的屬性統一計算,更新modelLayer
屬性,而後將圖層信息整合提交渲染服務進程。渲染服務進程反序列化獲取渲染樹信息,並準備開始渲染
Draw Calls + Render
渲染服務進程根據渲染樹信息,計算出動畫的幀數和圖層信息。此時GPU
利用渲染樹開始合成位圖並準備展現到屏幕上
Display
將渲染好的位圖信息展現到屏幕上,若是存在動畫則逐幀展現。若是在transaction
後發生卡頓,會對動畫展現形成必定的影響,但影響程度相對較低
結合渲染服務進程的工做流程,能夠知道實際上transaction
的工做是1
,在transaction
回調結束時已經將圖層樹提交給渲染服務進程了,所以以後即使主線程發生卡頓,也不會影響渲染服務進程的工做。而早於transaction
回調發生的卡頓會致使應用不能將圖層樹及時的提交到渲染服務進程,從而形成了動畫開始前的界面停滯現象。
說完了隱式動畫
如何開始、瓶頸等信息,對應的也理當說說顯式動畫
。雖然直接響應屬性修改是顯式動畫
的最大特色,但經過已有的測試能夠直接證實這一點。修改CALayerDelegate
的代理方法,主動返回一個CABasicAnimation
對象:
#pragma mark - CALayerDelegate
- (id<CAAction>)actionForLayer: (CALayer *)layer forKey: (NSString *)event {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: event];
CGFloat randomScale = (arc4random() % 20 + 1) * 0.1;
animation.toValue = [NSValue valueWithCATransform3D: CATransform3DScale(CATransform3DIdentity, randomScale, randomScale, 1)];
return animation;
}
複製代碼
一樣添加斷點進行測試,運行後能夠看到在動畫開始以後,斷點纔會停下來。也能夠肯定雖然transaction
雖然也負責了顯式動畫
的渲染事務,但會當即commit
到渲染服務進程響應屬性修改。
默認的轉場動畫實際上也是由transaction
來完成的,屬於隱式動畫。經過hook
掉獲取CAAction
的代理方法,在忽略掉nil
和NSNull
的無效返回值後,一個push
跳轉動畫總共涉及到了三個CAAction
子類。從類名上來看_UIViewAdditiveAnimationAction
是和轉場動畫關聯最密切的子類,也證實了系統默認的轉場跳轉實際上也是交給了transaction
機制來處理的。另外從log
上的執行來看,轉場實際上也屬於隱式動畫
:
轉場卡頓從效果上看能分爲轉場前卡頓
和轉場後卡頓
,後者屬於常見的的轉場性能瓶頸,大多因爲新界面視圖層級複雜、大量IO
等工做致使,是最容易定位的一類問題。而前者屬於少見,且不容易定位的卡頓現象之一。結合上面的測試,若是發生了轉場前卡頓
,那麼說明渲染工做在1
開始以前就發生了卡頓。
在上面的log
中能夠看到viewDidLoad
和viewWillAppear
的調用一樣處在before waiting
階段。假設這兩個方法的調用時機在transaction
前面,那麼一旦兩個方法發生了卡頓,確定會跳轉動畫卡幀後執行的效果。經過分別在兩個方法中添加sleep
操做測試,還原了gif
的卡頓效果。所以能夠得出轉場動畫過程當中的流程:
view did load --> view will appear --> CATransaction callback --> animate
複製代碼
雖然蘋果文檔和測試結果都說明了一件事情:transaction
的回調處在before waiting
階段,可是否存在可能:runloop
沒法進入before waiting
呢?實際上這種多是徹底存在的,根據蘋果文檔中的描述,下圖能夠用來表示runloop
的內部邏輯:
假如runloop
中一直有source1
事件,那麼會一直在二、三、四、五、9
之間循環處理。而touches
發生時,就是典型的持續source1
事件環境。換句話說,若是用戶一直在滾動列表,那麼before waiting
將不會到來。但實際在應用使用中,即使是手指不離開屏幕,cell
依舊可以展現各類動畫。所以能夠推斷出transaction
至少還註冊了UITracking
這個模式下的runloop
監聽處理,感興趣的同窗能夠在滾動列表上採用相似的手段測試具體的處理時機。
因爲隱式動畫的特殊性質,咱們與之打交道的地方基本在頁面跳轉
環節,一旦這個過程發生了卡頓,不管是跳轉前卡頓
或者是跳轉後卡頓
,都會使得應用的體驗大打折扣。總結了一下,在平常開發中,咱們與隱式動畫打交道時記住幾點:
隱式動畫開始前的卡頓是由於CATransaction
回調前其餘任務佔用了大量的CPU
資源,經過懶加載、延後加載、異步執行能夠有效的避免這個問題
viewDidLoad
和viewWillAppear
是一丘之貉,它們都會致使轉場動畫前的卡頓。因此若是你將前者的工做放到後者執行,並無任何做用
動畫在開始以後,即使是應用發生卡頓,對動畫的影響也要低於先於transaction
的卡頓。所以若是你不知道如何優化動畫前的爛攤子,那麼放到動畫開始以後吧