隐式动画的性能瓶颈

原文连接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的卡顿。所以若是你不知道如何优化动画前的烂摊子,那么放到动画开始以后吧

关注个人公众号获取更新信息
相关文章
相关标签/搜索