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