笔记主要来源iOS核心动画高级技巧,感谢作者与翻译的各位同学.
一、图层树
- UIView、NSView都有一个关联的CALayer,不用CALayer处理所有事情的原因是为了职责分离,在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同.
二、寄宿图
-
UIView有个contentMode属性,经常在它的子类UIImageView上使用,而实际上这个属性是对应CALayer的contentGravity属性的,并且CALayer的contentGravity属性是NSString类型,可选的常量值如下:
* KCAGravityCenter * KCAGravityTop * KCAGravityBottom * KCAGravityLeft * KCAGravityRight * KCAGravityTopRight * KCAGravityBottomLeft * KCAGravityBottomRight * KCAGravityResize * KCAGravityResizeAspect * KCAGravityResizeAepectFill
contentScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认值为1.0,contentScale属性属于支持高分辨率屏幕机制的一部分(与图片的2x,3x使用相对应,iPhone4、5、6系列均为2.0,iPhone6p系列均为3.0).为了使图片显示争取可设置
layer.contentScale = [UISCreen mainScreen].scale;
maskToBounds属性与UIView的
clipsToBounds
属性对应,决定是否显示超出边界的内容.-
contentsRect属性CALayer的
contentsRect
属性允许我们在图层边框里显示寄宿图的一个子域.和bounds
,frame
不同,contentsRect
不是按点来计算的,它使用了单位坐标,单位坐标指定在0和1之间,是一个相对值.iOS的坐标系统:点 —— 在iOS和Mac OS中最常见的坐标体系。点就是虚拟的像素。也被称作逻辑像素。在标准设备上,一个点就是一个像素点,但是在retina设备上,一个点等于2*2个像素。
像素——物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以你要清楚在Retina设备和普通设备上,他们表现出来了不同的大小。
-
单位——对于与图片或者图层边界相关的显示,单位坐标是一个方便的度量方式,当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。
默认的
contentsRect
是{0,0,1,1},这意味着整个寄宿图默认都是可见的.详细的使用参考Layer的寄宿图contents属性. contentsCenter属性.可以用Interface Builder探测窗口控制
contentsCenter
属性(View里).-
Custom Drawing
-drawRect:
方法没有默认的实现,如果不需要自定义的绘制,就不要创建这个方法,这会造成CPU资源和内存浪费。虽然-drawRect:
方法是一个UIView方法,事实上都是底层的CALayer安排了重绘工作和保存了因此产生的图片。CALayer有一个可选的delegate属性,实现了CALayerDelegate
,当CALayer需要一个内容特定的信息时,就会从协议中请求。当需要被重绘时,CALayer会请求它的代理来给他一个寄宿图来显示。它通过调用这个方法做到:-(void)displayLayer:(CALayer *)layer
如果代理不实现
-displayLayer:
方法,CALayer就会转而尝试调用下面这个方法:-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
提供了以上方法,可以使用CALayer的
display
去强制layer去绘制。UIView创建了它的宿主图层时,它就会自动地把图层的delegate设置为它自己,并提供了一个
-displayLayer:
的实现。所以一般绘制调用UIView的-drawRect:
方法就可以。
三、图层几何学
-
布局
UIView的三个属性
frame
,bounds
,center
对应CALayer的三个属性frame
,bounds
,postion
。center
和position
都代表了相对于父图层anchorPoint
所在的位置.视图的
frame
,bounds
和center
属性仅仅是存取方法,当操纵视图的frame
,实际上是在改变视图下方CALayer
的frame
,不能够独立于图层之外改变视图的frame
。对于视图或者图层来说,
frame
并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据bounds
,bounds
和transform
计算而来,所以当其中任何一个值发生改变,frame都会变化。相反,改变frame的值同样会影响到他们当中的值。 锚点(
anchorPoint
)可以参考这篇文章-
坐标系.
一个图层的
position
依赖于它父图层的bounds
,如果父图层发生了变化,它的所有子图层也会跟着移动.CALayer
给不同坐标系之间的图层转换提供了一些工具类方法:- (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;
-
Hit Testing.
CALayer
并不关心任何响应事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:
和-hitTest:
.hitTest:
方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子点图层。
需要注意的是,当调用图层的-hitTest:
方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理事件类似).zPosition
属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。
四、视觉效果
圆角.
-
图层边框.
边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前。边框并会把寄宿图或者子图层的形状计算进来,如果图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明的蒙版,边框仍然会沿着图层的边界绘制出来。
-
阴影.
shadowOpacity
shadowColor
-
shadowOffest
,该属性控制着阴影的方向和距离。它是一个CGSize
的值,宽度控制着阴影横向的位移,高度控制着纵向的位移。shadowOffese
的默认值是{0,-3},即阴影相对于Y轴有3个点的向上位移。 -
shadowRadius
,该属性控制着阴影的模糊的,当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然.(所以不要设置为0)。 -
shadowPath
.
-
图层蒙版.
mask
图层的color
属性是无关紧要的,真正重要的是图层的轮廓。mask
属性就像是一个饼干切割机,mask
图层实心的部分被保留下来,其他的则会被抛弃。 拉伸过滤.
-
组透明.
UIView
的alpha
属性与CALayer
的opacity
属性相对应。
五、变换
-
仿射变换.
UIView的
transform
与CALayer的affineTransform
相对应,都是CGAffineTransform
类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform
是一个可以和二维空间向量做乘法的3*2矩阵。 -
3D变换
CALayer的属性
transform
是CATransform3D
类型 ,CATransform3D
也是一个矩阵,是一个可以在3维空间内做变换的4*4矩阵。
六、专用图层
- CAShapeLayer(十分重要)
- CATextLayer
- CAGradientLayer.
CAGradientLayer
是用来生成两种或更多颜色平滑渐变的。 - CAScrollLayer
- AVPlayerLayer(十分重要).
七、隐式动画
-
事务
Core Animation基于一个假设,屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确的关闭,否则它会一直存在.
事务事件上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发送变化。而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过
CATransaction
类来做管理,CATransaction
没有属性或者实例方法,并且也不能用+alloc
和-init
方法创建它。但是可以用+begin
和+commint
分别来入栈和出栈.Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用
[CATransaction begin]
开始一次事务,任何在一次run loop循环中属性的改变都会被机种起来,然后做一次0.25秒的动画。UIView
有两个方法,+beginAnimations:context:
和+commitAnimations
,和CATransaction
的begin
和commit
方法类似。实际上在+beginAnimations:context
和+commitAnimations
之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction
的原因。在iOS4中,苹果对UIView添加了一种基于block的动画:+animateWithDuration:animations:
.实质上它们都是在做同样的事情。 完成块
-
图层行为
Core Animation通常对
CALayer
的所有属性(可动画的属性)做隐式动画,但UIView把它关联的图层的这个属性关闭了。 -
呈现于模型
CALayer
的属性行为其实很不正常,因为改变一个图层的属性并没有立即生效,而是通过一段时间渐变更新。当你改变一个图层的属性,属性值的确立刻更新的,但是在屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,它只是定义了图层动画结束之后将要变化的外观。
在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着
CALayer
除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。通过
presentationLayer
来获取屏幕上真正显示出来的值;而在presentationLayer
上调用-modelLayer
将会返回它正在呈现所依赖的layer
.如果想让做动画的图层响应用户输入,可以使用
-hitTest:
方法,来判断呈现图层(presentationLayer)是否被触摸来响应。(具体代码请参考原书籍)
八、显式动画
-
属性动画
-
CABasicAnimation
当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的CABasicAnimation,另一次是因为隐式动画。(在设置属性的时候)
通过
keyPath
、fromValue
、toValue
设置动画,fromValue
可以不设置.byValue
是一个相对值,会从当前的值上动画到byValue
的值。 -
CAKeyframeAnimation(关键帧动画)
属性动画通过设置
keyPath
和values
属性得以实现关键帧动画;还可以通过设置CGPath
来实现一个路径相关的动画,另外,设置rotationMode
属性为KCAAnimationRotateAuto
,图层将会根据曲线的切线自动旋转。 -
虚拟属性
transform.rotation
transform.postion
transform.scale
keyPath
可以设置为以上三个虚拟属性,然后再通过byValue
、toValue
来设置需要动画的值。
-
CAAnimationGroup(动画组)
-
过渡
最常见到的过渡就是在childViewController之间切换view.
为了创建一个过渡动画,将使用
CATransiton
,同样是另一个CAAnimation
的子类,和别的子类不同,CATransiton
有一个Type
和subtype
来标识变换效果.type
属性是一个NSString
类型,可以被设置成以下值:KCATransitionFade KCATransitionMoveIn KCATransitionPush KCATransitionReveal
隐式过渡.当设置
CALayer
的content
属性的时候,CATransition
的确是默认的行为。但是对于视图关联的图层,或者其他隐式动画的行为,这个特性依然是被禁用的。 -
在动画过程中取消动画
为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:
-(void)removeAnimationForKey:(NSString *)key;
或者移除所有的动画:
-(void)removeAllAnimations;
动画一旦被移除,图层的外观就立刻更新到当先的模型图层的值。一般来说,动画在结束之后被自动移除,除非设置
removeAllAnimation
为NO
,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。
九、图层时间
-
CAMediaTiming协议
CAMediaTiming
协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer
和CAAnimation
都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。-
持续和重复
duration
是一个CFTimeInterval
类型,对将要进行的动画的一次迭代指定了时间;repeatCount
代表动画重复的迭代次数。例如,duration
是2,repeatCount
是3.5,那么完整的动画时长将是7秒。 -
相对时间
beginTime
指定动画开始之前的延迟时间。这里的延迟是从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。speed
是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果speed
为2.0,那么对于一个duration
为1的动画,实际上在0.5秒的时候就已经完成了。timeOffset
让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset
为0.5意味着动画将从一般的地方开始。需要注意的是,和beginTime
不同的是,timeOffset
并不受speed
的影响。所以如果你把speed
设置为2.0,把timeOffset
设置0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset
让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。 -
fillMode
fillModel
是一个NSString
类型,可以接受如下四种常量:KCAFillModeForwards KCAFillModeBackwards KCAFillModeBoth KCAFillModeRemoved
默认是
KCAFillModeRemoved
,当动画不再播放的时候就显示图层模型指定的值,剩下的三种类型,向前、向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。这就对避免对动画结束的时候急速返回提供了另一种方案。但是,当它来解决这个问题的时候,需要把
removeOnCompletion
设置为NO
,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。
-
-
层级关系时间
每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量,对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。
对
CALayer
或者CAGroupAnimation
调整duration
和repeatCount
、repeatDuration
属性并不会影响到子动画。但是beginTime
、timeOffset
和speed
属相将会影响到子动画。CoreAnimation有一个全局时间的概念—马赫时间。马赫时间在设备上所有进程都是全局的,但是在不同的设备上并不是全局的(手机休眠时,马赫时间会暂停),但是比较两次马赫时间差很有价值。访问马赫时间:
CFTimeInterval time = CACurrentMediaTime();
-
手动动画
timeOffset
一个很有用的功能在于它可以让你手动控制动画进程,通过设置speed
为0,可以禁用动画的自动播放,然后用timeOffset
来来回显示动画序列。这可以使得运用手势来控制动画变得很简单。 -
总结
十、缓冲
-
动画速度
使用缓冲方程式需要设置
CAAnimation
的timingFunction
属性,timingFunction
是CAMediaTimingFunction
类的一个对象;如果想改变隐式动画的计时函数,同样也可以使用CATransaction
的+setAnimationTimingFunctions:
方法。创建
CAMediaTimingFunction
的最简单的方式是调用+timingFuctionWithName:
的构造方法。传入的常量如下:KCAMediaTimingFunctionLinear KCAMediaTimingFunctionEaseIn KCAMediaTimingFunctionEaseOut KCAMediaTimingFunctionEaseInEaseOut KCAMediaTimingFunctionDefault
自定义缓冲函数
十一、基于定时器的动画
-
定时帧
iOS按照每秒60次刷新屏幕,用定时器1/60秒去更新view的属性便可以实现定时帧动画.
-
NSTimer
iOS上的每个线程都管理了一个
NSRunloop
,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:- 处理触摸事件
- 发送和接受网络数据包
- 执行使用GCD的代码
- 处理计时器的行为
- 屏幕重绘
当设置一个
NSTimer
,他会被插入到当前任务列表中,然后知道指定的时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。如何精确动画:
- 可以用
CADisplayLink
让更新频率严格控制在屏幕刷新之后. - 基于真实帧的持续时间而不是假设的更新频率来做动画(通过两次马赫时间差来取动画帧).
- 调整动画计时器的
runloop
模式,这样就不会被别的事件干扰.
-
CADisplayLink
CADisplayLink
是CoreAnimation提供的类似NSTimer的类,它总是在屏幕完成一次更新之前启动。CADisplayLink
有一个整型的frameInterval
属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval
为2,就是说动画每隔一帧执行一次。 -
Run Loop模式
常见的run loop模式:
-
NSDefaultRunLoopModel
- 标准优先级 -
NSRunLoopCommonModes
- 高优先级 -
UITrackingRunLoopMode
- 用于UISCrollView
和别的控件的动画
可以对
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];
NSTimer
同样:self.timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
-
-
十二、性能调优
CPU VS GPU
-
测量,而不是猜测
可以在程序中用
CADisplayLink
来测量帧率(用两次马赫时间差的倒数即为帧率) -
Instruments
- Time Profiler
- Leaks
- Core Animation
- Allocations
- GPU Driver
十三、高效绘图
-
软件绘图
在iOS中,软件绘图通常是由Core Graphics框架完成。但是在一些必要的情况下,相比Core Animation和OpenGL, Core Graphics要慢不少。
一旦实现了
CALayerDelegate
协议中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽图层高4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配(所以一般还是不要重写view的-drawRect:
方法)。 -
矢量图形
在某些情况下,需要使用Core Graphics来绘图:
- 任意多边形(不仅仅是一个矩形)
- 斜线或曲线
- 文本
- 渐变
但Core Animation实际上为这样的绘制提供了专门的类,如
CAShapeLayer
绘制多边形、直线和曲线,CATextLayer
绘制文本,CAGradientLayer
用来绘制渐变。 脏矩形
异步绘制
十四、图像IO
- 加载和潜伏
-
+imageNamed:
方法会解压图片,并缓存图片,但是+imageNamed:
只对应用资源束中的图片有效,所以对用户生成的图片或者下载的图片就没法使用了。 -
+imageWithContentsOfFile:
加载大图片比较耗时,并且不会解压图片。如果要实现主线程快速加载图片,需要在后台线程加载图片数据并强制解压。 - 第三方库
SDWebImage
实现了图片的下载、解压、缓存,可以多学习。
-
- 缓存
- NSCache
-
-setCountLimit:
,设置缓存数量。 -
-setObject:forKey:cost:
,对每个存储的对象指定消耗的值来提供一些暗示。 -
-setTotalCostLimit:
,设置全体缓存的尺寸。 -
NSCache
在系统低内存的时候自动丢弃存储的对象。
-
- NSCache
- 文件格式
十五、图层性能
隐式绘制
-
离屏渲染
当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:
- 圆角(当和
maskToBounds
一起使用时) - 图层蒙板
- 阴影
对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用
CAShapeLayer
,contentsCenter
或者shadowPath
来获得同样的表现而且较少地影响到性能。 - 圆角(当和
-
混合和过渡绘制
透明、半透明颜色的View,会增加GPU的计算,降低性能。
shouldRasterize
属性的使用。 减少图层数量