常用动画的原理与实现

2016-06-12 » iOS开发基础

一、CoreAnimation(核心动画)

1.什么是核心动画

Core Animation可以用在 Mac OS X 和 iOS平台。Core Animation的动画执行过程是在后台操作的.不会阻塞主线程。要注意的是, Core Animation是直接作用在CALayer上的,并非UIView。

总体来说核心动画的优点有:

1、性能强大,使用硬件加速,可以同时向多个图层添加不同的动画效果

2、接口易用,只需要少量的代码就可以实现复杂的动画效果。

3、运行在后台线程中,在动画过程中可以响应交互事件(UIView动画默认动画过程中不响应交互事件)

4、只有在发生改变的时候才重绘内容,消除了动画的帧速率上的运行代码,提高应用性能

动画操作过程:

1、创建一个CAAnimation对象

2、设置一些动画的相关属性

3、给CALayer添加动画(addAnimation:forKey: 方法)

4、移除CALayer中的动画(removeAnimationForKey: 方法)

2.核心动画相关类

png

CAAnimation是所有动画对象的父类,实现CAMediaTiming协议,负责控制动画的时间、速度和时间曲线等等,是一个抽象类,不能直接使用。

CAPropertyAnimation 是CAAnimation的子类,它支持动画地显示图层的keyPath,不直接使用。

CATransition 转场动画

综上,核心动画类中可以直接使用的类有:

1. CABasicAnimation  基础动画

2. CAKeyframeAnimation  关键帧动画

3. CATransition 转场动画

4. CAAnimationGroup 组动画

5. CASpringAnimation 弹性动画 (iOS9.0之后,它实现弹簧效果的动画,是CABasicAnimation的子类。)

1、CAAnimation (一部分属性来自 CAMediaTiming协议)

duration:动画的持续时间,默认为0.25秒

speed :速度 speed = 1.0 / duration = 1.0 的动画效果 和 speed = 2.0 / duration = 2.0 的动画效果是一模一样的,我们设置的duration可能和动画进行的真实duration不一样,这个还依赖于speed。

timeOffset 设置动画线的起始结束时间点

 //假定一个3s的动画,它的状态为t0,t1,t2,t3,当没有timeOffset的时候,正常的状态序列应该为:
 //t0->t1->t2->t3
 //当设置timeOffset为1的时候状态序列就变为
 //t1->t2->t3->t0
 //同理当timeOffset为2的时候状态序列就变为:
 //t2->t3->t0->t1

autoreverses:是否自动回到动画开始状态

repeatCount:动画的重复次数

repeatDuration:动画的重复时间

removedOnCompletion:默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode属性为kCAFillModeForwards。比如进入后台回来动画依然执行,可以使用这个属性。

fillMode:决定当前对象在非active时间段的行为。比如动画开始之前,动画结束之后。

beginTime:可以用来设置动画延迟执行时间,若想延迟2s,就设置为CACurrentMediaTime() + 2,CACurrentMediaTime()为图层的当前时间。 CALayer 的beginTime 一般用于动画暂停的使用,CAAnimation 的beginTime一般用于动画延迟执行,但只在使用groupAnimation的时候生效,直接添加在layer上的animation使用会导致动画不执行。

timingFunction:速度控制函数,控制动画运行的节奏

枚举参数:

 kCAMediaTimingFunctionLinear  时间曲线函数,匀速
 kCAMediaTimingFunctionEaseIn  时间曲线函数,由慢到特别快
 kCAMediaTimingFunctionEaseOut  时间曲线函数,由快到慢
 kCAMediaTimingFunctionEaseInEaseOut  时间曲线函数,由慢到快
 kCAMediaTimingFunctionDefault   系统默认

delegate:动画代理,一般设置隐式代理,该代理是NSObject的分类,需要遵守协议CAAnimationDelegate

-(void)animationDidStart:(CAAnimation *)anim; 核心动画开始时执行

-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag; 核心动画执行结束后调用

2、CAPropertyAnimation

keyPath:通过指定CALayer的一个属性名做为keyPath里的参数(NSString类型),并且对CALayer的这个属性的值进行修改,达到相应的动画效果。比如,指定@”position”为keyPath,就修改CALayer的position属性的值,以达到平移的动画效果。

CAPropertyAnimation *animation = [CAPropertyAnimation animationWithKeyPath:@"position.y"];
[self.view.layer addAnimation:animation forKey:@"position_y"];

一些常用的animationWithKeyPath值的总结

说明 使用形式
transform.scale 比例转化 @(0.8)
transform.scale.x 宽的比例 @(0.8)
transform.scale.y 高的比例 @(0.8)
transform.rotation.x 围绕x轴旋转 @(M_PI)
transform.rotation.y 围绕y轴旋转 @(M_PI)
transform.rotation.z 围绕z轴旋转 @(M_PI)
cornerRadius 圆角的设置 @(50)
backgroundColor 背景颜色的变化 (id)[UIColor purpleColor].CGColor
bounds 大小,中心不变 [NSValue valueWithCGRect:CGRectMake(0, 0, 200, 200)];
position 位置(中心点的改变) [NSValue valueWithCGPoint:CGPointMake(300, 300)];
contents 内容,比如UIImageView的图片 imageAnima.toValue = (id)[UIImage imageNamed:@”to”].CGImage;
opacity 透明度 @(0.7)
contentsRect.size.width 横向拉伸缩放 @(0.4)最好是0~1之间的

3、CABasicAnimation

fromValue : keyPath相应属性的初始值 toValue : keyPath相应属性的结束值,到某个固定的值(类似transform的make含义)

注意:

随着动画的进行,在长度为duration的持续时间内,keyPath相应属性的值从fromValue渐渐地变为toValue.
如果fillMode = kCAFillModeForwards和removedOnComletion = NO;那么在动画执行完毕后,图层会保持显示动画执行后的状态,但实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变.比如: CALayer的postion初始值为(0,0),CABasicAnimation的fromValue为(10,10),toValue为 (100,100),虽然动画执行完毕后图层保持在(100,100) 这个位置,实质上图层的position还是为(0,0);

byValue:不断进行累加的数值(byvalue 值加上fromValue => tovalue)

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
animation.byValue = @(M_PI * 2);

构建一个动画

CABasicAnimation *a1 = [CABasicAnimation animation];
a1.keyPath = @"position";
a1.toValue = [NSValue valueWithCGPoint:CGPointMake(150 + 28, 180 + 28)];
a1.duration = 0.5;
a1.fillMode = kCAFillModeForwards;
a1.removedOnCompletion = NO;
[self.view.layer addAnimation:a1 forKey:@"position"];

4、 CAKeyframeAnimation

就和画图一样吗,画三个点,三个点连起来的线就是动画轨迹,每个点有对应的位置,对应的时间点。

values:NSArray对象,里面的元素称为”关键帧”(NSValue类型),动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧( NSValue)

//设置动画属性
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
NSValue *p1 = [NSValue valueWithCGPoint:CGPointMake(50, 150)];
NSValue *p2 = [NSValue valueWithCGPoint:CGPointMake(250, 150)];
NSValue *p3 = [NSValue valueWithCGPoint:CGPointMake(50, 550)];
NSValue *p4 = [NSValue valueWithCGPoint:CGPointMake(250, 550)];
animation.values = @[p1, p2, p3, p4];
animation.keyTimes = @[ [NSNumber numberWithFloat:0.0],
                        [NSNumber numberWithFloat:0.4],
                        [NSNumber numberWithFloat:0.8],
                        [NSNumber numberWithFloat:1.0]];

keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧的时间节点的百分比,当keyTimes没有设置的时候,各个关键帧的时间是平分的

path:可以设置一个CGPathRef、CGMutablePathRef,让层跟着路径移动,path只对CALayer的anchorPoint和position起作用,如果设置了path,那么values、keyTimes将被忽略。

CAKeyframeAnimation * ani = [CAKeyframeAnimation animationWithKeyPath:@"position"];
CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddEllipseInRect(path, NULL, CGRectMake(130, 200, 100, 100));
ani.path = path.CGPath;

rotationMode:旋转模式

(1)默认nil

(2)设置为kCAAnimationRotateAuto 或 kCAAnimationRotateAutoReverse 会随着旋转的角度做 ”自转“ animKey.rotationMode = kCAAnimationRotateAuto;

构建一个动画

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.duration = 2;
//animation.repeatCount = HUGE_VAL;
NSMutableArray *array = [NSMutableArray array];
[array addObject:[NSValue valueWithCGPoint:CGPointMake(30, 150)]];
[array addObject:[NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH / 2.0, 70)]];
[array addObject:[NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH - 30, 150)]];
[array addObject:[NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH / 2.0, 300 - 50)]];
[array addObject:[NSValue valueWithCGPoint:CGPointMake(30, 150)]];
animation.values = array;
//每一个动画的时间节点位置  百分比制 0-1之间
NSMutableArray *time_array = [NSMutableArray array];
[time_array addObject:[NSNumber numberWithFloat:0.0]];
[time_array addObject:[NSNumber numberWithFloat:0.25]];
[time_array addObject:[NSNumber numberWithFloat:0.5]];
[time_array addObject:[NSNumber numberWithFloat:0.75]];
[time_array addObject:[NSNumber numberWithFloat:1.0]];
animation.keyTimes = time_array; //设置关键帧对应的时间点,范围:0-1。如果没有设置该属性,则每一帧的时间平分。

[self.imageView.layer addAnimation:animation forKey:@"CAKeyframeAnimation"];

5、 CASpringAnimation

iOS9才引入的动画类,它继承于CABasicAnimation,用于制作弹簧动画

mass 质量 ,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大

stiffness 刚度系数(劲度系数/弹性系数),刚度系数越大,形变产生的力就越大,运动越快

damping 阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快

initialVelocity 初始速率,动画视图的初始速度大小 速率为正数时,速度方向与运动方向一致,速率为负数时,速度方向与运动方向相反 如果把速率改成-20,则动画变成

settlingDuration 结算时间 返回弹簧动画到停止时的估算时间,根据当前的动画参数估算 通常弹簧动画的时间使用结算时间比较准确

构建一个动画

CASpringAnimation *animation = [CASpringAnimation animationWithKeyPath:@"position.y"];
animation.fromValue = @(132);
animation.toValue = @(80);
animation.duration = animation.settlingDuration;
animation.damping = 5;
animation.initialVelocity = 0;
animation.stiffness = 500.0;
animation.mass = 1.0;
[self.imageView.layer addAnimation:animation forKey:@"CASpringAnimation"];

6、 CAAnimationGroup

animations:动画组,用来保存一组动画对象的NSArray。默认情况下,一组动画对象是同时运行的,也可以通过设置动画对象的beginTime属性来更改动画的开始时间。

构建动画

// 2. 向组动画中添加各种子动画
// 2.1 旋转
CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
// anim1.toValue = @(M_PI * 2 * 500);
anim1.byValue = @(M_PI * 2 * 1000);

// 2.2 缩放
CABasicAnimation *anim2 = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
anim2.toValue = @(0.1);

// 2.3 改变位置, 修改position
CAKeyframeAnimation *anim3 = [CAKeyframeAnimation animationWithKeyPath:@"position"];
anim3.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(50, 100, 250, 100)].CGPath;

// 把子动画添加到组动画中
CAAnimationGroup *groupAnima = [CAAnimationGroup animation];

groupAnima.animations = @[ anim1, anim2, anim3];

groupAnima.duration = 2.0;
[self.imageView.layer addAnimation:groupAnima forKey:@"animationGroup"];

7、 CATransition

转场动画

type:设置动画过渡的类型

kCATransitionFade 交叉淡化过渡

kCATransitionMoveIn 新视图移到旧视图上面

kCATransitionPush 新视图把旧视图推出去

kCATransitionReveal 将旧视图移开,显示下面的新视图

下面类型包装成字符串赋值 转场动画过渡效果

subtype:设置动画过渡方向

kCATransitionFromRight

kCATransitionFromLeft

kCATransitionFromTop

kCATransitionFromBottom

startProgress:动画起点(在整体动画的百分比)

endProgress:动画终点(在整体动画的百分比)

(IBAction)didRecognizeSwipeGesture:(UISwipeGestureRecognizer *)sender {

  // 1. 创建一个转场动画对象
  CATransition *anim = [[CATransition alloc] init];
  // 设置转场动画的类型
  anim.type = @"suckEffect";
  // 设置转场动画时间
  anim.duration = 1.5;
  anim.delegate = self;
  // 判断方向
  if (sender.direction == UISwipeGestureRecognizerDirectionLeft) {
      // 设置转场动画的子类型
      anim.subtype = kCATransitionFromRight;
      // NSLog(@"left");
      self.index++;
  } else {
      // 设置转场动画的子类型
      anim.subtype = kCATransitionFromLeft;
      // NSLog(@"right");
      self.index--;
  }

  // 判断是否越界
  if (self.index > 4) {
      self.index = 0;
  }

  if (self.index < 0) {
      self.index = 4;
  }

  // 拼接图片名称
  NSString *imgName = [NSString stringWithFormat:@"%d", self.index + 1];
  // 切换图片
  self.imgViewIcon.image = [UIImage imageNamed:imgName];
  // 把转场动画添加到对应的控件上
   [self.imgViewIcon.layer addAnimation:anim forKey:@"anim1"];
}

构建动画

CATransition *ani = [CATransition animation];
ani.type = @"rippleEffect"; //水滴入水振动的效果
ani.subtype = kCATransitionFromLeft;
ani.duration = 1.5;
[self.imageView.layer addAnimation:ani forKey:nil];

3.CALayer图形绘制

1、CALayer

CALayer是NSObject的子类而非UIResponder的子类,因此图层本身无法响应用户操作事件却拥有着事件响应链相似的判断方法,所以CALayer需要包装成一个UIView容器来完成这一功能。

每一个UIView自身存在一个CALayer来显示内容。在后者的属性中我们可以看到存在着多个和UIView界面属性对应的变量,因此我们在修改UIView的界面属性的时候其实是修改了这个UIView对应的layer的属性。

CALayer拥有和UIView一样的树状层级关系,也有类似UIView添加子视图的addSublayer这些类似的方法。

为什么有了UIView还要CALayer

CALayer常用属性

1.position和anchorPoint

anchorPoint(锚点)是一个x和y值取值范围内在0~1之间CGPoint类型,它决定了当图层发生几何仿射变换时基于的坐标原点。默认情况下为0.5, 0.5,由anchorPoint和frame经过计算获得图层的position这个值。

2.mask和maskToBounds

maskToBounds值为true时表示超出图层范围外的所有子图层都不会进行渲染,当我们设置UIView的clipsToBounds时实际上就是在修改maskToBounds这个属性。mask这个属性表示一个遮罩图层,在这个遮罩之外的内容不予渲染显示。

3.cornerRadius、borderWidth和borderColor

borderWidth和borderColor设置了图层的边缘线条的颜色以及宽度,正常情况下这两个属性在layer的层次上不怎么使用。后者cornerRadius设置圆角半径,这个半径会影响边缘线条的形状。

4.shadowColor、shadowOpacity、shadowOffset和shadowRadius

这四个属性结合起来可以制作阴影效果。shadowOpacity默认情况下值为0,这意味着即便你设置了其他三个属性,只要不修改这个值,你的阴影效果就是透明的。其次,不要纠结shadowOffset这个决定阴影效果位置偏移的属性为什么会是CGSize而不是CGPoint。

注意:

1.隐式属性动画的本质是这些属性的变动默认隐含了CABasicAnimation动画实现。

2.在CALayer中很少使用frame属性,因为frame本身不支持动画效果,通常使用bounds和position代替。

3.CALayer中透明度使用opacity表示而不是alpha;中心点使用position表示而不是center。

4.anchorPoint属性是图层的锚点,范围在(0-1,0-1)表示在x、y轴的比例,这个点永远可以同position(中心点)重合,当图层中心点固定后,调整anchorPoint即可达到调整图层显示位置的作用(因为它永远和position重合)

为了进一步说明anchorPoint的作用,假设有一个层大小100*100,现在中心点位置(50,50),由此可以得出frame(0,0,100,100)。上面说过anchorPoint默认为(0.5,0.5),同中心点position重合,此时使用图形描述如下图1;当修改anchorPoint为(0,0),此时锚点处于图层左上角,但是中心点poition并不会改变,因此图层会向右下角移动,如下图2;然后修改anchorPoint为(1,1),position还是保持位置不变,锚点处于图层右下角,此时图层如图3。

png

2、图形绘制(基础属性、基础方法的使用介绍、使用场景、实例)

CALayer的图形绘制有两种方法:

1.通过图层代理方法drawLayer:inContext进行图形绘制的。

2.使用drawInContext:方法,通过创建图层CALayer来进行自定义图层绘制。

需要注意的是调用这两种方法以后,必须调用setNeedsDisplay方法,否则无法显示内容。setNeedsDisplay方法的作用是移除旧的图层内容(contents),设置新的图层内容。

绘制常用属性方法:

CGContextRef  ctx 图形上下文,可以将其理解为一块画布
//ctx 的备份
CGContextSaveGState(ctx);
//线的粗细
CGContextSetLineWidth(ctx, 5);

CGContextSetLineCap(ctx, kCGLineCapRound);//线条两端的样式
CGContextSetLineJoin(ctx, kCGLineJoinRound);//两线条转折相接端的样式

//ctx 的出栈  把  ctx 恢复成默认
CGContextRestoreGState(ctx);
//画笔颜色
CGContextSetRGBStrokeColor(ctx, 1, 0, 0, 1);
//两点成线
CGContextMoveToPoint(ctx, 120, 50);    //起点
CGContextAddLineToPoint(ctx, 270, 50); //画线

CGContextStrokePath(ctx); //根据ctx 线条方式绘制
CGContextFillPath(ctx); //根据ctx 填充方式绘制

CALayer常用绘制

画实线
CGContextMoveToPoint(ctx, 120, 50);    //起点
CGContextAddLineToPoint(ctx, 270, 50); //画线

画虚线
ctx方式:

CGFloat dash[2] = {3, 1};
CGContextSetLineWidth(ctx, 0.5);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineDash(ctx, 0.0, dash, 2);
CGContextMoveToPoint(ctx, 120, 100);
CGContextAddLineToPoint(ctx, 270, 100);
CGContextStrokePath(ctx);

CAShapeLayer方式:

UIBezierPath *pathFour = [UIBezierPath bezierPath];
pathFour.lineWidth = 3;
[pathFour moveToPoint:CGPointMake(120, 50)];
[pathFour addLineToPoint:CGPointMake(270, 50)];
[pathFour stroke];

CAShapeLayer *layer4 = [CAShapeLayer layer];
layer4.path = pathFour.CGPath;
[layer addSublayer:layer4];
layer4.strokeColor = [[UIColor greenColor] CGColor];
layer4.fillColor = [[UIColor clearColor] CGColor];
//线型模板 这是一个NSNumber的数组,索引从1开始记,奇数位数值表示实线长度,偶数位数值表示空白长度
[layer4 setLineDashPattern:@[ @3, @1, @10, @5 ]];

画矩形
CGContextAddRect(ctx, CGRectMake(150, 50, 50, 50));

画圆、圆弧
CGContextAddArc(ctx, 60, 50, 45, 0, M_PI, 0);

画圆、画椭圆
CGContextAddEllipseInRect(ctx, CGRectMake(150, 40, 90, 50));

部分绘制
CAShapeLayer  的 strokeStart 和 strokeEnd 属性

贝塞尔曲线

UIBezierPath对象是CGPathRef数据类型的封装。path如果是基于矢量形状的,都用直线和曲线段去创建。 我们使用直线段去创建矩形和多边形,使用曲线段去创建弧(arc),圆或者其他复杂的曲线形状。 每一段都包括一个或者多个点,绘图命令定义如何去诠释这些点。每一个直线段或者曲线段的结束的地方是下一个的开始的地方。每一个连接的直线或者曲线段的集合成为subpath。一个UIBezierPath对象定义一个完整的路径包括一个或者多个subpaths。

创建和使用一个path对象的过程是分开的。创建path是第一步,包含一下步骤:

(1)创建一个Bezier path对象。

(2)使用方法moveToPoint:去设置初始线段的起点。

(3)添加line或者curve去定义一个或者多个subpaths。

(4)改变UIBezierPath对象跟绘图相关的属性。

构建动画

UIBezierPath *path = [UIBezierPath bezierPath];
CAShapeLayer *layer = [CAShapeLayer layer];
[path moveToPoint:CGPointMake(50, 100)];
[path addLineToPoint:CGPointMake(200, 100)];
[path addLineToPoint:CGPointMake(200, 250)];
[path addLineToPoint:CGPointMake(50, 250)];

layer.frame = self.contentViewOne.bounds;
layer.strokeColor = [[UIColor redColor] CGColor];
layer.fillColor = [[UIColor clearColor] CGColor];
layer.lineCap = kCALineCapRound;
layer.lineJoin = kCALineJoinRound;
layer.path = [path CGPath];
layer.lineWidth = 6.0;

[self.contentViewOne.layer addSublayer:layer];

CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 3;
pathAnimation.fromValue = @(0.0);
pathAnimation.toValue = @(1.0);
pathAnimation.autoreverses = YES;
pathAnimation.repeatCount = HUGE;
[layer addAnimation:pathAnimation forKey:nil];

3.显示动画和隐式动画

当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。

Core Animation在每个run loop周期中自动开始一次新的事务,即使你不显式的用 [CATransaction begin] 开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画 (例如:position的变化)。

注意:只有非rootLayer才有隐式动画

4.CATransaction 事务

核心动画里面存在事务(CATransaction)这样一个概念,它负责协调多个动画原子更新显示操作。

简单来说事务是核心动画里面的一个基本的单元,动画的产生必然伴随着layer的Animatable属性的变化,而layer属性的变化必须属于某一个事务。因此,核心动画依赖事务。

事务的作用:

保证一个或多个layer的一个或多个属性变化同时进行

事务分为隐式和显式:

1.隐式:没有主动调用事务的方法,由系统自动生成事务。比如直接设置一个layer的position属性,则会在当前线程自动生成一个事务,并在下一个runLoop中自动commit事务。

2.显式:主调用事务的方法:[CATransaction begin] 和 [CATransaction commit]。

事务的可设置属性(会覆盖隐式动画的设置):

animationDuration:动画时间
animationTimingFunction:动画时间曲线
disableActions:是否关闭动画
completionBlock:动画执行完毕的回调

事务支持嵌套使用:当最外层的事务commit后动画才会开始。

使用实例:

[CATransaction begin];
[CATransaction setAnimationDuration:2.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
//    [CATransaction setDisableActions:YES]; //设置为YES就关闭动画
self.subLayer.bounds = self.centerShow.layer.bounds;
[CATransaction commit];

二、UIViewAnimation

1.UIViewAnimation介绍

可设置动画属性

1. frame            //大小变化:改变视图框架(frame)和边界。
2. bounds           //拉伸变化:改变视图内容的延展区域。
3. center           //居中显示
4. transform        //旋转:即任何应用到视图上的仿射变换(transform)
5. alpha            //改变透明度:改变视图的alpha值。
6. backgroundColor  //改变背景颜色
7. contentStretch   //拉伸内容

参数

1. duration   //为动画持续的时间。 
2. animations //为动画效果的代码块。
3. completion //为动画执行完毕以后执行的代码块
4. options    //为动画执行的选项
5. delay      //为动画开始执行前等待的时间

2.核心动画类

UIView 的动画方面扩展有三部分 :

1、UIView(UIViewAnimation)

 UIView(UIViewAnimation);
 设置动画ID 方便查询
 + (void)beginAnimations:(nullable NSString *)animationID context:(nullable void *)context;
 提交动画 执行动画
 + (void)commitAnimations;
 设置动画执行时间
 + (void)setAnimationDuration:(NSTimeInterval)duration;
 设置动画执延迟执行时间
 + (void)setAnimationDelay:(NSTimeInterval)delay;
 设置动画代理对象,当动画开始或者结束时会发消息给代理对象
 + (void)setAnimationDelegate:(nullable id)delegate;
 设置动画开始时调用的方法 执行delegate对象的selector,并且把beginAnimations:context:中传入的参数传进selector
 + (void)setAnimationWillStartSelector:(nullable SEL)selector;
 设置动画结束时调用的方法 执行delegate对象的selector,并且把beginAnimations:context:中传入的参数传进selector
 + (void)setAnimationDidStopSelector:(nullable SEL)selector;
 设置动画的开始时间,默认为now
 + (void)setAnimationStartDate:(NSDate *)startDate
 设置视图view的过渡效果, transition指定过渡类型, cache设置YES代表使用视图缓存,性能较好
 + (void)setAnimationTransition:(UIViewAnimationTransition)transition forView:(UIView *)view cache:(BOOL)cache
 设置是否自动恢复执行 YES,代表动画每次重复执行的效果会跟上一次相反
 + (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses
 设置动画的重复次数
 + (void)setAnimationRepeatCount:(float)repeatCount
 设置动画执行效果
 + (void)setAnimationCurve:(UIViewAnimationCurve)curve
 设置动画是否生效
 + (void)setAnimationsEnabled:(BOOL)enabled; 

构建动画

self.animationImageView.frame = CGRectMake(0, 132, 56, 36);
[UIView beginAnimations:@"btnOne" context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:1.0];
[UIView setAnimationDelay:0.0];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationDidStopSelector:@selector(AnimationDidStop)];
[UIView setAnimationWillStartSelector:@selector(AnimationWillBegin)];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.animationImageView cache:YES];

self.animationImageView.frame = CGRectMake(SCREEN_WIDTH - 56, 132, 56, 36);
[UIView commitAnimations];

2、UIView(UIViewAnimationWithBlocks)

将动画实现封装在block区域,参数构建在类方法上。

可选动画执行效果,如进出效果等
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

带回调block动画,动画执行完成后进入block
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); // delay = 0.0, options = 0

不带回调动画
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0); // delay = 0.0, options = 0, completion = NULL

弹簧动画
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);

view的转场动画
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

view到另一个view的转场动画
+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); // toView added to fromView.superview, fromView removed from its superview

+ (void)performSystemAnimation:(UISystemAnimation)animation onViews:(NSArray<__kindof UIView *> *)views options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))parallelAnimations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);

构建动画

[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:5.0 initialSpringVelocity:0 options:UIViewAnimationOptionAutoreverse animations:^{

    self.animationImageView.frame = CGRectMake(SCREEN_WIDTH - 56, 132, 56, 36);

} completion:^(BOOL finished) {
    self.animationImageView.frame = CGRectMake(0, 132, 56, 36);
}];

3、UIView (UIViewKeyframeAnimations)

关键帧动画
+ (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);

在上面的block中添加关键帧
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations NS_AVAILABLE_IOS(7_0); // start time and duration are values between 0.0 and 1.0 specifying time and duration relative to the overall time of the keyframe animation

构建动画

[UIView animateKeyframesWithDuration:2.0 delay:0.0 options:UIViewKeyframeAnimationOptionAutoreverse animations:^{

    [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{

        self.animationImageView.frame = CGRectMake(200, 132, 56, 36);
    }];

    [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{

        self.animationImageView.frame = CGRectMake(200, 232, 56, 36);
    }];

} completion:^(BOOL finished) {
    self.animationImageView.frame = CGRectMake(0, 132, 56, 36);
}];

三、其他动画

1.控制器转场动画

原理:UIViewControllerAnimatedTransitioning (过渡协调器)

iOS7以后UIViewControllerAnimatedTransitioning 或者 UIViewControllerContextTransitioning这些协议已经可以比较方便的自定义ViewController之间的动画了,比如修改UINavigationController的动画,下面举个例子来看一看如何做一个自定义的NavigationController的Push和Pop非交互动画。

1.首先 我们定义一个类 TransitionOneManager 基于NSObject 实现 UIViewControllerAnimatedTransitioning 协议。

2.实现下面两个协议方法

//定义转场动画时间
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext
//定义转场动画效果
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext 

3.定义两个控制器 分别是 ONE 和 TWO ONE push 到TWO

在TWO中 实现 UINavigationControllerDelegate 实现下面方法

//这里返回的就是navigationController push 要使用的动画效果
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
  //在这里把 TransitionOneManager 实现的动画效果返回回去
}

4.在ONE 中 把 navigationController 的代理挂到TWO控制器上面,因为我们需要使用TWO中实现的push效果

self.navigationController.delegate = vc;
[self.navigationController pushViewController:vc animated:YES];

2.动力学

UIDynamic 是苹果在iOS7之后添加的一套动力学框架,运用它我们可以极其方便地模拟现实生活中的运动,比如重力,碰撞等等。它是通过添加行为的方式让动力学元素参与运动的。

iOS7.0中提供的动力学行为包括:

UIGravityBehavior:重力行为
UICollisionBehavior:碰撞行为
UIAttachmentBehavior:附着行为
UISnapBehavior:吸附行为
UIPushBehavior:推行为
UIDynamicItemBehavior:动力学元素行为

UIDynamic的使用还是相对简单

1.首先我们创建一个小方块 boxView 并把它放在self.view的上面部分。(只有遵循了UIDynamicItem协议的对象才能参与仿真模拟,而UIView正遵循了此协议,因此所有视图控件都能参与仿真运动)

2.然后定义一个 UIDynamicAnimator 物理仿真器(凡是要参与运动的对象必须添加到此容器中)

_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view]; //refrerence表示 self.view内都算仿真器范围

3.再添加一个重力行为 到仿真器,并且 这个行为作用对象是我们之前定义的boxView

[[UIGravityBehavior alloc] initWithItems:@[ boxView ]];
[_animator addBehavior:_gravity];

4.然后启动app,可以发现 放在self.view上半部分的boxView受重力行为影响,往下掉落。但是会掉出self.view范围。

5.为了不掉出self.view 范围 我们还需要给boxView添加一个别的行为:碰撞行为,接触到仿真器边界或者其他self.view中得容器会产生碰撞效果。

_collision = [[UICollisionBehavior alloc] initWithItems:@[ _behaviorView, _behaviorViewTwo ]];
_collision.translatesReferenceBoundsIntoBoundary = YES; //边界检测
[_animator addBehavior:_collision];

6.这样小方块就不会掉出仿真器范围了,同理,其他行为的使用方式和上面一样,一定要添加到仿真器才能生效。

CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink对象,把它添加到一个runloop中,并给它提供一个 target 和 selector 在屏幕刷新的时候调用。

一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector,这时 target 可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。

在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。

duration属性

提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是 duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。

frameInterval属性

是可读可写的NSInteger型值,标识间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。

我们通过pause属性来控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用

-(void)invalidate

从runloop中删除并删除之前绑定的 target跟selector

CADisplayLink 不能被继承。

CADisplayLink 与 NSTimer 有什么不同

1、iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

2、CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

构建动画

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationMethod)];
self.displayLink.frameInterval = 30;
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

animationMethod 完成对应的动画操作

4.CAEmitterLayer 粒子动画

在UIKit中,粒子系统由两部分组成:

一个或多个CAEmitterCells:发射器电池可以看作是单个粒子的原型(例如,一个单一的粉扑在一团烟雾)。当散发出一个粒子,UIKit根据这个发射粒子和定义的基础上创建一个随机粒子。此原型包括一些属性来控制粒子的图片,颜色,方向,运动,缩放比例和生命周期。

一个或多个CAEmitterLayers,但通常只有一个:这个发射的层主要控制粒子的形状(例如,一个点,矩形或圆形)和发射的位置(例如,在矩形内,或边缘)。这个层具有全局的乘法器,可以施加到系统内的CAEmitterCells。这些给你一个简单的方法覆盖的所有粒子的变化。

构建动画

_snowEmitter = [CAEmitterLayer layer];

_snowEmitter.emitterPosition = CGPointMake(self.view.bounds.size.width / 2.0, -30);
_snowEmitter.emitterSize = CGSizeMake(self.view.bounds.size.width * 2.0, 0.0);
_snowEmitter.emitterShape = kCAEmitterLayerLine;
_snowEmitter.emitterMode = kCAEmitterLayerOutline;

CAEmitterCell *snowflake = [CAEmitterCell emitterCell];

snowflake.birthRate = 1.0;
snowflake.lifetime = 120.0;
snowflake.velocity = -10;
snowflake.velocityRange = 10;
snowflake.yAcceleration = 2;
snowflake.emissionRange = 0.5 * M_PI;
snowflake.spinRange = 0.25 * M_PI;
snowflake.contents = (id)[[UIImage imageNamed:@"scream"] CGImage];
snowflake.color = [[UIColor colorWithRed:0.600 green:0.658 blue:0.743 alpha:1.000] CGColor];

_snowEmitter.shadowOpacity = 1.0;
_snowEmitter.shadowRadius = 0.0;
_snowEmitter.shadowOffset = CGSizeMake(0.0, 1.0);
_snowEmitter.shadowColor = [[UIColor whiteColor] CGColor];
_snowEmitter.emitterCells = [NSArray arrayWithObject:snowflake];
[self.view.layer insertSublayer:_snowEmitter atIndex:0];

5.Facebook POP 动画框架

核心动画类中可以直接使用的类有:

POPSpringAnimation 有弹性效果的动画类 POPBasicAnimation 基本动画类 POPDecayAnimation 衰减动画类 POPCustomAnimation 可以自定义动画的类

可以同时作用于UIView 和 CALayer 可以响应用户事件

四、常见问题

1、 如果当动画正在执行的时候, 将程序退出到后台, 那么当程序再次进入前台的时候就不执行了。

原因: 因为再次进入前台后动画已经被删除了。

解决: anim.removedOnCompletion = NO;

2、代理造成的循环引用问题

原因:由于CAAnimation的delegate使用的strong类型,所以在全局变量如下设置时会产生循环引用的情况

self.animation.delegate = self; //可通过复用dealloc方法来验证

解决:使用NSProxy解决,在一个对象中对self 弱引用处理 然后通过类方法把 弱引用处理过的self对象转给delegate (YYWeakProxy)

3、.cornerRadius 属于layer层的参数,无法通过UIView animation来动画变更

CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
basicAnimation.duration = 0.2;
[self.animationView.layer setCornerRadius:20.0f];
[self.animationView.layer addAnimation:basicAnimation forKey:@"cornerRadius"];

4、CGAffineTransformMakeRotation 使用的时候 如果直接frame变更 会导致形变 使用center的变更来变更位置就不会。

5、当UIView remove出父容器 UIView 会自动销毁 layer动画也是 但我们处理layer销毁的时候最好还是主动去remove掉动画

6、如何主动停止动画(UIView 动画 / 核心动画 通用)

removeAllAnimations 或者移除某个动画

暂停/恢复:

if (self.imageViewOne.layer.speed == 0.0) {
  CFTimeInterval pausedTime = [self.imageViewOne.layer timeOffset];
  self.imageViewOne.layer.speed = 1.0;
  self.imageViewOne.layer.timeOffset = 0.0;
  self.imageViewOne.layer.beginTime = 0.0;
  CFTimeInterval timeSincePause = [self.imageViewOne.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
  self.imageViewOne.layer.beginTime = timeSincePause;
}else{

CFTimeInterval pausedTime = [self.imageViewOne.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.imageViewOne.layer.speed = 0.0;
self.imageViewOne.layer.timeOffset = pausedTime;
}

在一个动画过程中插入其他动画 阻塞?

7、在给UIView添加绘图delegate的时候的报错

不能再将某个UIView设置为CALayer的delegate,因为UIView对象已经是它内部根层的delegate,再次设置为其他层的delegate就会出问题。ShapeLayer 设置代理也会出错

8、UIView的setNeedsDisplay和setNeedsLayout方法

首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews, 就可以 处理子视图中的一些数据。综上所诉,setNeedsDisplay方便绘图,而layoutSubViews方便出来数据。

layoutSubviews在以下情况下会被调用:

1、init初始化不会触发layoutSubviews。 2、addSubview会触发layoutSubviews。 3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。 4、滚动一个UIScrollView会触发layoutSubviews。 5、旋转Screen会触发父UIView上的layoutSubviews事件。 6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。 7、直接调用setLayoutSubviews。

drawRect在以下情况下会被调用:

1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值). 2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。 3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。 4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。 以上1,2推荐;而3,4不提倡

drawRect方法使用注意点:

1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。 2、若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法 3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕

9、CALayer上动画的暂停和恢复

暂停CALayer的动画

-(void)pauseLayer:(CALayer*)layer
{
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed=0.0; // 让CALayer的时间停止走动
layer.timeOffset=pausedTime; // 让CALayer的时间停留在pausedTime这个时刻
}

恢复CALayer的动画

-(void)resumeLayer:(CALayer*)layer
{
CFTimeInterval pausedTime =layer.timeOffset;
layer.speed=1.0; // 让CALayer的时间继续行走
layer.timeOffset=0.0; // 取消上次记录的停留时刻
layer.beginTime=0.0; // 取消上次设置的时间

//计算暂停的时间(这里用CACurrentMediaTime()-pausedTime也是一样的)
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
//设置相对于父坐标系的开始时间(往后退timeSincePause)
layer.beginTime = timeSincePause;
}

五、动画性能测试调优

1、影响性能的原因

对于一些需要优化图像性能的场景,我们可以检查我们是否触发了offscreen rendering (离屏渲染)。并用更高效的实现手段来替换。

1、阴影绘制:使用ShadowPath来替代shadowOffset等属性的设置

不使用shadowPath

CALayer *imageViewLayer = cell.imageView.layer;
imageViewLayer.shadowColor = [UIColor blackColor].CGColor;
imageViewLayer.shadowOpacity = 1.0;
imageViewLayer.shadowRadius = 2.0;
imageViewLayer.shadowOffset = CGSizeMake(1.0, 1.0);

使用shadowPath

imageViewLayer.shadowPath = CGPathCreateWithRect(imageRect, NULL);

我们可以在下图看到两种方式巨大的性能差别。

shadowPath高效的原因是使用shadowPath避免了offscreen渲染,因为仅需要直接绘制路径即可,不需要提前读取图像去渲染。

png

2、裁剪图片为圆

使用CornerRadius:

CALayer *imageViewLayer = cell.imageView.layer;
imageViewLayer.cornerRadius = imageHeight / 2.0;
imageViewLayer.masksToBounds = YES;

利用一张中间为透明圆形的图片来进行遮盖,虽然会引起blending,但性能仍然高于offerScreen。

根据苹果测试,第二种方式比第一种方式更高效: png

以上举了两个例子阐明了在避免大量的offerscreen渲染后,性能能够得到非常直观有效的提高。

2、关于blending (图层混合)

前面提到了用透明圆形的图片来进行遮盖,会引起blending。blending也会耗费性能。我们先来认识一下Blending.

  • 什么是Blending?

在iOS的图形处理中,blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。

  • 会导致blending的原因:
    • layer(UIView)的Alpha < 1
    • UIImgaeView的image含有Alpha channel(即使UIImageView的alpha是1,但只要image含透明通道,则仍会导致Blending)
  • 为什么Blending会导致性能的损失?

原因是很直观的,如果一个图层是不透明的,则系统直接显示该图层的颜色即可。而如果图层是透明的,则会引入更多的计算,因为需要把下面的图层也包括进来,进行混合后颜色的计算。

在了解完Blending之后,我们就知道为什么很多优化准则都需要我们尽量使用不透明图层了。接下来就是在开发中留意和进行优化了。

3、性能调优

具体的调优 在另一篇介绍instruments 的文章中有写 问题的调试与处理