Core Animation动画的不同阶段
之前在阅读Core Animation Programming Guide的时候写过一篇很短的总结;内容不多,主要用来整理一些当时吸收到的知识。最近,又把文档看了一遍,而且开始逐步做了翻译,有了更多新的体会。另外,WWDC这个关于Optimizing 2D Graphics and Animation Performance中对于还介绍了Core Animation
在动画之前的各个阶段准备。
动画不同的阶段和动画性能的调优密不可分。
这篇博客主要是对上篇博客的补充,内容基本都来自于上面提到的WWDC的视频前面部分;另外自己使用了一个简单的Demo和对这个Demo的profile数据来验证视频内容。
动画的阶段
首先动画的整个过程可以分为大的三个阶段:
- 创建动画和更新视图层级(create animation and update view hierachy)
- 准备和提交动画(prepare and commit animation)
- 渲染每一帧(render each frame)
实际上,对于开发者而言,这三个大的阶段中的后两个,我们都是无法直接干预的。阶段二,由Core Animation
代劳;阶段三是通过进程通信IPC - Inter-Process Communication
将图层数据给到了另外一个render server
的线程去渲染绘制的。这个render server
据说在iOS 6之前叫SpringBoard
,之后叫BackBoard
(参见iOS Core Animation: Advanced Techniques - chapter 12)。
对于上面提到的前面两个阶段,可以通过Time Profiler
在动画过程中对call stack
的采样,来将这个过程更加具体的细分为四个步骤。
WWDCOptimizing 2D Graphics and Animation Performance使用的Demo动画为:
我们将这段代码整理到Demo中:
- (void)startTestAnimation {
//used to profile core animation's possible call stack trace.
NSBundle *mainBundle = [NSBundle mainBundle];
UINib *viewNib = [UINib nibWithNibName:@"JWAnimationTestView" bundle:mainBundle];
NSArray *views = [viewNib instantiateWithOwner:nil options:nil];
JWAnimationTestView *animationView = [views firstObject];
animationView.frame = CGRectMake(0, 0, 300, 500);
animationView.backgroundColor = [UIColor brownColor];
animationView.transform = CGAffineTransformMakeScale(0.01, 0.01);
[UIView animateWithDuration:3
animations:
^{
[self.view addSubview:animationView];
animationView.transform = CGAffineTransformIdentity;
}
completion:
^(BOOL finished) {
[animationView removeFromSuperview];
}];
}
需要特别指出的是JWAnimationTestView
的视图结构中,包括了一个有文字的UILabel
和有图片的UIImageView
。因为文字和图片的存在,会调用相应的文字绘制和将图片处理设置给layer
的contents
属性这些操作,所以这样的设定是为了能够看到完整的call stack
。
另外,我的Demo代码中使用了一个重复调用的timer以5.f秒的时间间隔调用startTestAnimation
方法。这样做是为了让time profiler
能够采样到足够多的call stack
数据,我自己在验证的过程中,让动画运行了10次。
上面红框部分是时钟方法的重复调用:创建视图,开始动画。
下面部分是Core Animation
在将图层数据和相关信息提交给render server
之前所做的事情。你会发现Core Animation
是注册了主线程runloop
的观察者,在一次runloop
的结束的时候,runloop
会回调Core Animation
的观察者,让它执行相应的layout和显示的方法;实际上,我们通常对某个视图的图层属性所做的修改,都是在当前runloop
结束之前,Core Animation
才将修改提交。(这是不是为什么不能在子线程上去做UI相关的工作的原因?因为Core Animation
的提交是在主线程runloop
上进行的)。
time profiler
的call statck以1ms左右频率采样本身可能会漏采样执行时间很短方法;另外,WWDC中用于动画的视图结构和我Demo中使用的也可能不同。因此我获得的call stack
和WWDC中展示的有些不同。
WWDC中展示的call stack采样:
当中有一个很大的区别:
在wwdc展示的call stack中,在CA::Context::commit_transaction(CA::Transaction*)
方法下级,有下列几个过程:
CA::Layer::layout_and_display_if_needed(CA::Transaction*)
代表layout和dispaly两个步骤:CA::Layer::layout_if_needed(CA::Transaction*)
layout过程CA::Layer::display_if_needed(CA::Transaction*)
dispaly过程
CA::Layer::prepare_commit(CA::Transaction*)
提交准备阶段CA::Layer::commit_if_needed(CA::Transaction*, void (*)(CA::Layer*, unsigned int, unsigned int, void*), void*)
提交阶段
在我们的call stack中是看不到CA::Layer::layout_and_display_if_needed(CA::Transaction*)
,而是由CA::Context::commit_transaction(CA::Transaction*)
直接调用的CA::Layer::layout_if_needed(CA::Transaction*)
以及[CALayer _display]
。按照Time Profiler
的采样原理,如果能够采样到下级方法的调用,那么该方法的caller一定是也能够采样到的。在我们的call stack中,既然有CA::Layer::layout_if_needed(CA::Transaction*)
,但是没有CA::Layer::layout_and_display_if_needed(CA::Transaction*)
是不是说明Core Animation
内部的代码结构已经不一样了?毕竟WWDC是2012年的。
不管怎样,从我们的call stack中也能够看到这四个完整的过程。
结合WWDC,以我们的call stack为例,来说明这四个过程分别大概都做了什么。
layout过程
从上面layout的过程可以看出,其所做的主要任务就是将图层调用代理(也就是视图)实现整个视图层级的布局;比较有意思的是,autolayout
的约束也是在这个时候更新和施加apply
的(-[UIView(Hierarchy) _updateConstraintsAsNecessaryAndApplyLayoutFromEngine]
)。
display过程
按照WWDC视频的说法,display
这个阶段是负责draw图层的内容。如果你的某些视图实现了drawRect:
方法或者其他UIKit提供的视图采用了图层的drawInContext:
方法来提供内容的,比如这里的UILabel的实现,那么也会在这个阶段去调用。从这个阶段的工作来讲,其是CPU bound
(受限于CPU性能)的。
prepare过程
prepare过程也是Core Animation
准备图层内容的环节,但是这个过程的准备是给那些没有实现drawInContext:
(视频说的是drawRect
,但实际上drawRect
的底层是图层的drawInContext
)而通过其他方式设置图层内容的视图准备的;比如,UIImageView,它的实现就是直接解码压缩格式的png图,设置给其layer的contents属性的。那么图片解码的过程就在这个prepare阶段。
commit过程
commit阶段是讲所有的图层数据打包,通过进程间通信给到render server(另外一个进程)来绘制显示到屏幕上。通常情况下,CPU的工作在这个阶段是比较少的-从上面的10次重复动画来看,这个过程仅仅分担了1ms左右的cpu时间。但是WWDC指出,如果你的视图层级过于复杂,有非常多的图层,那这个commit的过程也会耗费比较长的时间。