隱式動畫的性能瓶頸

原文連接html

隱式動畫實現的背後體現了核心動畫精心設計的許多機制。在layer的屬性發生改變以後,會向它的代理方請求一個CAAction行爲來完成後續的工做,系統容許代理方返回nil指針。一旦這麼作,修改屬性的工做最終移交給CATransaction處理,由修改的屬性值決定是否自動生成一個CABasicAnimation。若是知足,此時隱式動畫將被觸發。併發

關於CATransaction

在覈心動畫中,每一個圖層的修改都是事務CATransaction的一部分,它能夠同時對多個layer的屬性進行修改,而後成批的將將多個圖層樹包裝起來,一次性發送到渲染服務進程。CATransaction事務對象被分爲implicitexplicit兩種類型,分別對應隱式顯式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];
複製代碼

layer如何實現動畫

圖層屬性被修改時,會朝着本身的代理對象請求一個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監聽者,回調優先級分別設爲NSIntegerMaxNSIntegerMin,監控最先和最晚的回調階段,而且在對應位置添加斷點,查看斷點先後圖層是否更新:

- (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時傳入的優先級能夠影響回調順序,爲了排除回調順序可能對測試的干擾,能夠經過hookCFRunLoopAddObserver這一註冊函數,來獲取已有的全部註冊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,處於NSIntegerMinNSIntergerMax之間,足以肯定測試的正確性。

隱式動畫的性能瓶頸

經過上面的測試,可知layer的隱式動畫發生在before waiting這一階段。那麼理論上來講,假如在兩個監聽回調之間發生了卡頓,應該會對動畫效果形成影響。另外,卡頓的時機可能也會影響動畫的效果。分別在transaction的回調讓主線程進入休眠來測試不一樣時機的卡頓對動畫形成的效果,上面的測試證實了註冊的兩個已有回調能夠用於製做不一樣時機的卡頓:

NSLog(@"ready sleep");
[NSThread sleepForTimeInterval: 1];
NSLog(@"after sleep");
複製代碼

先於CATransaction回調發生卡頓。點擊按鈕後,界面卡頓1s,而後纔開始執行動畫。期間屢次點擊按鈕無效:

後於CATransaction回調發生卡頓。點擊按鈕後,動畫馬上開始執行。界面會中止響應1s,一樣卡頓期間不響應點擊。動畫存在卡幀現象,但不嚴重:

transaction先後製做卡頓確實產生了不一樣的效果,可是即使更換卡頓的時機,動畫效果還是比較流暢的,這證實了渲染、展現過程和主線程多是併發執行的。實際上在WWDC2014的視頻中有對圖層渲染過程的詳細講述,隱式動畫遵循這樣的渲染過程。圖層渲染過程分爲三個階段:

  1. Commit Transaction + Decode

    transaction此時會將被修改的一個或者多個layer的屬性統一計算,更新modelLayer屬性,而後將圖層信息整合提交渲染服務進程。渲染服務進程反序列化獲取渲染樹信息,並準備開始渲染

  2. Draw Calls + Render

    渲染服務進程根據渲染樹信息,計算出動畫的幀數和圖層信息。此時GPU利用渲染樹開始合成位圖並準備展現到屏幕上

  3. 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的代理方法,在忽略掉nilNSNull的無效返回值後,一個push跳轉動畫總共涉及到了三個CAAction子類。從類名上來看_UIViewAdditiveAnimationAction是和轉場動畫關聯最密切的子類,也證實了系統默認的轉場跳轉實際上也是交給了transaction機制來處理的。另外從log上的執行來看,轉場實際上也屬於隱式動畫

轉場卡頓從效果上看能分爲轉場前卡頓轉場後卡頓,後者屬於常見的的轉場性能瓶頸,大多因爲新界面視圖層級複雜、大量IO等工做致使,是最容易定位的一類問題。而前者屬於少見,且不容易定位的卡頓現象之一。結合上面的測試,若是發生了轉場前卡頓,那麼說明渲染工做在1開始以前就發生了卡頓。

在上面的log中能夠看到viewDidLoadviewWillAppear的調用一樣處在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資源,經過懶加載、延後加載、異步執行能夠有效的避免這個問題

  • viewDidLoadviewWillAppear是一丘之貉,它們都會致使轉場動畫前的卡頓。因此若是你將前者的工做放到後者執行,並無任何做用

  • 動畫在開始以後,即使是應用發生卡頓,對動畫的影響也要低於先於transaction的卡頓。所以若是你不知道如何優化動畫前的爛攤子,那麼放到動畫開始以後吧

關注個人公衆號獲取更新信息
相關文章
相關標籤/搜索