我能够指导你,可是你必须按照我说的作。 -- 骇客帝国面试
在第10章“缓冲”中,咱们研究了CAMediaTimingFunction
,它是一个经过控制动画缓冲来模拟物理效果例如加速或者减速来加强现实感的东西,那么若是想更加真实地模拟物理交互或者实时根据用户输入修改动画改怎么办呢?在这一章中,咱们将继续探索一种可以容许咱们精确地控制一帧一帧展现的基于定时器的动画。数据库
动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展现像素的时候并不能作到这一点。通常来讲这种显示都没法作到连续的移动,能作的仅仅是足够快地展现一系列静态图片,只是看起来像是作了运动。编程
咱们以前提到过iOS按照每秒60次刷新屏幕,而后CAAnimation
计算出须要展现的新的帧,而后在每次屏幕更新的时候同步绘制上去,CAAnimation
最机智的地方在于每次刷新须要展现的时候去计算插值和缓冲。数组
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。缓存
在第10章中,咱们解决了如何自定义缓冲函数,而后根据须要展现的帧的数组来告诉CAKeyframeAnimation
的实例如何去绘制。全部的Core Animation实际上都是按照必定的序列来显示这些帧,那么咱们能够本身作到这些么?性能优化
NSTimer
实际上,咱们在第三章“图层几何学”中已经作过相似的东西,就是时钟那个例子,咱们用了NSTimer
来对钟表的指针作定时动画,一秒钟更新一次,可是若是咱们把频率调整成一秒钟更新60次的话,原理是彻底相同的。服务器
咱们来试着用NSTimer
来修改第十章中弹性球的例子。因为如今咱们在定时器启动以后连续计算动画帧,咱们须要在类中添加一些额外的属性来存储动画的fromValue
,toValue
,duration
和当前的timeOffset
(见清单11.1)。网络
清单11.1 使用NSTimer
实现弹性球动画多线程
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, assign) NSTimeInterval duration; @property (nonatomic, assign) NSTimeInterval timeOffset; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add ball image view UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; self.ballView = [[UIImageView alloc] initWithImage:ballImage]; [self.containerView addSubview:self.ballView]; //animate [self animate]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //replay animation on tap [self animate]; } float interpolate(float from, float to, float time) { return (to - from) * time + from; } - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time { if ([fromValue isKindOfClass:[NSValue class]]) { //get type const char *type = [(NSValue *)fromValue objCType]; if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint from = [fromValue CGPointValue]; CGPoint to = [toValue CGPointValue]; CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); return [NSValue valueWithCGPoint:result]; } } //provide safe default implementation return (time < 0.5)? fromValue: toValue; } float bounceEaseOut(float t) { if (t < 4/11.0) { return (121 * t * t)/16.0; } else if (t < 8/11.0) { return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; } else if (t < 9/10.0) { return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; } return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; } - (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; } - (void)step:(NSTimer *)step { //update time offset self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } } @end
很赞,并且和基于关键帧例子的代码同样不少,可是若是想一次性在屏幕上对不少东西作动画,很明显就会有不少问题。app
NSTimer
并非最佳方案,为了理解这点,咱们须要确切地知道NSTimer是如何工做的。iOS上的每一个线程都管理了一个NSRunloop,字面上看就是经过一个循环来完成一些任务列表。可是对主线程,这些任务包含以下几项:
处理触摸事件
发送和接受网络数据包
执行使用gcd的代码
处理计时器行为
屏幕重绘
当你设置一个NSTimer
,他会被插入到当前任务列表中,而后直到指定时间过去以后才会被执行。可是什么时候启动定时器并无一个时间上限,并且它只会在列表中上一个任务完成以后开始执行。这一般会致使有几毫秒的延迟,可是若是上一个任务过了好久才完成就会致使延迟很长一段时间。
屏幕重绘的频率是一秒钟六十次,可是和定时器行为同样,若是列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,因而就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘以后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,因而动画看起来就跳动了。
咱们能够经过一些途径来优化:
咱们能够用CADisplayLink
让更新频率严格控制在每次屏幕刷新以后。
基于真实帧的持续时间而不是假设的更新频率来作动画。
调整动画计时器的run loop
模式,这样就不会被别的事件干扰。
CADisplayLink
是CoreAnimation提供的另外一个相似于NSTimer
的类,它老是在屏幕完成一次更新以前启动,它的接口设计的和NSTimer
很相似,因此它实际上就是一个内置实现的替代,可是和timeInterval
以秒为单位不一样,CADisplayLink
有一个整型的frameInterval
属性,指定了间隔多少帧以后才执行。默认值是1,意味着每次屏幕更新以前都会执行一次。可是若是动画的代码执行起来超过了六十分之一秒,你能够指定frameInterval
为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。
用CADisplayLink
而不是NSTimer
,会保证帧率足够连续,使得动画看起来更加平滑,但即便CADisplayLink
也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会致使动画偶尔地丢帧。当使用NSTimer
的时候,一旦有机会计时器就会开启,可是CADisplayLink
却不同:若是它丢失了帧,就会直接忽略它们,而后在下一次更新的时候接着运行。
不管是使用NSTimer
仍是CADisplayLink
,咱们仍然须要处理一帧的时间超出了预期的六十分之一秒。因为咱们不可以计算出一帧真实的持续时间,因此须要手动测量。咱们能够在每帧开始刷新的时候用CACurrentMediaTime()
记录当前时间,而后和上一帧记录的时间去比较。
经过比较这些时间,咱们就能够获得真实的每帧持续的时间,而后代替硬编码的六十分之一秒。咱们来更新一下上个例子(见清单11.2)。
清单11.2 经过测量没帧持续的时间来使得动画更加平滑
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) CADisplayLink *timer; @property (nonatomic, assign) CFTimeInterval duration; @property (nonatomic, assign) CFTimeInterval timeOffset; @property (nonatomic, assign) CFTimeInterval lastStep; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue; @end @implementation ViewController ... - (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } - (void)step:(CADisplayLink *)timer { //calculate time delta CFTimeInterval thisStep = CACurrentMediaTime(); CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep; //update time offset self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } } @end
注意到当建立CADisplayLink
的时候,咱们须要指定一个run loop
和run loop mode
,对于run loop来讲,咱们就使用了主线程的run loop,由于任何用户界面的更新都须要在主线程执行,可是模式的选择就并不那么清楚了,每一个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,并且当UI很活跃的时候的确会暂停一些别的任务。
一个典型的例子就是当是用UIScrollview
滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,因此标准的NSTimer
和网络请求就不会启动,一些常见的run loop模式以下:
NSDefaultRunLoopMode
- 标准优先级
NSRunLoopCommonModes
- 高优先级
UITrackingRunLoopMode
- 用于UIScrollView和别的控件的动画
在咱们的例子中,咱们是用了NSDefaultRunLoopMode
,可是不能保证动画平滑的运行,因此就能够用NSRunLoopCommonModes
来替代。可是要当心,由于若是动画在一个高帧率状况下运行,你会发现一些别的相似于定时器的任务或者相似于滑动的其余iOS动画会暂停,直到动画结束。
一样能够同时对CADisplayLink
指定多个run loop模式,因而咱们能够同时加入NSDefaultRunLoopMode
和UITrackingRunLoopMode
来保证它不会被滑动打断,也不会被其余UIKit控件动画影响性能,像这样:
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
和CADisplayLink
相似,NSTimer
一样也可使用不一样的run loop模式配置,经过别的函数,而不是+scheduledTimerWithTimeInterval:
构造器
self.timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
即便使用了基于定时器的动画来复制第10章中关键帧的行为,但仍是会有一些本质上的区别:在关键帧的实现中,咱们提早计算了全部帧,可是在新的解决方案中,咱们实际上实在按须要在计算。意义在于咱们能够根据用户输入实时修改动画的逻辑,或者和别的实时动画系统例如物理引擎进行整合。
咱们来基于物理学建立一个真实的重力模拟效果来取代当前基于缓冲的弹性动画,但即便模拟2D的物理效果就已近极其复杂了,因此就不要尝试去实现它了,直接用开源的物理引擎库好了。
咱们将要使用的物理引擎叫作Chipmunk。另外的2D物理引擎也一样能够(例如Box2D),可是Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合。Chipmunk有不少版本,包括一个和Objective-C绑定的“indie”版本。C语言的版本是免费的,因此咱们就用它好了。在本书写做的时候6.1.4是最新的版本;你能够从http://chipmunk-physics.net下载它。
Chipmunk完整的物理引擎至关巨大复杂,可是咱们只会使用以下几个类:
cpSpace
- 这是全部的物理结构体的容器。它有一个大小和一个可选的重力矢量
cpBody
- 它是一个固态无弹力的刚体。它有一个坐标,以及其余物理属性,例如质量,运动和摩擦系数等等。
cpShape
- 它是一个抽象的几何形状,用来检测碰撞。能够给结构体添加一个多边形,并且cpShape
有各类子类来表明不一样形状的类型。
在例子中,咱们来对一个木箱建模,而后在重力的影响下下落。咱们来建立一个Crate
类,包含屏幕上的可视效果(一个UIImageView
)和一个物理模型(一个cpBody
和一个cpPolyShape
,一个cpShape
的多边形子类来表明矩形木箱)。
用C版本的Chipmunk
会带来一些挑战,由于它如今并不支持Objective-C的引用计数模型,因此咱们须要准确的建立和释放对象。为了简化,咱们把cpShape
和cpBody
的生命周期和Crate
类进行绑定,而后在木箱的-init
方法中建立,在-dealloc
中释放。木箱物理属性的配置很复杂,因此阅读了Chipmunk
文档会颇有意义。
视图控制器用来管理cpSpace
,还有和以前同样的计时器逻辑。在每一步中,咱们更新cpSpace
(用来进行物理计算和全部结构体的从新摆放)而后迭代对象,而后再更新咱们的木箱视图的位置来匹配木箱的模型(在这里,实际上只有一个结构体,可是以后咱们将要添加更多)。
Chipmunk使用了一个和UIKit颠倒的坐标系(Y轴向上为正方向)。为了使得物理模型和视图之间的同步更简单,咱们须要经过使用geometryFlipped
属性翻转容器视图的集合坐标(第3章中有提到),因而模型和视图都共享一个相同的坐标系。
具体的代码见清单11.3。注意到咱们并无在任何地方释放cpSpace
对象。在这个例子中,内存空间将会在整个app的生命周期中一直存在,因此这没有问题。可是在现实世界的场景中,咱们须要像建立木箱结构体和形状同样去管理咱们的空间,封装在标准的Cocoa对象中,而后来管理Chipmunk对象的生命周期。图11.1展现了掉落的木箱。
清单11.3 使用物理学来对掉落的木箱建模
#import "ViewController.h" #import #import "chipmunk.h" @interface Crate : UIImageView @property (nonatomic, assign) cpBody *body; @property (nonatomic, assign) cpShape *shape; @end @implementation Crate #define MASS 100 - (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { //set image self.image = [UIImage imageNamed:@"Crate.png"]; self.contentMode = UIViewContentModeScaleAspectFill; //create the body self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height)); //create the shape cpVect corners[] = { cpv(0, 0), cpv(0, frame.size.height), cpv(frame.size.width, frame.size.height), cpv(frame.size.width, 0), }; self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2)); //set shape friction & elasticity cpShapeSetFriction(self.shape, 0.5); cpShapeSetElasticity(self.shape, 0.8); //link the crate to the shape //so we can refer to crate from callback later on self.shape->data = (__bridge void *)self; //set the body position to match view cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2)); } return self; } - (void)dealloc { //release shape and body cpShapeFree(_shape); cpBodyFree(_body); } @end @interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, assign) cpSpace *space; @property (nonatomic, strong) CADisplayLink *timer; @property (nonatomic, assign) CFTimeInterval lastStep; @end @implementation ViewController #define GRAVITY 1000 - (void)viewDidLoad { //invert view coordinate system to match physics self.containerView.layer.geometryFlipped = YES; //set up physics space self.space = cpSpaceNew(); cpSpaceSetGravity(self.space, cpv(0, -GRAVITY)); //add a crate Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)]; [self.containerView addSubview:crate]; cpSpaceAddBody(self.space, crate.body); cpSpaceAddShape(self.space, crate.shape); //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } void updateShape(cpShape *shape, void *unused) { //get the crate object associated with the shape Crate *crate = (__bridge Crate *)shape->data; //update crate view position and angle to match physics shape cpBody *body = shape->body; crate.center = cpBodyGetPos(body); crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body)); } - (void)step:(CADisplayLink *)timer { //calculate step duration CFTimeInterval thisStep = CACurrentMediaTime(); CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep; //update physics cpSpaceStep(self.space, stepDuration); //update all the shapes cpSpaceEachShape(self.space, &updateShape, NULL); } @end
图11.1 真实引力场下的木箱交互
对于实现动画的缓冲效果来讲,计算每帧持续的时间是一个很好的解决方案,可是对模拟物理效果并不理想。经过一个可变的时间步长来实现有着两个弊端:
若是时间步长不是固定的,精确的值,物理效果的模拟也就随之不肯定。这意味着即便是传入相同的输入值,也可能在不一样场合下有着不一样的效果。有时候没多大影响,可是在基于物理引擎的游戏下,玩家就会因为相同的操做行为致使不一样的结果而感到困惑。一样也会让测试变得麻烦。
因为性能故常形成的丢帧或者像电话呼入的中断均可能会形成不正确的结果。考虑一个像子弹那样快速移动物体,每一帧的更新都须要移动子弹,检测碰撞。若是两帧之间的时间加长了,子弹就会在这一步移动更远的距离,穿过围墙或者是别的障碍,这样就丢失了碰撞。
咱们想获得的理想的效果就是经过固定的时间步长来计算物理效果,可是在屏幕发生重绘的时候仍然可以同步更新视图(可能会因为在咱们控制范围以外形成不可预知的效果)。
幸运的是,因为咱们的模型(在这个例子中就是Chipmunk的cpSpace
中的cpBody
)被视图(就是屏幕上表明木箱的UIView
对象)分离,因而就很简单了。咱们只须要根据屏幕刷新的时间跟踪时间步长,而后根据每帧去计算一个或者多个模拟出来的效果。
咱们能够经过一个简单的循环来实现。经过每次CADisplayLink
的启动来通知屏幕将要刷新,而后记录下当前的CACurrentMediaTime()
。咱们须要在一个小增量中提早重复物理模拟(这里用120分之一秒)直到遇上显示的时间。而后更新咱们的视图,在屏幕刷新的时候匹配当前物理结构体的显示位置。
清单11.5展现了固定时间步长版本的代码
清单11.5 固定时间步长的木箱模拟
#define SIMULATION_STEP (1/120.0) - (void)step:(CADisplayLink *)timer { //calculate frame step duration CFTimeInterval frameTime = CACurrentMediaTime(); //update simulation while (self.lastStep < frameTime) { cpSpaceStep(self.space, SIMULATION_STEP); self.lastStep += SIMULATION_STEP; }  //update all the shapes cpSpaceEachShape(self.space, &updateShape, NULL); }
当使用固定的模拟时间步长时候,有一件事情必定要注意,就是用来计算物理效果的现实世界的时间并不会加速模拟时间步长。在咱们的例子中,咱们随意选择了120分之一秒来模拟物理效果。Chipmunk很快,咱们的例子也很简单,因此cpSpaceStep()
会完成的很好,不会延迟帧的更新。
可是若是场景很复杂,好比有上百个物体之间的交互,物理计算就会很复杂,cpSpaceStep()
的计算也可能会超出1/120秒。咱们没有测量出物理步长的时间,由于咱们假设了相对于帧刷新来讲并不重要,可是若是模拟步长更久的话,就会延迟帧率。
若是帧刷新的时间延迟的话会变得很糟糕,咱们的模拟须要执行更多的次数来同步真实的时间。这些额外的步骤就会继续延迟帧的更新,等等。这就是所谓的死亡螺旋,由于最后的结果就是帧率变得愈来愈慢,直到最后应用程序卡死了。
咱们能够经过添加一些代码在设备上来对物理步骤计算真实世界的时间,而后自动调整固定时间步长,可是实际上它不可行。其实只要保证你给容错留下足够的边长,而后在指望支持的最慢的设备上进行测试就能够了。若是物理计算超过了模拟时间的50%,就须要考虑增长模拟时间步长(或者简化场景)。若是模拟时间步长增长到超过1/60秒(一个完整的屏幕更新时间),你就须要减小动画帧率到一秒30帧或者增长CADisplayLink
的frameInterval
来保证不会随机丢帧,否则你的动画将会看起来不平滑。
代码应该运行的尽可能快,而不是更快 - 理查德
在第一和第二部分,咱们了解了Core Animation提供的关于绘制和动画的一些特性。Core Animation功能和性能都很是强大,但若是你对背后的原理不清楚的话也会下降效率。让它达到最优的状态是一门艺术。在这章中,咱们将探究一些动画运行慢的缘由,以及如何去修复这些问题。
关于绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器)。在现代iOS设备中,都有能够运行不一样软件的可编程芯片,可是因为历史缘由,咱们能够说CPU所作的工做都在软件层面,而GPU在硬件层面。
总的来讲,咱们能够用软件(使用CPU)作任何事情,可是对于图像处理,一般用硬件会更快,由于GPU使用图像对高度并行浮点运算作了优化。因为某些缘由,咱们想尽量把屏幕渲染的工做交给硬件去处理。问题在于GPU并无无限制处理性能,并且一旦资源用完的话,性能就会开始降低了(即便CPU并无彻底占用)
大多数动画性能优化都是关于智能利用GPU和CPU,使得它们都不会超出负荷。因而咱们首先须要知道Core Animation是如何在这两个处理器之间分配工做的。
Core Animation处在iOS的核心地位:应用内和应用间都会用到它。一个简单的动画可能同步显示多个app的内容,例如当在iPad上多个程序之间使用手势切换,会使得多个程序同时显示在屏幕上。在一个特定的应用中用代码实现它是没有意义的,由于在iOS中不可能实现这种效果(App都是被沙箱管理,不能访问别的视图)。
动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。在iOS5和以前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6以后的版本中叫作BackBoard
。
当运行一段动画时候,这个过程会被四个分离的阶段被打破:
布局 - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
显示 - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:
和-drawLayer:inContext:
方法的调用路径。
准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程当中将要显示的图片的时间点。
提交 - 这是最后的阶段,Core Animation打包全部图层和动画属性,而后经过IPC(内部处理通讯)发送到渲染服务进行显示。
可是这些仅仅阶段仅仅发生在你的应用程序以内,在动画在屏幕上显示以前仍然有更多的工做。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来造成另外一个叫作渲染树的图层树(在第一章“图层树”中提到过)。使用这个树状结构,渲染服务对动画的每一帧作出以下工做:
对全部的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
在屏幕上渲染可见的三角形
因此一共有六个阶段;最后两个阶段在动画过程当中不停地重复。前五个阶段都在软件层面处理(经过CPU),只有最后一个被GPU执行。并且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。
这并非个问题,由于在布局和显示阶段,你能够决定哪些由CPU执行,哪些交给GPU去作。那么改如何判断呢?
GPU为一个具体的任务作了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合而后把它们输送到屏幕上。现代iOS设备上可编程的GPU在这些操做的执行上又很大的灵活性,可是Core Animation并无暴露出直接的接口。除非你想绕开Core Animation并编写你本身的OpenGL着色器,从根本上解决硬件加速的问题,那么剩下的全部都仍是须要在CPU的软件层面上完成。
宽泛的说,大多数CALayer
的属性都是用GPU来绘制。好比若是你设置图层背景或者边框的颜色,那么这些能够经过着色的三角板实时绘制出来。若是对一个contents
属性设置一张图片,而后裁剪它 - 它就会被纹理的三角形绘制出来,而不须要软件层面作任何绘制。
可是有一些事情会下降(基于GPU)图层绘制,好比:
太多的几何结构 - 这发生在须要太多的三角板来作变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片能够处理几百万个三角板,因此在Core Animation中几何结构并非GPU的瓶颈所在。但因为图层在显示以前经过IPC发送到渲染服务器的时候(图层其实是由不少小物体组成的特别重量级的对象),太多的图层就会引发CPU的瓶颈。这就限制了一次展现的图层个数(见本章后续“CPU相关操做”)。
重绘 - 主要由重叠的半透明图层引发。GPU的填充比率(用颜色填充像素的比率)是有限的,因此须要避免重绘(每一帧用相同的像素填充屡次)的发生。在现代iOS设备上,GPU都会应对重绘;即便是iPhone 3GS均可以处理高达2.5的重绘比率,并任然保持60帧率的渲染(这意味着你能够绘制一个半的整屏的冗余信息,而不影响性能),而且新设备能够处理更多。
离屏绘制 - 这发生在当不能直接在屏幕上绘制,而且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会下降GPU性能。对于特定图层效果的使用,好比圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提早渲染图层的离屏绘制。但这不意味着你须要避免使用这些效果,只是要明白这会带来性能的负面影响。
过大的图片 - 若是视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必需要用CPU在图层每次显示以前对图片预处理,一样也会下降性能。
大多数工做在Core Animation的CPU都发生在动画开始以前。这意味着它不会影响到帧率,因此很好,可是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。
如下CPU的操做都会延迟动画的开始时间:
布局计算 - 若是你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤其明显,它应该是比老版的自动调整逻辑增强了CPU的工做。
视图懒加载 - iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间颇有好处,可是当呈现到屏幕上以前,按下按钮致使的许多工做都会不能被及时响应。好比控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操做”),都会比CPU正常操做慢得多。
Core Graphics绘制 - 若是对视图实现了-drawRect:
方法,或者CALayerDelegate
的-drawLayer:inContext:
方法,那么在绘制任何东西以前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须建立一个内存中等大小的寄宿图片。而后一旦绘制结束以后,必须把图片数据经过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,因此在一个对性能十分挑剔的场景下这样作十分很差。
解压图片 - PNG或者JPEG压缩以后的图片文件会比同质量的位图小得多。可是在图片绘制到屏幕上以前,必须把它扩展成完整的未解压的尺寸(一般等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS一般直到真正绘制的时候才去解码图片(14章“图片IO”会更详细讨论)。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView
)或者把它绘制到Core Graphics中,都须要对它解压,这样的话,对于一个较大的图片,都会占用必定的时间。
当图层被成功打包,发送到渲染服务器以后,CPU仍然要作以下工做:为了显示屏幕上的图层,Core Animation必须对渲染树种的每一个可见图层经过OpenGL循环转换成纹理三角板。因为GPU并不知晓Core Animation图层的任何结构,因此必需要由CPU作这些事情。这里CPU涉及的工做和图层个数成正比,因此若是在你的层级关系中有太多的图层,就会致使CPU没一帧的渲染,即便这些事情不是你的应用程序可控的。
还有一项没涉及的就是IO相关工做。上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能须要从山村(甚至是远程URL)来加载。一个典型的例子就是两个视图控制器之间的过渡效果,这就须要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大,须要动态滚动来加载。
IO比内存访问更慢,因此若是动画涉及到IO,就是一个大问题。总的来讲,这就须要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提早加载当前不须要的资源,可是以后可能须要用到)。这些技术将会在第14章中讨论。
因而如今你知道有哪些点可能会影响动画性能,那该如何修复呢?好吧,其实不须要。有不少种诡计来优化动画,但若是盲目使用的话,可能会形成更多性能上的问题,而不是修复。
如何正确的测量而不是猜想这点很重要。根据性能相关的知识写出代码不一样于仓促的优化。前者很好,后者实际上就是在浪费时间。
那该如何测量呢?第一步就是确保在真实环境下测试你的程序。
当你开始作一些性能方面的工做时,必定要在真机上测试,而不是模拟器。模拟器虽然是加快开发效率的一把利器,但它不能提供准确的真机性能参数。
模拟器运行在你的Mac上,然而Mac上的CPU每每比iOS设备要快。相反,Mac上的GPU和iOS设备的彻底不同,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操做在模拟器上运行的更慢,尤为是使用CAEAGLLayer
来写一些OpenGL的代码时候。
这就是说在模拟器上的测试出的性能会高度失真。若是动画在模拟器上运行流畅,可能在真机上十分糟糕。若是在模拟器上运行的很卡,也可能在真机上很平滑。你没法肯定。
另外一件重要的事情就是性能测试必定要用发布配置,而不是调试模式。由于当用发布环境打包的时候,编译器会引入一系列提升性能的优化,例如去掉调试符号或者移除并从新组织代码。你也能够本身作到这些,例如在发布环境禁用NSLog语句。你只关心发布性能,那才是你须要测试的点。
最后,最好在你支持的设备中性能最差的设备上测试:若是基于iOS6开发,这意味着最好在iPhone 3GS或者iPad2上测试。若是可能的话,测试不一样的设备和iOS版本,由于苹果在不一样的iOS版本和设备中作了一些改变,这也可能影响到一些性能。例如iPad3明显要在动画渲染上比iPad2慢不少,由于渲染4倍多的像素点(为了支持视网膜显示)。
为了作到动画的平滑,你须要以60FPS(帧每秒)的速度运行,以同步屏幕刷新速率。经过基于NSTimer
或者CADisplayLink
的动画你能够下降到30FPS,并且效果还不错,可是没办法经过Core Animation作到这点。若是不保持60FPS的速率,就可能随机丢帧,影响到体验。
你能够在使用的过程当中明显感到有没有丢帧,但没办法经过肉眼来获得具体的数据,也无法知道你的作法有没有真的提升性能。你须要的是一系列精确的数据。
你能够在程序中用CADisplayLink
来测量帧率(就像11章“基于定时器的动画”中那样),而后在屏幕上显示出来,但应用内的FPS显示并不可以彻底真实测量出Core Animation性能,由于它仅仅测出应用内的帧率。咱们知道不少动画都在应用以外发生(在渲染服务器进程中处理),但同时应用内FPS计数的确能够对某些性能问题提供参考,一旦找出一个问题的地方,你就须要获得更多精确详细的数据来定位到问题所在。苹果提供了一个强大的Instruments工具集来帮咱们作到这些。
在这章中,咱们学习了Core Animation是如何渲染,以及咱们可能出现的瓶颈所在。你一样学习了如何使用Instruments来检测和修复性能问题。
在下三章中,咱们将对每一个普通程序的性能陷阱进行详细讨论,而后学习如何修复。