基础
核心动画是 iOS 和 MacOS 上的图形渲染和动画基础结构,用于为应用的视图和其他视觉元素设置动画。
核心动画位于 APPKit 和 UIKit 之下,且集成于 Cocoa 和 Cocoa Touch 的视图工作流之中。
核心动画自己也具有一些接口,可以扩展视图所展现的功能,并可以更好的控制应用的动画。
核心动画作为应用程序基础结构所扮演的角色:
- 管理程序的内容
- 通过图层修改动画
- 组织图层的层次结构
- 更改图层的默认行为
核心动画类族图:
image
为绘图和动画提供基础
Layer 管理有关其曲面的几何图形、内容和视觉属性的信息。
大多数 Layer 不会执行任何实际的绘图。Layer 会不会程序提供的内容并将其缓存在位图中,该位图有时称为后备存储。当更改 Layer 的属性时,实际是更改的 Layer 对象关联的状态信息。当更改触发动画时,核心动画将Layer 的位图和状态信息传递给图形硬件,由其完成使用新信息渲染位图的工作。
基于 Layer 的动画
Layer 对象的数据和状态信息与屏幕上该 Layer 内容的可视呈现分离。
在 Layer 可以执行的动画示例:
- 位移
- 缩放
- 旋转
- 透明度
- 圆角化
- 背景色
Layer 的坐标系统
iOS 和 macOS 的默认几何图形
image其中 position 基于 anchorPoint(默认:(0.5,0.5)) 属性值而变动。
iOS 和 macOS 的默认坐标系
image图层树反应了动画的不同状态
使用 Core Animation 的应用程序有三组图层对象,每一组图层对象在使程序的内容呈现在屏幕上时有不同的作用:
- 模型层:在程序中最长交互使用的对象。此树中的对象时存储任何动画的目标值的模型对象。无论何时更改图层属性,都可以使用这些对象之一。
- 呈现树:反映了屏幕上显示的当前值。
- 渲染树: 其中的对象执行实际的动画,对 Core Animation 是私有的。
与窗口关联的 Layer
image
窗口的图层树
image
图层树
Core Animation 是一个复合引擎,其职责是尽可能快的组合屏幕上不同的可视内容,这些内容可被分解成独立的图层,存储在一个叫做图层树的体系中。
图层与视图:Layer & View
⼀个视图就是在屏幕上显⽰的⼀个矩形块(⽐如图⽚,⽂字或者视频),它能够拦截类似于⿏标点击或者触摸⼿势等⽤户输⼊。视图在层级关系中可以互相嵌套,⼀个视图可以管理它的所有⼦视图的位置。
iOS 中所有的 View 读派生于基类 UIView。UIView 可以处理触摸事件,可以支持基于 Core Graphics绘图,可以做仿射变换(旋转或缩放),或简单的滑动渐变等动画。
CALayer
类似 UIView,是一些被层级关系树管理的矩形块,同样可包含一些内容(如图片、文本或背景色),管理子图层的位置。
与 UIView 最大的不同是不处理 UI 交互。
每一个 UIView 都有一个对应的 CALayer,即 backing layer, View 的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或删除的时候,他们关联的图层一同样对应的在层级关系树中有相同操作。
实际上,UIView 所在屏幕上的显示和动画,都是在操作 CALayer,UIView 仅仅是对他的一个封装,提供处理触摸相关的功能以及基于 Core Animation底层方法的高级接口。
iOS 为什么要基于 UIView 和 CAlayer 提供2个平行的层级关系?
- 职责分离:UIView 处理 UI 交互,CALayer 处理内容绘制和动画;
- 代码公用:在 iOS 和 macOS 2个平台上,事件和UI 交互有许多不同点,基于触控和鼠标键盘交互有本质的区别;故针对不同的平台,UI 交互这些代码做不同的处理,而内容绘制和动画这些代码可以复用。
CALayer 能做哪些 UIView 不能做的
- 阴影,圆角,带颜色的边框
- 3D 变换
- 非矩形范围
- 透明遮罩
- 多级非线性动画
寄宿图
寄宿图,即图层中包含的图片。
contents
类型:id, 设置图层的内容,layer.contents = (__bridge id)image.CGImage;
contentGravity
类型:NSString, 决定内容在图层内边界如何对齐。
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
基于位置的常量
image基于比例的常量
imagecontentsScale
定义寄宿图的像素尺寸与视图大小的比例,默认1.0.
属于支持高分辨率屏幕机制的一部分。用来判断回执图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度。
layer.contentsScale = [UIScreen mainScreen].scale;
masktoBounds
用来决定是否显示超出边界的内容。UIView 中为 clipsToBounds
//设置为YES,边界外不显示
layer.masktoBounds = YES;
contentsRect
设置在内容区域显示寄宿图的一个子域。默认:{0, 0, 1, 1}
使用坐标单位(范围0~1), 一个相对值来指定。
contentsCeter
类型:CGRect, 定义一个固定的边框和一个在图层上可拉伸的区域。默认:{0, 0, 1, 1}
如下:{0.25,0.25, 0.5,0.5}
image
图层几何学
布局
UIView/CALayer 3个重要的属性:
- frame:相对于父级的外部坐标
- bounds:内部坐标
- center/position:相对于父级anchorPoint 所在的位置。
UIView & CALayer 的坐标系
imageframe 是一个虚拟属性,根据 bounds,position,transform 计算而来,其中任一值发生变化,frame 都会改变。
当对 Layer 进行变换操作时,比如缩放或旋转,frame 代表的是覆盖在 Layer 旋转之后的整个轴对齐的矩形区域,既 frame 和 bounds 的宽高不再一致。
锚点:anchorPoint
可以认为是用来移动 Layer 的把柄。
默认居于 Layer 的中心点,可改变。示例如下:
实际应用,通过改变锚点,使 Layer 围绕其的一个端点旋转,如指针钟表。
坐标系
iOS 和 macOS 的默认坐标系
image不同坐标系之间的转换:
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer*)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer*)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer*)layer;
Z轴坐标
- zPosition, 通常用来改变 Layer 的显示层级。
- anchorPointZ,确定3维图形的层级。
Hit Testing
- containsPoint: 接收一个在本图层之下的 CGPoint,如果这个点在图层 frame 范围内,则返回 YES。
- hitTest: 接收一个 CGPoint,返回图层本身,或包含这个坐标点的子图层。
视觉效果
圆角
- CornerRadius,控制图层角的曲率。默认0,直角。配合 masksToBounds 使用。
//set the corner radius on our layers
self.layerView2.layer.cornerRadius = 20.0f;
//enable clipping on the second layer
self.layerView2.layer.masksToBounds = YES;
图层边框
- borderWidth,以点为单位,默认0,浮点数。
- borderColor,默认黑色。
两者共同定义了图层边的绘制样式。
imagemyLayer.backgroundColor = [NSColor greenColor].CGColor;
myLayer.borderColor = [NSColor blackColor].CGColor;
myLayer.borderWidth = 3.0;
阴影
- shadowOpacity, 0~1之间的浮点值
- shadowColor, 控制阴影颜色
- shadowOffset,控制阴影方向和距离,CGSize,默认{0, -3},相对于 Y 轴有3个点的向上位移。
- shadowRadius,控制阴影模糊度
阴影裁剪
内层裁剪,外层阴影
shadowPath
CGPathRef 类型,指向 CGPath 的指针,CGPath 是一个 Core Graphics 对象,用来指定任意的一个矢量图形。
//create a square shadow
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
self.layerView1.layer.shadowPath = squarePath;
CGPathRelease(squarePath);
//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = circlePath;
CGPathRelease(circlePath);
图层蒙版
- mask, CALayer 类型,类似一个子图层,定义父图层的部分可见区域。其其他属性无关紧要,重要的是轮廓相关属性。
//create mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.layerView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
//apply mask to image layer
self.imageView.layer.mask = maskLayer;
拉伸过滤
- minificationFilter, 缩小图片,默认kCAFilterLinear
- magnificationFilter, 放大图片,默认kCAFilterLinear
在显示图片时,我们一般期望以最好的画质,原大小来显示图片,但某些情况下,需要显示与原图不同尺寸的图片时,这就需要用到拉伸过滤的算法:作用于原图的像素上并根据需要生产新的像素显示在屏幕上。
CALayer 中的3中拉伸过滤方法:
- kCAFilterLinear, 默认,双线性滤波算法。对多个像素取样最终生成新的值,得到一个平滑的拉伸。
- kCAFilterNearest, 最近取样算法,取样最近的但像素点而不管其他的颜色。适合具有垂直水平边界的图片,如更加小尺寸图片设置一个 LCD 风格的时钟。
- kCAFilterTrilinear, 较kCAFilterLinear,存储了多个大小情况下的图片,并三维取样,同时结合大小图的存储进而得到最终结果。
组透明
- alpha(UIView)/opacity(CALayer), 设置图层的透明度。子图层会受其影响。
设置自定义 Button,为0.5 alpha 的默认情况:
image
想要的效果是,Button 内子控件和 Button 透明度一样,但实际上却如上图。这时因为透明度混合叠加造成。
解决方案:
- 全局,在 Info.list 中设置 UIViewGroupOpacity = YES.
- shouldRasterize, 光栅化,在应用 alpha 前,图层和子图层被整合成一个整体的图片,此时,无透明度混合问题。需配合 rasterizationScale 使用,避免出现 Retina 屏幕像素化问题。
//enable rasterization for the translucent button
button2.layer.shouldRasterize = YES;
button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
变换
仿射变换
UIView 的 transform 属性,一个 CGAffineTransform 类型,基于二维空间的旋转,缩放和平移。
简单变换:
- CGAffineTransformMakeRotation(CGFloat angle)
- CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
- CGAffineTransformMakeTranslation(CGFLoat tx, CGFloat ty)
混合变换:
- CGAffineTransformRotate(CGAffinTransform t, CGFloat angle)
- CGAffineTransformScale(CGAffinTransform t, CGFloat sx, CGFloat sy)
- CGAffineTransformTranslate(CGAffinTransform t, CGFLoat tx, CGFloat ty)
- CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
初始常量
- CGAffineTransformIdentity
3D变换
CALayer 的 transform 属性,一个 CATransform3D 类型。
CATransform3D 是一个可以在三维空间内左变换的4*4的矩阵,如下:
常用的变换函数:
- CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
- CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
- CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
透视投影
CATransform3D 的透视效果通过⼀个矩阵中⼀个很简单的元素来控制: m34。 m34 ⽤于按⽐例缩放X和Y的值来计算到底要离视⾓多远。
imagem34 的默认值是0,我们可以通过设置 m34 为-1.0/d 来应⽤透视效果,d代表了想象中视⾓相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?
实际上并不需要,⼤概估算⼀个就好了。 通常为 500 ~ 1000.
//create a new transform
CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;
sublayerTransform
CATransform3D 类型,但和对⼀个图层的变换不同,它影响到所有的⼦图层。这意味着可以⼀次性对包含这些图层的容器做 变换,于是所有的⼦图层都⾃动继承了这个变换⽅法。
专用图层
CAShapeLayer
CAShapeLayer 是一个通过矢量图形而不是 bitmap 来绘制的图层子类。通过制定诸如颜色和线宽等属性,用 CGPath 来定义想要绘制的图形。
相比⽤Core Graphics直接向原始的CALayer 的内容中绘制路径,其优点如下:
- 渲染快速。其使用了硬件加速,绘制同一图形比 Core Craphics 快得多。
- 高效使用内存。不需要向 CALayer 一样创建一个寄宿图,无论多大,也不会占用太多的内存。
- 不会被图层边界裁掉。⼀个 CAShapeLayer 可以在边界之 外绘制。
- 不会出现像素化。当对 CAShapeLayer 做3D变换时,它不像⼀个有寄宿图的普通图层⼀样变得像素化。
创建一个路径
//create path
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100)
radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];
[path addLineToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(125, 225)];
[path moveToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(175, 225)];
[path moveToPoint:CGPointMake(100, 150)];
[path addLineToPoint:CGPointMake(200, 150)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;
//add it to our view
[self.containerView.layer addSublayer:shapeLayer];
创建圆角
使⽤ CAShapLayer 可以单独指定每个⾓。
如下,绘制⼀个有三个圆⾓⼀个直⾓的矩形:
//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight |
UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rectbyRoundingCorners:corners cornerRadii:radii];
CATextLayer
它以图层的形式包含 了 UILabel ⼏乎所有的绘制特性,并且额外提供了⼀些新的特性。
需设置 contentScale 属性,保证高清显示。
textLayer.contentsScale = [UIScreen mainScreen].scale;
CATransformLayer
不同于普通的 CALayer ,因为它不能显⽰它⾃⼰的内容。只有当存在了⼀个能作⽤域⼦图层的变换它才真正存在。 CATransformLayer 并不平⾯化它的⼦图层,所以它能够⽤于构造⼀个层级的3D结构.
CAGradientLayer
⽤来⽣成两种或更多颜⾊平滑渐变的图层。
基础渐变
CAGradientLayer 的colors 是一个数组,存储多个颜色值。
CAGradientLayer 的startPoint 和 endPoint。他们决定了渐变的⽅向。这两个参数是以单位坐标系进⾏的定义,所以左上⾓坐标是{0, 0},右下⾓坐标是{1, 1}。
//create gradient layer and add it to our container view
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:gradientLayer];
//set gradient colors
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];
//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
多重渐变
在基础渐变的基础上,通过 locations 属性来调整空间。
locations 属性是⼀个浮点数值的数组(以 NSNumber 包装)。这些浮点数定义了 colors 属性中每个不同颜⾊的位置,也是以单位坐标系进⾏标定。0.0代表着渐变的开始, 1.0代表着结束。
locations 数组并不是强制要求的,但是如果你给它赋值了就⼀定要确保 locations 的数组⼤⼩和 colors 数组⼤⼩⼀定要相 同,否则你将会得到⼀个空⽩的渐变。
//create gradient layer and add it to our container view
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:gradientLayer];
//set gradient colors
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id) [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];
//set locations
gradientLayer.locations = @[@0.0, @0.25, @0.5];
//set gradient start and end points gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
CAReplicatorLayer
其⽬的是为了⾼效⽣成许多相似的图层。它会绘制⼀个或多个图层的⼦图层,并在每个复制体上应⽤不同的变换。
- instanceCount 指定实例数量
- instanceTransform 指定实例的3D变换
重复图层
//create a replicator layer and add it to our view
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:replicator];
//configure the replicator
replicator.instanceCount = 10;
//apply a transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, 0, 200, 0);
transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
transform = CATransform3DTranslate(transform, 0, -200, 0);
replicator.instanceTransform = transform;
//apply a color shift for each instance
replicator.instanceBlueOffset = -0.1;
replicator.instanceGreenOffset = -0.1;
//create a sublayer and place it inside the replicator
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[replicator addSublayer:layer];
反射图形
使⽤ CAReplicatorLayer 并应⽤⼀个负⽐例变换于⼀个复制图层,就可以创建指定视图(或整个视图层次)内容的镜像图 ⽚,这样就创建了⼀个实时的『反射』效果。
指定⼀个继承于 UIView 的 ReflectionView, ⾃动产⽣内容的反射效果。
@implementation ReflectionView
+ (Class)layerClass
{
return [CAReplicatorLayer class];
}
- (void)setUp
{
//configure replicator
CAReplicatorLayer *layer = (CAReplicatorLayer*)self.layer;
layer.instanceCount = 2;
//move reflection instance below original and flip vertically
CATransform3D transform = CATransform3DIdentity;
CGFloat verticalOffset = self.bounds.size.height + 2;
transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
transform = CATransform3DScale(transform, 1, -1, 0);
layer.instanceTransform = transform;
//reduce alpha of reflection layer
layer.instanceAlphaOffset = -0.6;
}
- (id)initWithFrame:(CGRect)frame
{
//this is called when view is created in code
if ((self = [super initWithFrame:frame])) {
[self setUp];
}
return self;
}
@end
CAScorllLayer
CAScrollLayer 有⼀个 scrollToPoint ⽅法,它⾃动适应 bounds 的原点以便图层内容出现在滑动的地⽅。
CATiledLayer
为载⼊⼤图造成的性能问题提供了⼀个解决⽅案:将⼤图分解成⼩⽚然后将他们单独按需载⼊。
CAEmitterLayer
是⼀个⾼性能的粒⼦引擎,被⽤来创建实时例⼦动画如:烟雾,⽕,⾬等等这些效果。
//create particle emitter layer
CAEmitterLayer *emitter = [CAEmitterLayer layer];
emitter.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:emitter];
//configure emitter
emitter.renderMode = kCAEmitterLayerAdditive;
emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);
//create a particle template
CAEmitterCell *cell = [[CAEmitterCell alloc] init];
cell.contents = (__bridge id)[UIImage imageNamed:@"Spark.png"].CGImage; cell.birthRate = 150;
cell.lifetime = 5.0;
cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
cell.alphaSpeed = -0.4;
cell.velocity = 50;
cell.velocityRange = 50;
cell.emissionRange = M_PI * 2.0;
//add particle template to emitter
emitter.emitterCells = @[cell];
CAEAGLLayer
⽤来显⽰任意的OpenGL图形。
AVPlayerLayer
AVPlayerLayer ⽤来在iOS上播放视频的。他是⾼级接口 MPMoivePlayer 的底层实现,提供了显⽰视频的底层控制。
//get video URL
NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];
//create player and player layer
AVPlayer *player = [AVPlayer playerWithURL:URL];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
//set player layer frame and attach it to our view
playerLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:playerLayer];
//play the video
[player play];
隐式动画
当改变 CALayer 的可动画属性时,它会从原先的值平滑过渡到新的值。默认时长:0.25s。即不指定任何动画类型,在改变 CALayer 的属性时,Core Animation 来决定如何并且何时去做动画。
事务
Core Animation是如何判断动画类型和持续时间的呢?
实际上动画执⾏的时间取决于当前事务的设置,动画类型取决于图层⾏为。
事务:Core Animation 用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立即发生变化,而是当事务提交的时候开始使用一个动画过渡到新值。
事务使用 CATransaction 来进行管理。此类无实例方法,使用+begin
和 +commit
来入栈和出栈。 当改变图层属性值时,任何可动画的图层属性都会被加入到栈顶的事务,可以通过 +setAnimationDUration:
和 +animationDuration
来设置/获取值(默认0.25s)。
Core Animation 在每个 runloop 周期中自动开始一次新的事务,即使不显式的使用 [CATransaction begin] 开始一次事务,任何在一次 runloop 循环中属性的改变都会被集中起来,然后做一次0.25s 的动画。
完成块
基于 UIView 的block的动画允许你在动画结束的时候提供⼀个完 成的动作。
CATranscation:
- setCompletionBlock: 在动画结束后,执行一些操作。
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//add the spin animation on completion
[CATransaction setCompletionBlock:^{
//rotate the layer 90 degrees
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.colorLayer.affineTransform = transform;
}];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
图层行为
改变属性时, CALayer ⾃动应⽤的动画称作⾏为,当 CALayer 的属性被修改时候,它会调⽤ -actionForKey:
⽅ 法,传递属性的名称。 实质步骤如下:
- 图层首先检测它是否有委托,并且事发后实现 CALayerDelegate 协议指定的
-actionForLayer: forKey
方法。如果有,直接调用并返回结果。 - 如果没有委托,或委托未实现
-actionForLayer: forKey
方法,图层接着检查包含属性名称对应行为映射的 actions 字典。 - 如果 actions 字典没有包含对应的属性,那么图层接着在它的 style 字典接着搜索属性名。
- 最后,如果 style 中也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的
-defaultActionForKey:
方法。
为什么 UIView 关联的 CALayer 隐式动画被禁用?
UIView 对它关联的 CALayer 都扮演了一个委托的角色,并实现了 actionForLayer: forKey:
方法。当不在一个动画块的实现中,其返回 nil,此时将不会有动画发生;在动画块的范围之内,则返回一个非空值。
//test layer action when outside of animation block
NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//begin animation block
[UIView beginAnimations:nil context:nil];
//test layer action when inside of animation block
NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//end animation block
[UIView commitAnimations];
//output:
$ LayerTest[21215:c07] Outside: <null>
$ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>
另外禁用隐式动画的方法:
- setDisableActions: , 用来对所有属性打开或关闭隐式动画。
[CATransaction setDisableActions:YES];
通过设置 actions 字典自定义动画行为
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add a custom action
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
self.colorLayer.actions = @{@"backgroundColor": transition};
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];
呈现与模型
当改变⼀个图层的属性,属性值的确是⽴刻更新的(如果读取它的数据,会发现它的值在设置它的那⼀刻就已经⽣效了),但是屏幕上并没有马上发⽣改变。这是因为设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。
CALayer 是⼀个连接⽤户界⾯(就是MVC中的view)虚构的类,但是在界⾯本⾝这个场景下, CALayer 的⾏为更像是存储了视图如何显⽰和动画的数据模型。
每个图层属性的显⽰值都被存储在⼀个叫做呈现图层的独⽴图层当中,他可以通过 -presentationLayer
⽅法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指 定时刻当前外观效果。换句话说,可以通过呈现图层的值来获取当前屏幕上真正显⽰出来的值。
呈现图层的使用场景:
- 如果在实现⼀个基于定时器的动画,⽽不仅仅是基于事务的动画,这个时候准确地知道在某⼀时刻图层显⽰在什么位置就会对正确摆放图层很有⽤。
- 如果你想让你做动画的图层响应⽤户输⼊,你可以使⽤
-hitTest:
⽅法来判断指定图层是否被触摸,这时候对呈现图层⽽不是模型图层调⽤-hitTest:
会显得更有意义,因为呈现图层代表了⽤户当前看 到的图层位置,⽽不是当前动画结束之后的位置。[self.colorLayer.presentationLayer hitTest:point])
可通过 persentationLayer
获取当前图层的呈现层对象。
显式动画
对⼀些属性做指定的⾃定义动画,或者创建⾮线性动画,⽐如沿着任意⼀条曲线移动。
属性动画
即对可动画属性做动画处理。基于CABasicAnimation。
- CABasicAnimation
当更新属性的时候,我们需要设置⼀个新的事务,并且禁⽤图层⾏为。否则动画会发⽣两次,⼀个是显式的 CABasicAnimation ,另⼀次是隐式动画.
动画本⾝会作为⼀个参数传⼊委托的⽅法,也许你会认为可以控制器中把动画存储为⼀个属性,然后在回调⽤⽐较,但实际上并 不起作⽤,因为委托传⼊的动画参数是原始值的⼀个深拷贝,从⽽不是同⼀个值。
关键帧动画
- CAKeyframeAnimation
和 CABasicAnimation 类似, CAKeyframeAnimation是另⼀种UIKit没有暴露出来但功能强⼤的类, CAKeyframeAnimation 同样是 CAPropertyAnimation 的⼀个⼦类,它依然作⽤于单⼀的⼀个属性, 和 CABasicAnimation 不⼀样的是,它不限制于设置⼀个起始和结束的值,⽽是可以根据⼀连串随意的值来做动画。
关键帧起源于传动动画,意思是指主导的动画在显著改变发⽣时 重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过 关键帧推算出)将由熟练的艺术家来完成。
CAKeyframeAnimation 也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进⾏插⼊。
示例如下:
//create a keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[ (__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
注意到序列中开始和结束的颜⾊都是蓝⾊,这是因为CAKeyframeAnimation并不能⾃动把当前值作为第⼀帧(就像 CABasicAnimation 那样把 fromValue 设为 nil )。动画会在CAKeyframeAnimation 开始的时候突然跳转到第⼀帧的值,然后在动画结束的时候突然恢复到原始的值。所以为了动画的平滑特性,需要开始和结 束的关键帧来匹配当前属性的值。
通过 CGPath 指定动画路径。
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 64, 64);
shipLayer.position = CGPointMake(0, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto; //根据曲线方向自动跳转旋转方向
[shipLayer addAnimation:animation forKey:nil];
动画组
- CAAnimationGroup, 是另⼀个继承于 CAAnimation 的⼦类, 它添加了⼀个
animations
数组的属性,⽤来组合别的动画。
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add a colored layer
CALayer *colorLayer = [CALayer layer];
colorLayer.frame = CGRectMake(0, 0, 64, 64);
colorLayer.position = CGPointMake(0, 150);
colorLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.containerView.layer addSublayer:colorLayer];
//create the position animation
CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
animation1.keyPath = @"position";
animation1.path = bezierPath.CGPath;
animation1.rotationMode = kCAAnimationRotateAuto;
//create the color animation
CABasicAnimation *animation2 = [CABasicAnimation animation];
animation2.keyPath = @"backgroundColor";
animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
//create group animation
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1, animation2];
groupAnimation.duration = 4.0;
//add the animation to the color layer
[colorLayer addAnimation:groupAnimation forKey:nil];
过渡
对不可动画属性或整个图层来做动画处理。 基于 CATransition。
两个关键属性 type 和 subtype.
type, NSString 类型,值如下:
- kCATransitionFade,新图层渐入 ,默认值
- kCATransitionMoveIn,新图层顶部滑动进入
- kCATransitionPush,新图层把老图层推走,自己进入
- kCATransitionReveal,原始图片滑出来显示新的图层。
subtype, NSString 类型,值如下:
- kCATransitionFromRight
- kCATransitionFromLeft
- kCATransitionFromTop
- kCATransitionFromBottom
实际使用, 对图片替换加入渐变效果:
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
动画过程中取消动画
移除某一属性的动画:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有动画:
- (void)removeAllAnimations;
动画⼀旦被移除,图层的外观就⽴刻更新到当前的模型图层的值。⼀般说来,动画在结束之后被⾃动移除,除⾮设置 removedOnCompletion 为 NO ,如果你设置动画在结束之后不被⾃动移除,那么当它不需要的时候你要⼿动移除它;否则它会⼀直存在于内存中,直到图层被销毁。
图层时间
CAMediaTiming
协议
定义了一段动画内用来控制逝去时间的属性的集合, CALayer 和 CAAnimation 都实现了这个协议,所以时间可以被任意基于图层或动画的类控制。
- beginTime,开始之前的延迟时间
- timeOffset,时间偏移值
- repeatCount,动画重复次数, 不可和 repeatDuration 同时使用
- repeatDuration,动画重复总时间,不可和 repeatCount 同时使用
- duration,CFTimeInterval, 动画时长
- speed,动画速度,默认1.0,取值0~n,一个时间的倍数, 当等于0时相当于暂停。
- autoreverses,动画完成后是否反向动画到原始值
- fillMode,动画完成后,动画值的填充模式
- kCAFillModeForwards,动画完成后保持动画结束状态
- kCAFillModeBackwards,动画开始前立即进入动画初始状态
- kCAFillModeBoth, kCAFillModeForwards | kCAFillModeBackwards
- kCAFillModeRemoved,动画完车后移除动画结束状态
层级关系时间
马赫时间,Core Animation 中的全局时间,在设备上所有进程都是全局的:
//返回当前的绝对时间,秒,底层:mach_absolute_time()
CGTimeInterval time = CACurrentMediaTime();
为动画的实际测量提供一个相对值
每个 CALayer 和 CAAnimation 实例都有⾃⼰本地时间的概念, 是根据⽗图层/动画层级关系中的 beginTime , timeOffset 和 speed 属性计算。就和转换不同图层之间坐标关系⼀样, CALayer 同样也提供了⽅法来转换
不同图层之间的本地时间。如下:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
用来同步不同图层之间的 speed, timeOffset, beginTime.
暂停、倒回和快进
通过设置 speed = 0 可以暂停动画,但动画加入到图层之后便不能修改,所以不能对正在进⾏的动画使⽤这个属性。
给图层添加一个CAAnimation,实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画没用。可以通过 -animationForKey:
来查询图层正在进行的动画对象,但对其属性进行修改会抛出异常。
通过改变 UIWindow 的 speed 来控制整个应用的动画。
self.window.layer.speed = 100;
手动动画
- speed = 0, 禁用动画的自动播放
- 调整 timeOffset, 来控制动画的进度
- (void)viewDidLoad {
[super viewDidLoad];
//add the door
self.doorLayer = [CALayer layer];
self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
self.doorLayer.position = CGPointMake(150 - 64, 150);
self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
[self.containerView.layer addSublayer:self.doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add pan gesture recognizer to handle swipes
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:pan];
//pause all layer animations
self.doorLayer.speed = 0.0;
//apply swinging animation (which won't play because layer is paused)
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 1.0;
[self.doorLayer addAnimation:animation forKey:nil];
}
- (void)pan:(UIPanGestureRecognizer *)pan
{
//get horizontal component of pan gesture
CGFloat x = [pan translationInView:self.view].x;
//convert from points to animation duration
//using a reasonable scale factor
x /= 200.0f;
//update timeOffset and clamp result
CFTimeInterval timeOffset = self.doorLayer.timeOffset;
timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
self.doorLayer.timeOffset = timeOffset;
//reset pan gesture
[pan setTranslation:CGPointZero inView:self.view];
}
缓冲
用来控制动画速度,是动画看起来更加平滑自然。
动画速度
动画实际上就是⼀段时间内的变化,这就暗⽰了变化⼀定是随着 某个特定的速率进⾏。速率由以下公式计算⽽来:
velocity = change / time
这⾥的变化可以指的是⼀个物体移动的距离,时间指动画持续的 时长,⽤这样的⼀个移动可以更加形象的描述(⽐ 如 position 和 bounds 属性的动画),但实际上它应⽤于任意可以做动画的属性(⽐如 color 和 opacity )。
CAMediaTimingFunction , Core Animation 内嵌的一系列标准函数:
- kCAMediaTimingFunctionLinear
- kCAMediaTimingFunctionEaseIn 慢慢加速,全速停止
- kCAMediaTimingFunctionEaseOut 全速开始,慢慢减速停止
- kCAMediaTimingFunctionEaseInEaseOut
- kCAMediaTimingFunctionDefault ,类似kCAMediaTimingFunctionEaseInEaseOut,但缓冲速度略慢
通过 +setAnimationTimingFunction
或 +timingFUnctionWithName
来使用。
自定义缓冲函数
通过+functionWithControlPoints::::
来使用:
[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
三次贝塞尔曲线
CAMediaTimingFunction 函数的主要原则在于它把输⼊的时间转换成起点和终点之间成⽐例的改变。可以⽤⼀个简单的图标 来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是⼀条从起点开始的简单的斜线。
image这条曲线的斜率代表了速度,斜率的改变代表了加速度,原则上来说,任何加速的曲线都可以⽤这种图像来表⽰,但是 CAMediaTimingFunction 使⽤了⼀个叫做三次贝塞尔曲线的函数,它只可以产出指定缓冲函数的⼦集。
⼀个三次贝塞尔曲线通过四个点来定义,第⼀个和最后⼀个点代表了曲线的起点和终点,剩下中间两个点叫做 控制点,因为它们控制了曲线的形状,贝塞尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不⼀定要穿过它们。
image基于定时器的动画
NSTime
可以使用 NSTime 做动画,但非最佳选择。
主线程所做的任务:
- 处理触摸事件
- 发送和接受⽹络数据包
- 执⾏使⽤gcd的代码
- 处理计时器(NSTime)⾏为
- 屏幕重绘
当设置⼀个 NSTimer ,它会被插⼊到当前任务列表中,然后直到指定时间过去之后才会被执⾏。但是何时启动定时器并没有 ⼀个时间上限,⽽且它只会在列表中上⼀个任务完成之后开始执⾏。这通常会导致有⼏毫秒的延迟,但是如果上⼀个任务过了很 久才完成就会导致延迟很长⼀段时间。
屏幕重绘的频率是⼀秒钟六⼗次,但是和定时器⾏为⼀样,如果列表中上⼀个任务执⾏了很长时间,它也会延迟。这些延迟都是⼀个 随机值,于是就不能保证定时器精准地⼀秒钟执⾏六⼗次。有时候发⽣在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起 来就是动画卡了。有时候定时器会在屏幕更新的时候执⾏两次,于是动画看起来就跳动了。
优化途径:
- ⽤CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
- 基于真实帧的持续时间⽽不是假设的更新频率来做动画。
- 调整动画计时器的 run loop 模式,这样就不会被别的事件⼲扰。
CADisplayLink
CADisplayLink 类似 NSTimer,但是和 timeInterval 以秒为单位不同, 有⼀个整型的 frameInterval 属性,指定 CADisplayLink 了间隔多少帧之后才执⾏。默认值是1,意味着每次屏幕更新之 前都会执⾏⼀次。但是如果动画的代码执⾏起来超过了六⼗分之 ⼀秒,你可以指定 frameInterval 为2,就是说动画每隔⼀帧执 ⾏⼀次(⼀秒钟30帧)或者3,也就是⼀秒钟20次,等等。
⽤CADisplayLink ⽽不是 NSTimer ,会保证帧率⾜够连续,使得动画看起来更加平滑,但即使 CADisplayLink 也不能保证每⼀ 帧都按计划执⾏,⼀些失去控制的离散的任务或者事件(例如资 源紧张的后台程序)可能会导致动画偶尔地丢帧。当使⽤NSTimer 的时候,⼀旦有机会计时器就会执行,但 CADisplayLink 却不⼀样:如果它丢失了帧,就会直接忽略,然后在下⼀次更新的时候接着运⾏。
计算帧的持续时间
在每帧开始刷新的时候⽤ CACurrentMediaTime() 记录当前时间,然后和上⼀帧记录的时间去⽐较。
通过⽐较这些时间,我们就可以得到真实的每帧持续的时间,然后代替硬编码的六⼗分之⼀秒。
- (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;
}
}
Run Loop 模式
- NSDefaultRunLoopMode
- NSRunLoopCommonModes
- UITrackingRunLoopMode
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
性能调优
探究⼀些动画运⾏慢的原因,以及如何去修复这些问题。
CPU & GPU
关于绘图和动画有两种处理的⽅式:CPU和GPU。在现代iOS设备中,都有可以运⾏不同软件的可编程芯⽚,但是由于历史原因,可以说CPU所做的⼯作都在软件层⾯,⽽GPU在硬件层⾯。
总的来说,可以⽤软件(使⽤CPU)做任何事情,但是对于图像处理,通常⽤硬件(使⽤GPU)会更快,因为GPU使⽤图像对⾼度并⾏ 浮点运算做了优化。由于某些原因,我们想尽可能把屏幕渲染的⼯作交给硬件去处理。问题在于GPU并没有⽆限制处理性能,⽽ 且⼀旦资源⽤完的话,性能就会开始下降了(即使CPU并没有完全占⽤)
⼤多数动画性能优化都是关于智能利⽤GPU和CPU,使得它们都不会超出负荷。于是⾸先需要知道Core Animation是如何在这两个处理器之间分配⼯作的。
动画的管理
动画和屏幕上组合的图层实际上被⼀个单独的进程管理,⽽不是应⽤程序。这个进程就是所谓的渲染服务。在iOS5和之前 的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做 BackBoard 。
当你执行一个动画时,这个工作在应用内被分解成四个独立的阶段:
- 布局:准备视图/图层的层级关系,以及设置图层属性(如位置、背景色、边框等等)的阶段。
- 显示:图层的寄宿图被绘制的阶段。绘制可能涉及
-drawRect
和-drawLayer:inContext:
方法的调用。 - 准备:Core Animation 准备发送动画数据到渲染服务的阶段。也是 Core Animation 将要执行一些别的事务如解码动画过程中将要显示的图片的时间点。
- 提交:Core Animation 打包所有图层和动画属性,然后通过 ICP( 内部处理通信)发送到渲染服务进行显示。
以上阶段完成后,⼀旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另⼀个叫做渲染树的图层 树。使⽤这个树状结构,渲染服务对动画的每⼀帧做出如下⼯作:
- 对所有的图层计算中间值,设置 OpenGLS 几何形状来执行渲染;
- 在屏幕上渲染可见的图形;
总而言之,共6个阶段,前5个阶段在软件层面执行(通过 CPU),最后一个阶段被 GPU 执行。而能被实际控制只有2个阶段:布局和显示。
在这两个阶段,可以决定哪些由 CPU 执行,哪些交给 GPU 执行。
GPU 的相关操作
GPU针对特定的任务进行优化:它采用图像和几何(三角形),执行转换,应用纹理和混合,然后将它们放到屏幕上。
一般来说,CALayer的大部分属性都是使用GPU绘制的。例如,如果你设置了图层背景或边框颜色,那么可以使用彩色三角形来有效地绘制这些颜色。如果您将一个图像分配给内容属性——即使您使用了缩放和裁剪——它会使用纹理三角形,而不需要任何软件绘图。
可能减缓图层绘制的几个因素:
- 太多的几何结构:这发⽣在需要太多的三⾓板来做变换,以 应对处理器的栅格化的时候。现代iOS设备上的图形芯片可以处理数百万个三角形,因此当涉及到核心动画时,几何图形实际上不太可能成为GPU瓶颈。但是由于图层必须经过预处理并通过IPC发送到呈现服务器(图层是相当重的对象,由几个子对象组成),太多的图层会导致CPU瓶颈。这限制了可以同时显示的图层的实际数量。
- 重绘:这主要是由重叠的半透明层引起的。GPU有一个有限的填充率(用颜色填充像素的速率),所以过度绘制(每个帧中填充相同像素的倍数)是可以避免的。
- 离屏渲染:当一个特定的效果无法实现时,就会出现这种情况。直接绘制到屏幕上,但必须先将其绘制到屏幕外的图像上下文中。Offscreen绘图是一个通用术语,它可以应用于CPU或基于GPU的绘图,但无论哪种方式,都需要为Offscreen映像分配额外的内存,并在绘图上下文之间切换,这两种方式都将降低GPU性能。使用某些图层效果,例如圆角、图层蒙板、投影阴影或图层光栅化,将会迫使核心动画预先渲染图层的屏幕。这并不意味着需要完全避免这些影响,只是需要意识到它们可能会产生性能影响。
- 过⼤的图⽚ - 如果视图绘制超出GPU⽀持的2048x2048或者 4096x4096尺⼨的纹理,就必须要⽤CPU在图层每次显⽰之前 对图⽚预处理,同样也会降低性能。
CPU 的相关操作
在动画开始之前,核心动画中的大部分CPU工作都是提前完成的。这很好,因为这意味着它通常不会影响帧速率,但它也是不好的,因为它可能延迟动画的开始,使你的界面看起来没有响应。
可能降低动画的启动速度的CPU操作:
- 布局计算:如果视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗⼀部分时间。特别是使⽤⾃动布局尤为明显。
- 视图懒加载:iOS只会当视图控制器的视图显⽰到屏幕上时才会加载它。这对内存使⽤和程序启动时间很有好处,但是当 呈现到屏幕上之前,按下按钮导致的许多⼯作都会不能被及时响应。⽐如控制器从数据库中获取数据,或者视图从⼀个 nib⽂件中加载,或者涉及IO的图⽚显⽰,都会⽐CPU正常操作慢得多。
- Core Graphics 绘制:如果对视图实现了
drawRect
⽅法,或者 CALayerDelegate 的-drawLayer:inContext:
⽅法,那么 在绘制任何东西之前都会产⽣⼀个巨⼤的性能开销。为了⽀持对图层内容的任意绘制,Core Animation必须创建⼀个内存 中等⼤⼩的寄宿图⽚。然后⼀旦绘制结束之后,必须把图⽚数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘 制就会变得⼗分缓慢,所以在⼀个对性能⼗分挑剔的场景下这样做⼗分不好。 - 解压图片: PNG或JPEG压缩之后的图⽚⽂件会⽐同质量的位图⼩得多。但是在图⽚绘制到屏幕上之前,必须把它扩展 成完整的未解压的尺⼨(通常等同于图⽚宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解 码图⽚。根据加载图⽚的⽅式,第⼀次对图层内容赋值的时候(直接或者间接使⽤ UIImageView )或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于⼀个较⼤的图⽚,都会占⽤⼀定的时间。
IO 相关操作
上下⽂中的IO(输⼊/输出)指的是例如内存或者⽹络接口的硬件访问。⼀些动画可能需要从内存(甚⾄是远程URL)来加载。一个典型的例子是两个视图控制器之间的转换,它可以延迟加载一个nib文件及其内容,或者是一个图像的旋转木马,这些图像可能太大,无法存储在内存中,因此需要动态地载入carousel滚动条。
IO比正常的内存访问要慢得多,所以如果动画是IO绑定的,这可能是一个大问题。一般来说,这必须通过使用聪明的、但不合适的技术来解决,比如线程、缓存和投机性加载(提前加载你不需要的东西,但是预测你将来会需要)。
度量而不是猜测
- 使用真机测试,可支持的性能最差的设备,而不是模拟器
- 使用 Build 配置,而不是 Debug 配置
- 保持一致的帧率(60FPS左右),可以在程序中⽤ CADisplayLink 来测量帧率
Instruments
- Time Profiler-用于测量CPU使用量,按方法/功能划分。
- COre Animation-用于调试各种核心动画的性能问题。
Time Profiler
时间分析器⼯具⽤来检测CPU的使⽤情况。它可以告诉我们程序中的哪个⽅法正在消耗⼤量的CPU时间。使⽤⼤量的CPU并不⼀定是个问题 - 你可能期望动画路径对CPU⾮常依赖,因为动画往往是iOS设备中最苛刻的任务。
但是如果你有性能问题,查看CPU时间对于判断性能是不是和 CPU 相关,以及定位到函数都很有帮助。
时间分析器有⼀些选项来帮助我们定位到我们关⼼的的⽅法。 可以使⽤左侧的复选框来打开。其中最有⽤的是如下⼏点:
- Separate by Thread-这组方法由它们执行的线程来处理。如果代码在多个线程之间被分割,这将有助于识别是哪些线程导致了问题。
- Hide System Libraries-隐藏了苹果框架中所有的方法和函数。这有助于确定哪些方法包含瓶颈。由于不能优化框架方法,所以经常切换这种方法来帮助缩小问题的范围,以解决实际上可以解决的问题。
- Show Obj-C Only-隐藏除了Objective-C之外的所有代码。⼤ 多数内部的Core Animation代码都是⽤C或者C++函数,所以 这对我们集中精⼒到我们代码中显式调⽤的⽅法就很有⽤。
Core Animation
Core Animation⼯具也提供了⼀系列复选框选项来帮助调试渲染瓶颈:
- Color Blended Layers - 这个选项基于渲染程度对屏幕中的混合区域进⾏绿到红的⾼亮(也就是多个半透明图层的叠 加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸⾸之⼀。
- Color Hits Green and MissesRed - 当使⽤ shouldRasterizep 属性的时候,耗时的图层绘制会被缓存,然后当做⼀个简单的 扁平图⽚呈现。当缓存再⽣的时候这个选项就⽤红⾊对栅格化图层进⾏了⾼亮。如果缓存频繁再⽣的话,就意味着栅格 化可能会有负⾯的性能影响了。
- Color Copied Images - 有时候寄宿图⽚的⽣成意味着Core Animation被强制⽣成⼀些图⽚,然后发送到渲染服务器,⽽不是简单的指向原始指针。这个选项把这些图⽚渲染成蓝⾊。复制图⽚对内存和CPU使⽤来说都是⼀项⾮常昂贵的操作,所以应该尽可能的避免。
- Color Immediately - 通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜⾊。对某些效果来说,这显然太 慢了。这个选项就可以⽤来设置每帧都更新(可能会影响到渲染性能,⽽且会导致帧率测量不准,所以不要⼀直都设置它)。
- Color Misaligned Images - 这⾥会⾼亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图⽚(也就是⾮整型坐 标)。这些中的⼤多数通常都会导致图⽚的不正常缩放,如果把⼀张⼤图当缩略图显⽰,或者不正确地模糊图像,那么 这个选项将会帮你识别出问题所在。
- Color Offscreen-Rendered Yellow - 这⾥会把那些需要离屏渲染的图层⾼亮成黄⾊。这些图层很可能需要⽤ shadowPath 或者 shouldRasterize 来优化。
- Color OpenGL Fast Path Blue - 这个选项会对任何直接使⽤ OpenGL 绘制的图层进⾏⾼亮。如果仅仅使⽤UIKit或者Core Animation的API,那么不会有任何效果。如果使⽤ GLKView 或者 CAEAGLLayer ,那如果不显⽰蓝⾊块的话就意味着你正在强制CPU渲染额外的纹理,⽽不是绘制到屏幕。
- Flash Updated Regions - 这个选项会对重绘的内容⾼亮成黄⾊(也就是任何在软件层⾯使⽤Core Graphics绘制的图层)。这种绘图的速度很慢。如果频繁发⽣这种情况的话, 这意味着有⼀个隐藏的bug或者说通过增加缓存或者使⽤替代⽅案会有提升性能的空间。
这些⾼亮图层的选项同样在iOS模拟器的调试菜单也可⽤。使⽤iOS模拟器来验证问题是否解决也是⽐真机测试更有效的。
高效绘图
软件绘图
绘图,通常在核心动画的上下文中使用,指的是软件绘制(即不是GPU辅助的绘图)。iOS的软件绘制主要是使用Core Graphics框架,虽然有时是必要的,但与Core Animation和OpenGL的硬件加速渲染和合成相比,它确实很慢。
除了速度慢之外,软件绘图还需要大量的内存。CALayer需要相对较少的内存;它只是在RAM中占据任何重要空间的备份映像。即使您直接为内容属性指定了一个映像,它也不会使用任何额外的内存来存储映像的单个(未压缩的)副本;如果同一个映像被用作多个图层的内容,那么内存将在它们之间共享,而不是复制。
但是⼀旦你实现了CALayerDelegate 协议中的 drawLayer:inContext: ⽅法或者 UIView 中的 -drawRect: ⽅法 (其实就是前者的包装⽅法),图层就创建了⼀个绘制上下⽂, 这个上下⽂需要的⼤⼩的内存可从这个算式得出:图层宽图层 ⾼4字节,宽⾼的单位均为像素。对于⼀个在Retina iPad上的全 屏图层来说,这个内存量就是 204815264字节,相当于12MB 内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。
由于软件绘图非常昂贵,除非绝对必要,否则应该避免重新绘制视图。提⾼绘制性能的秘诀就在于尽量避免去绘制。
矢量图形
⽤Core Graphics来绘图的⼀个通常原因就是只是⽤图⽚或是图层效果不能轻易地绘制出⽮量图形。⽮量绘图包含⼀下这些:
- 任意多边形(不仅仅是⼀个矩形)
- 斜线或曲线
- ⽂本
- 渐变
Core Animation为这些图形类型的绘制提供了专门的类,并给他们提供硬件⽀持。 CAShapeLayer 可以绘制多边形,直线和曲线。CATextLayer 可以绘制⽂本。CAGradientLayer⽤来绘制渐变。这些总体上都⽐Core Graphics更快,同时他们也避免了创造⼀个寄宿图。
脏矩形(Dirty Rectangles)
为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称 作『脏区域』。在实际应⽤中,鉴于⾮矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,⽽这个位置 就是『脏矩形』。
异步绘制
UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这 意味着绘制会打断⽤户交互,甚⾄让整个app看起来处于⽆响应状态。
针对这个问题,有⼀些⽅法可以⽤到:⼀些情况下,可以推测性地提前在另外⼀个线程上绘制内容,然后将由此绘出的图 ⽚直接设置为图层的内容。这实现起来可能不是很⽅便,但是在特定情况下是可⾏的。Core Animation提供了⼀些选择: CATiledLayer 和 drawsAsynchronously 属性。
CATiledLayer
除了将图层再次分割成独⽴更新的⼩块(类似于脏矩形⾃动更新的概念), CATiledLayer 还有⼀个有趣的特性:在多个线程中为每 个⼩块同时调⽤ -drawLayer:inContext: ⽅法。这就避免了阻塞⽤户交互⽽且能够利⽤多核⼼新⽚来更快地绘制。只有⼀个⼩块的 CATiledLayer 是实现异步更新图⽚视图的简单⽅法。
drawsAsynchronously
drawsAsynchronously 属性对传⼊ -drawLayer:inContext: 的CGContext进⾏改动,允许CGContext延缓绘制命令的执⾏以⾄于不阻塞⽤户交互。
它与 CATiledLayer 使⽤的异步绘制并不相同。它⾃⼰的 drawLayer:inContext: ⽅法只会在主线程调⽤,但是CGContext 并不等待每个绘制命令的结束。相反地,它会将命令加⼊队列,当⽅法返回时,在后台线程逐个执⾏真正的绘制。
根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好 (⽐如我们的绘图应⽤,或者诸如 UITableViewCell 之类的)。
图形 IO
加载与延迟
- 异步线程加载,主线程显示
- 使用 CGContenxt 在后台线程强制解压图片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image on main thread, but only if index
still matches up dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image;
}
});
});
缓存
- imageNamed:
- 自定义缓存:(合适的缓存键值,提前缓存,缓存失效,缓存回收)
- NSCache
文件格式
PNG 与 JPEG 并没有明显的性能差距。
图层性能
隐式绘制
创建寄宿图的几种方式:
显式:
- 通过 Core Graphics 直接绘制;
- 直接载入一个图片文件赋值给 contents;
- 事先绘制一个屏幕之外的 CGContext 上下文给 Contents;
隐式:
- 使用特定的图层属性;
- 特定的视图;
- 特定的图层子类;
文本
- CATextLayer
- UILabel
两者都是基于 Core Text, 直接将文本绘制在图层的寄宿图。都是用了软件绘制的方式。
不论如何,尽可能地避免改变那些包含⽂本的视图的frame, 因为这样做的话⽂本就需要重绘。例如,如果你想在图层的⾓落 ⾥显⽰⼀段静态的⽂本,但是这个图层经常改动,你就应该把⽂ 本放在⼀个⼦图层中。
光栅化
启⽤ shouldRasterize 属性会将图层绘制到⼀个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的 contents 和⼦图层。如果有很多的⼦图层或者有复杂的效果应⽤,这样做就会⽐重绘所有事务的所有帧划得来得多。但是光 栅化原始图像需要时间,⽽且还会消耗额外的内存。
如果使⽤得当,光栅化可以提供很⼤的性能优势,但是⼀定要避免作⽤在内容不断变动的图层上,否则它缓存⽅⾯的好处就会消失,⽽且会让性能变的更糟。
为了检测是否正确地使⽤了光栅化⽅式,⽤Instrument查看⼀下Color Hits Green 和 Misses Red项⽬,是否已光栅化图像被频 繁地刷新(这样就说明图层并不是光栅化的好选择,或则⽆意间触发了不必要的改变导致了重绘⾏为)。
离屏渲染
当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被触发了。屏幕外渲染并不意味着软件 绘制,但是它意味着图层必须在被显⽰之前在⼀个屏幕外上下⽂中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:
- 圆角(配合 maskToBounds)
- 图层蒙版
- 阴影
有时候可以把那些需要屏幕外绘制的图层开启光栅化以作为⼀个优化⽅式,前提是这些图层并不会被频繁地重绘。
对于那些需要动画⽽且要在屏幕外渲染的图层来说,可以⽤ CAShapeLayer , contentsCenter 或者 shadowPath 来获得同样的表现⽽且较少地影响到性能。
CAShapeLayer
如果想要的只是圆⾓且沿着矩形边界裁切,同时还不希望引起性能问题。其实你可以⽤现成的 UIBezierPath 的构造器 +bezierPathWithRoundedRect:cornerRadius:
。这样做并不会⽐直接⽤ cornerRadius 更快,但是它避免了性能问题。
//create shape layer
CAShapeLayer *blueLayer = [CAShapeLayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.fillColor = [UIColor blueColor].CGColor;
blueLayer.path = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
可伸缩图片
另⼀个创建圆⾓矩形的⽅法就是⽤⼀个圆形内容图⽚并结合 contensCenter 属性去创建⼀个可伸缩图⽚。
//create layer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0);
blueLayer.contentsScale = [UIScreen mainScreen].scale;
blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
使⽤可伸缩图⽚的优势在于它可以绘制成任意边框效果⽽不需要额外的性能消耗。举个例⼦,可伸缩图⽚甚⾄还可以显⽰出矩形阴影的效果。
shadowPath
如果图层是⼀个简单⼏何图形如矩形或者圆⾓矩形(假设不包含任何透明部分或者⼦图层),创建出⼀个对应形状的阴影路径就⽐较容易,⽽且Core Animation绘制这个阴影也相当简单,避免了屏幕外的图层部分的预排版需求。这对性能来说很有帮助。
混合和过度绘制
GPU每⼀帧可以绘制的像素有⼀个最⼤限制 (就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同⼀区域的话,掉帧就可能发⽣了。
GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出⼀个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同 样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。
所以为了加速处理进程,不到必须时刻不要使⽤透明图层。任何情况下,应该这样做:
- 给视图的 backgroundColor 设置一个固定的,不透明的颜色;
- 设置 opaque 属性为 YES;
这样做减少了混合⾏为(因为编译器知道在图层之后的东西都不会对最终的像素颜⾊产⽣影响)并且计算得到了加速,避免了 过度绘制⾏为因为Core Animation可以舍弃所有被完全遮盖住的图层,⽽不⽤每个像素都去计算⼀遍。
如果⽤到了图像,尽量避免透明除⾮⾮常必要。
如果是⽂本的话,⼀个⽩⾊背景⾊会⽐透明背景要更⾼效。
明智地使⽤ shouldRasterize 属性,可以将⼀个固定的图层体系折叠成单张图⽚,这样就不需要每⼀帧重新合成了,也 就不会有因为⼦图层之间的混合和过度绘制的性能问题。
减少图层数量
初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成 OpenGL⼏何图形,这些是⼀个图层的⼤致资源开销。事实上, ⼀次性能够在屏幕上显⽰的最⼤图层数量也是有限的。确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。
裁剪
在对图层做任何优化之前,需要确定不是在创建⼀些不可见的图层,图层在以下⼏种情况下不可见:
- 图层在屏幕边界之外,或是在父图层边界之外;
- 完全在一个不透明图层之后;
- 完全透明;
Core Animation⾮常擅长处理对视觉效果⽆意义的图层。但是经常性地,⾃⼰的代码会⽐Core Animation更早地想知道⼀个 图层是否是有⽤的。理想状况下,在图层对象在创建之前就想知 ,以避免创建和配置不必要图层的额外⼯作。
对象回收
处理巨⼤数量的相似视图或图层时还有⼀个技巧就是回收他们。对象回收在iOS颇为常见; UITableView 和 UICollectionView 都有⽤到,
MKMapView 中的动画pin码也有⽤到,还有其他很多例⼦。
对象回收的基础原则就是需要创建⼀个相似对象池。当⼀个对象的指定实例(本例⼦中指的是图层)结束了使命,你把它添 加到对象池中。每次当需要⼀个实例时,就从池中取出⼀个。当且仅当池中为空时再创建⼀个新的。
这样做的好处在于避免了不断创建和释放对象(相当消耗资 源,因为涉及到内存的分配和销毁)⽽且也不必给相似实例重复赋值。
Core Graphics 绘制
当排除掉对屏幕显⽰没有任何贡献的图层或者视图之后,长远看来,可能仍然需要减少图层的数量。例如,如果正在使⽤多个 UILabel 或者 UIImageView 实例去显⽰固定内容,可以把他们全部替换成⼀个单独的视图,然后⽤ -drawRect: ⽅法绘制出那些复杂的视图层级。
这个提议看上去并不合理,因为⼤家都知道软件绘制⾏为要⽐ GPU合成要慢⽽且还需要更多的内存空间,但是在因为图层数量 ⽽使得性能受限的情况下,软件绘制很可能提⾼性能,因为它避免了图层分配和操作问题。
使⽤ CALayer 的 -renderInContext: ⽅法,你可以将图层及 其⼦图层快照进⼀个Core Graphics上下⽂然后得到⼀个图⽚,它 可以直接显⽰在 UIImageView 中,或者作为另⼀个图层 的 contents 。不同于 shouldRasterize —— 要求图层与图层树相关联 —— ,这个⽅法没有持续的性能消耗。
当图层内容改变时,刷新这张图⽚的机会取决于你(不同于 shouldRasterize ,它⾃动地处理缓存和缓存验证),但是⼀ 旦图⽚被⽣成,相⽐于让Core Animation处理⼀个复杂的图层树,节省了相当可观的性能。
网友评论