首页 > 代码库 > Core Animation学习总结

Core Animation学习总结

目录:
  1. The Layer Beneath
    1. The Layer Tree(图层树)
    2. The Backing Image(寄宿层)
    3. Layer Geometry(图层几何学)
    4. Visual Effects(视觉效果)
    5. Transforms(变换)
    6. Specialized Layers(专有图层)
  2. Setting Things in Motion
    1. Implicit Animations(隐式动画)
    2. Explicit Animations(显式动画)
    3. Layer Time(图层时间)
    4. Easing(缓冲)
    5. Timer-Based Animation(基于定时器的动画)
  3. The Performance of a Lifetime
    1. Tuning for Speed(性能调优)
    2. Efficient Drawing(高效绘图)
    3. Image IO(图像IO)
    4. Layer Performance(图层性能)



Core Animation 之 CALayer
The Layer Tree(图层树)

1、图层与视图的区别:
  • 视图是以树的数据结构来管理层级关系的,而图层同样也是以树的数据结构来管理层级关系
  • 视图在程序中以UIView及其子类来表示,而图层以CALayer及其子类专用 图层来表示
  • UIView总是与CALayer是一一对应的关系,所以本质上,iOS上界面上的内容的呈现与动态实际上是通过CALayer来实现的,而UIView是封装了CALayer的基础上添加了事件响应、布局等高级功能。

2、关于视图层级、图层树、呈现树、渲染树
  • 视图层级:主要负责实现事件响应、布局等功能,因为视图封装图层,图层向视图暴露部分编程接口与属性,于是通过修改视图的效果可以间接修改图层的属性。
  • 图层树:负责定义界面图形的绘制、动画效果,iOS是有一个绘制周期的——60FPS,也就是说图层树负责保存这个周期内的相关属性的修改,而到了周期结束要进行绘制的时候,才把图层树的数据模型更新到呈现树中。
  • 呈现树:确定当前屏幕上界面图形的具体效果,用于保存当前屏幕的图形的显示属性,每隔一个绘制周期和图层树同步一次。
  • 渲染树:iOS系统会专门创建一个进程来执行图形渲染的任务,当任意APP(包括系统App)需要图形渲染的时间,它们就会把渲染任务发送到该线程去执行渲染。
所以以上四层,每一层都负责不同的任务,其数据流是视图-》图层-》呈现树-》渲染树。并且绘制周期60FPS也是会随着系统的状态而变化的,若系统资源超载,而会通过减少绘制次数来确保系统能稳定运行。

3、视图动画
视图动画是显式,因为UIView默认把CALayer的隐式动画禁止掉了,所以要通过动画Block的形式才能取消动画的限制。(动画block实际上就是以前的动画栈)。
并且视图能实现的动画只是图层所开放的一部分,都是2D动画,所以视图动画的效果会稍稍简单。

4、图层动画
图层动画是隐性动画,只要修改图层的属性,其变化都会以默认的渐变形式呈现出来(具体原理见《隐式动画》)。并且图层动画支持更多视图动画无法实现的效果。如:
  • 阴影、圆角、带颜色的边框
  • 3D变化
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画。

5、更适宜使用CAlayer呈现内容的场景
因为UIView与CALayer都可以呈现内容,虽然CALayer不能直接实现事件响应,但开发者也可以通过hit-test机制的原理来自己实现事件响应,那什么场景更加适合用CALayer而不是UIView呢?如下所示:
  • 开发同时可以在MAC、iOS上运行的跨平台应用
  • 使用多种CALayer的子类(专有图层),并且不想创建额外的UIView去封装
  • 做一些对性能特别挑剔的工作,如对UIView一些可忽略不计的操作都会引起显著的不同(也可以通过OpenGL来解决)。

The Backing Image(寄宿层)

1、寄宿层&Contents属性
CALayer有一个名曰Contents的属性,这个属性是与寄宿层相对应了,而contents属性指向的对象必须为CGImageRef类型(一个指向CGImage结构的指针),所以寄宿层是用来展示图片所用的,如下情况会调用寄宿层:
  • 显示图片
  • core Graphics
core Graphics可以实现自定义绘制,但通常不建议那么做,因为core Graphics绘制会默认生成一个绘制专用的存储空间,而这空间有十多M那么多,会占用大量内存,所以Apple不建议实现空的core Graphics更不建议在core Graphics实现不属于该方法的代码。

2、contentGravity属性
与UIView的contentMode属性相对应,用于调整图片的布局,支持一下常量值:
  • kCAGravityCenter
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill

3、contentsScale
contentsScale属性定义了寄宿层的像素尺寸和视图大小比例,默认是1.0(一个点一个像素),在retina屏幕得设置在2.0(一个点两个像素),在plus设备上得设备为3.0(一个点3个像素)。
但若contentGravity设置了如kCAGravityResizeAspectFill自动适配大小的属性后,contentsScale会不起作用。所以不能依靠contentsScale来做缩放的操作,缩放还是得交给transform或者affineTransform。

3、maskToBounds
该属性与UIView的clipsToBounds属性相对应,都是应用于将超出图层边界的子图层的内容进行裁剪。
这里需要一点需要注意的,但我们实现radiusCorner时,实际上就是通过设置背景颜色来实现的,而与maskToBounds结合才是真正把边角内容裁剪掉,但radiusCorner+maskToBounds结合使用会引发离屏渲染,所以在radiusCorner满足要求,就不要再调用maskToBounds。

4、contentsRect
CALayer的contentsRect允许我们在图层边框内显示寄宿图的一个子域,而contentsRect是一个相对值属性,如{0,0,1,1}表示整个寄宿图,这种坐标系统也叫单位。如下简述iOS下的三种坐标系统单位:
  • 点:点是虚拟的像素,也叫逻辑像素,在不同的设备上一个点所代表的像素点是不一样的,如普通屏幕一个点就是一个像素,而retina屏幕一个点就是两个像素。
  • 像素:实际的物理像素坐标。
  • 单位:一种相对的坐标,好处就是应用相对值,方便移植。
contentsRect有一种经典的用法——image sprites(图片拼接,常用于游戏),这种用法就是用来实现一次性载入多张图片的,因为每一张图片的使用前都要经过,加载——》压缩——》呈现,的一个过程其中加载和压缩是十分耗时的,加载消耗IO,压缩算法的复杂会增加CPU的消耗,所以想游戏这样的App需要加载解压大量的图片时,可以把所有图片整合成一张图片来加载解压,然后通过contentsRect裁剪出子图,并显示,可以优化加载解压的过程。

5、contentsCenter
contentsCenter的名字其实有一点的误导性,其实际上与UIView的Stretching属性相对应,用来实现寄宿层的缩放时的呈现效果。也是一个相对值单位。


6、customs drwaing
除了通过CGImage设置到contents的方式来实现寄宿层,还可以通过Core Graphics来直接绘制寄宿层,但这种方式十分消耗内存不建议使用。
CALayer有一个可选的Delegate属性,实现了CALayerDelegate,但CALayer需要重新绘制,则调用它的displayLayer方法(与UIView的setNeedDispaly相对应),就会调用其代理方法drawLayer:inContext(与UIView的drawRect相对应)。若在视图层中,只要用户实现了drawRect那么只要屏幕需要重新绘制那么该方法都会被默认调用。

Layer Geometry(图层几何学)

1、布局
UIView的三个重要布局属性:frame、bounds、center
CALayer的三个重要的布局属性:frame、bounds、position
可以看出UIView与CALayer的布局属性是一一对应的,而唯一有啥差别的就是锚点的命名,UIView为center,而CALayer为position,但锚点最开始都是默认为图形的中心。
其中frame实际上就是一个虚拟属性,其由bounds与center/position计算所得,所以修改bounds与center/position也会影响到frame的数值。
还有无论是视图还是图层,在屏幕上无论怎么变形,本质上其还是一个矩形,但在旋转的情况下,frame的数值是会事实变动的,如下图:
技术分享技术分享

2、锚点
锚点属性是用来指定图层相对于父图层的位置,也可以理解为利用图层的句柄。默认为图层的中心但也可以修改,在图层中修改了锚点会产生移动(隐性动画),同样的锚点也是一个相对值。
如下,为锚点的实验效果图:

图一:时分秒针直接以图层的中点为锚点,因为旋转时也以其为中心,效果如下:
技术分享
图二:将锚点修改成指针的尾端
技术分享

图三:修改锚点后的旋转效果。
技术分享

3、坐标系
因为每个图层都有自己的坐标系,所以CALayer也提供了一系列的方法用于一个点在不同的坐标系之间转换,这些方法特别适合于子图层与父图层之间的坐标转换。
技术分享
其实UIView与CALayer都有zPosition,但由于UIView只支持2D变换,所以UIView的zPosition仅仅适合于用来作图层的调整,但更加建议用UIView提供的视图数组管理方法来调整视图的绘制顺序来调整。而CALayer的zPosition是一个重要的三维坐标系。

4、Hit Testing
Hit Testing是iOS中一个十分重要的机制,用于检索点击了的目标图标,与响应链条相互搭配的话,就会将最终目标图标设置为第一响应者。Hit Testing提供了两个核心的方法:
  • containPoint:(用于直接判断某个点是否属于某个图层)
  • -hitTest(通过递归遍历子对象的方式来直接返回目标图层)

5、自动布局
对于UIView,通过UIView暴露出来的UIViewAutoresizingMask和NSLayoutConstraint来实现自动布局。
但对于CALayer,则需要手动来操作实现,其中最为简单的方法就是实现CALayerDelegate的如下函数:
- (void)layoutSublayersOfLayer:(CALayer *)layer;
在该方法内根据当前layer的属性来计算调整来实现自动布局。
因为CALayer实现自动布局不方便,所以这也是为什么更加建议使用UIView来实现界面的构建。

Visual Effects(视觉效果)——本节探讨能够通过CALayer实现的附加视觉效果

1、圆角
CALayer有一个叫做cornerRadius的属性控制图层角的曲率,这个曲率只影响背景颜色而不影响图片或者子图片。圆角的计算原理就是一个以CALayer的中点,曲率所设的半径的原与矩形的交集就是目标图形。
还有一个名曰maskToBounds属性,就是实现将图层里面边界外的图形全部切割丢弃。
所以通过cornerRadius+maskToBounds组合可以实现圆角,但同时会引发离屏渲染,若是小量的离屏渲染还好,但如滚动条的场景,开会有大量的离屏渲染的任务产生,就会严重影响性能,这一点也是注意优化的。具体效果如下图所示:
技术分享



2、图层边框
通过设置CALayer的borderWidth与borderColor两个属性可以修改边框的效果。效果如下:
技术分享



3、阴影
阴影是一种能达到图层深度暗示效果的装饰。CALayer提供以下参数来定制阴影的效果:
  • shadowOpacity:设置一个大于零的数值,那么阴影就会显示在图层下面(默认为0)。
  • shadowColor:设置阴影的颜色(默认黑色)
  • shadowOffset:设置阴影的偏移量(默认{0,-3})
  • shadowRadius:设置阴影模糊度,数值越大,阴影的模糊度越高(默认为0),曲率越大,阴影效果越明显。
shadowRadius的设置效果图如下:
技术分享
有一点需要注意的,就是阴影的绘制是属于离屏绘制,是比较效果资源的,所以一定要减少同一屏幕进行大量阴影绘制的情况。


4、阴影裁剪
图层的阴影更多是继承于内容的外形,而不是根据边界和角半径来确定,为了计算出阴影的形状,core animation会将寄宿层也考虑在内。但若直接maskToBounds来裁剪,会把阴影效果也裁减掉。
为了解决以上问题,可以通过专门引入一个阴影图层来解决,具体效果如下:

图一:直接maskToBounds裁剪会把阴影效果一并裁剪掉。
技术分享
图二:通过添加一个另外的阴影图层在下面,然它来实现阴影,而具体的内容图层就直接maskToBounds裁剪。
技术分享
图三:最终既能实现裁剪,又能实现阴影。
技术分享
PS:无论是阴影还是圆角裁剪都会引发离屏渲染,大量的该类型的图形需要渲染的话,会减低帧率。


5、shadowPath 属性
阴影并一定是方形的,我们也可以通过shadowPath来定制自己想要的阴影形状,具体效果如下所示:
技术分享



6、图层蒙版
通过masksToBounds属性,我们可以实现边界裁剪,但我们还需要更加灵活的裁剪需求的时候就可以通过图层蒙版来实现。CALayer提供一个mask的属性来解决一个问题,而mask本来就是指向另一个图层的指针,其具体原理如下图所示:
技术分享

技术分享
从上图效果可以知道mask舒心只关心形状的交集,而颜色还是由原来的图形的颜色决定。
还有一点需要注意的是图层蒙版也会引发离屏渲染,会带来性能问题,所以使用的时候也得多加注意。


7、拉伸过滤
当我们视图显示一个图片的时候,都应该以正确的比例,正确的1:1像素显示在屏幕上,原因如下:
  • 能够显示更好的画质,像素既没有被压缩也没有被拉伸
  • 能更好使用内存,因为这就是你要存储的东西
  • 最好的性能表现,CPU不需要为此额外的计算 
但有时候我们就需要缩略图,所专门另外存储缩略图这对内存来说。这时就可以通过CALayer的minificationFilter(缩小滤波器)和magnificationFilter(方法滤波器)属性实现图形的缩放算法,其中提供以下三种默认的缩放算法:
  • kCAFilterLinear(双线性滤波算法,默认算法)
  • kCAFilterTrilinear(三线性滤波算法)
  • kCAFilterNearest(最近滤波算法)
对于大图的缩放,采用kCAFilterLinear、kCAFilterTrilinear,效果会比较好。
对于小图、较少斜线的图的缩放,,采用kCAFilterNearest效果比较好。

图一:采用kCAFilterLinear的方法滤波器算法的效果。
技术分享

图二:采用kCAFilterNearest的放大滤波器算法的效果:
技术分享



8、透明组
UIView有一个alpha属性类确定视图的透明度,而CALayer有一个与之对应的属性——opacity。这两个属性都会影响到子图层。如下图所示:
当我们将view2的alpha数值设置为0.5,这时候,对于label所处的点的颜色计算公式为:50%label + 25%view + 25%background,所以就是灰色偏白的颜色。
若想整个图层的色调保持一致,可以通过将shouldRasterize属性设置为YES,那么图层及其子图层将被整合成一个整体的图片。效果如下:

图一:shouldRasterize属性设置为默认值NO的效果。
技术分享
图二:shouldRasterize属性设置为默认值YES的效果。
技术分享



Transforms(变换)

1、仿射变换
UIView对应的transfrom属性是一个CGAffineTransfrom类型,用于实现二维空间旋转、平移、缩放、斜切。
而CALayer的transform属性是一个CATransforms3D,能应用3维空间进行旋转、平移、缩放、斜切等效果。
其中,仿射变化的数学原理如下:
技术分享
不过iOS为了运算的方便,已经提供了三个标准方法分别用于实现旋转、缩放,平移,而斜切得靠原生运算来实现。
技术分享
其中有一点需要注意的,就是旋转的angle是以弧度制为单位的。

iOS系统提供以下方法来实现混合变换,能同一以下的组合函数实现复杂的变形模式:

图一:创建一个空的变换结构体。
技术分享
图二:不断叠加变换结构体
技术分享
图三:直接将两个变换结构体合并
技术分享

剪切变换的效果与其实现代码:
技术分享
技术分享

2、3D变化
3D变化与2D变化是十分相似的,不同的只是3D变换是3个维度的,添加了Z轴。其数学原理如下:
技术分享
这种矩阵运算还是挺麻烦的,所以iOS系统提供给我们便捷的方法,:
技术分享
同样也会有变换叠加的方法,其中旋转的时候一定要注意当前的旋转的参考轴,顺时针旋转为正值,逆时针旋转为负值。
技术分享


3、透视投影
在真实的世界中,当物体远离我们,由于视角的原因,其会变小,所以为了让3D效果更佳真实,需要引入投影变换,虽然core animation并没有提供给我们,但十分简单,其数学实现如下:
技术分享
只要将m34 = -1/d, d= 500~1000,(d越小越失真,d越大透视效果越弱)。

图3.1:没实现透视投影的旋转效果。
技术分享

图3.2:实现了透视投影的旋转效果。
技术分享

4、灭点
在人的视觉中,当物体离人越远则会变得越小,当接近无穷远的时候就会汇聚成一个不可见的点,这个点就叫灭点。现实生活中,这个点通常就是中心点,所以在程序中我们也要让图形的灭点集中在屏幕的中点,如下所示:
技术分享
而实现方式则是,首先添加图形的时候先以屏幕中点为锚点添加,然后通过transfrom来讲图层移动到恰当的位置。


5、sublayerTransform
sublayerTransform是一种将所有相应的配置统一设置到该图层的所有子图层的便捷属性。

6、3D坐标下的图层背面
在3D坐标下,图层的背面也是会绘制的,而且是正面的镜像。
我们可以通过CALayer的doubleSided属性来决定是否执行双面绘制。

7、扁平化图层
如果父图层的坐标发生变换,那么子图层也会随着父图层而进行同样的变换,如7.1图所示。但有时我们只想变换父图层并不想变换子图层,这时就得做相对变换来抵消掉父图层的变换,让视觉效果看起来子图层是静止的。如下图所示:
图7.1:2D相对旋转理论图
技术分享
图7.2:2D相对旋转效果图
技术分享

图7.3:3D相对旋转理论图
技术分享
图7.4:3D相对旋转实际效果图
技术分享
我们可以看出,在2D坐标系下,相对变换是合理的,但在3D坐标系下,相对变换的结果并不如预想的一样,为什么呢?
这是因为core Animation图层虽然存在于3D空间之内,但并不是每一个图层都存在于同一个3D空间之间。而用户是以当前window所在的3D坐标系为参考的,这时7.4图的3D坐标系与window的已经不重合的,所以在我们的视觉里面,就是看不到预想的效果,这就是图层扁平化。
图层扁平化让core animation创建复杂的3D场景变得十分困难,因为很难使用图层树去创建一个3D结构的层级关系——将所有的场景下的3D都保持相同的3D坐标系。
不过CALayer提供一个专用图层——CATransformLayer来解决这个问题,所有添加到该图层内的3D图形都共享一个标准坐标系。



8、3D场景下的光亮和阴影
略,暂时不懂。

9、3D场景下的点击事件
在处理点击事件,一定要注意点击响应是由UIView中的图层结构来决定的,而不是CALayer在屏幕上的呈现效果,所以得注意事件拦截的顺序。
可以通过设置userInteractionEnabled以及简单的视图覆盖来实现点击事件正确的传递。

Specialized Layers(专有图层)

1、CAShapeLayer


2、CATextLayer


3、CATransformLayer


4、CAGradientLayer


5、CAReplicatorLayer


6、CAScrollLayer


7、CATiledLayer


8、CAEmitterLayer


9、CAEAGLlayer


10、AVPlayerLayer


Core Animation 之 动画
Implicit Animations(隐式动画)

1、隐式动画&事务
Core Animation基于一个假设,就是屏幕上的所有图形都可以做动画,而且这种动画不需要开发者去设置,会自动实现。这也是为什么直接修改CALayer的属性会以一种渐变的形式来更新到一个新的值。
这种只需要修改图层的数值就会引发动画的形式来更新到新数值的动画称为隐式动画。
而这种定义了动画的具体呈现效果的机制,就称为事务。
由于CALayer是默认开启了隐式动画的,而若我们在CALayer上调用显式动画的话,就考虑到隐式动画对效果的影响,进而决定是否需要取消隐式动画。

2、动画Block
UIView封装了CALayer,但UIView禁止了隐性动画,所以开发者需要在UIView上实现显性动画。在iOS4.0之前,UIView采用动画栈来管理显示,后来提供了动画Block这个更加便捷的方式,但原理都是一样的。

3、图层行为
我们把改变CALayer属性时自动应用的动态称为行为,一组行为的执行过程如下所示:
  1. CALayer调用-actionForKey:方法,传递属性名称。
  2. 检查CALayer是否持有实现了CALayerDelegate的代理对象(需要实现actionForLayer:forKey方法),若有直接调用并返回结果,若无继续以下步骤。
  3. 检查包含属性名称对应行为映射的actions字典,若有则返回,无则继续
  4. 在style字典接着搜索属性名,若有这返回,无则继续
  5. 调用属性的标准行为defaultActionForKey方法
经过以上的整个流程,要么actionForKey返回nil则无动画效果,要么就是返回CAAction,然后CALayer利用它实现动画。
因此可以推动出UIView是如何禁止隐式动画的,就是讲CALayer的delegate设置为自身,然后在actionForLayer:forKey方法中返回nil。

  • 禁止隐式动画的两个方法:
    • 根据图层行为的原理,实现代理方法actionForLayer:forKey强制返回nil。
    • 通过设置[CATransaction setDisableActions:YES]来实现禁止隐式动画

4、呈现与模型
在iOS中,屏幕每秒钟重新刷新屏幕60次。所以Core Animaiton就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。
正是以为上面的机制的存在,所以才会有iOS才会有图层以及呈现层之间的关系,图层更像是model,而呈现层就是view,Core Animation就是Controller,所以在一个绘制周期内,图层负责收集与保存用户对属性的修改,到绘制时刻时,就将图层的数据覆盖到呈现树。

我们可以通过CALayer的 -presentationLayer方法来获取正在屏幕上显示的呈现树的数据,虽然一般不需要,但在以下情况下会比较有作用
  • 做基于定时器的动画时,而不仅仅是基于事务的动画,这时准备地知道当前时刻的图层的显示位置是十分有用的
  • 如果你想做图层的响应输入,可以通过-hitTest方法来判断制定图层是否被触摸,这时候对呈现图层调用-hitTest会更加有效,因为呈现树就是当前屏幕上的图形实际的位置。

Explicit Animations(显式动画)

1、Core Animation的类图架构
技术分享
  • CAAnimation实现了CAMediaTiming协议,本身并没有做什么工作,主要实现动画的时间相关的属性以及计算函数(缓冲)
  • CATrasition描述变换的对象
  • CAAnimationGroup封装属性动画的队列。
  • CABasicAnimation基础属性动画
  • CAKeyFrameAnimation关键帧动画

2、基础属性动画
基础动画主要由CABasicAnimation实现,由上图可知道,而CABasicAnimation继承于CAPropertyAnimation属性动画,通过设置以下三个数值以及其他的选项,即可实现自动动画:
  • fromValue
  • toValue
  • byValue
其中toValue与byValue不能同时使用,toVaule是绝对值,byValue是相对值。属性动画都是针对关键帧的动画,也就是说只关心出发点与结束点,中间的过渡可以自己生成也可以默认。

3、关键帧动画
CAKeyFrameAnimation用于实现关键帧动画,能通过设置其animations数组来定义每个关键帧的位置,也可以直接通过path属性来定义运动的路径。

4、affineTransform属性
若我们让一个图形沿着曲线运动,可以通过设置affineTransform,让其能沿着曲线的切线运动从而让运动更加真实。前后效果图如下图所示:
技术分享技术分享


5、虚拟属性
属性动画实际上是针对关键路径而不是一个键的,这就意味着可以对子属性甚至虚拟属性做动画。
如若我们想实现旋转的效果,本来我们需要在keyPath上设置“transform”,
若使用虚拟属性,可以直接在keyPath上直接设置“transfrom.rotation”,这样在设置formValue与toValue时就可以直接设置弧度叫的值。
其实transfrom.rotation这个属性是并不存在的,而是core aniamiton自动根据CAValueFunction来重新计算transform的数值所得,正因为如此才称之为虚拟属性。

6、动画组
CAAnimationGroup是一种组合动画的解决方案。通过CAAnimationGroup添加沿曲线运动与颜色变化的动画。
技术分享


7、过渡
略。


8、在动画的过程中取消动画
Core Animation提供了一部分的方法来实现动画添加以及移除,如下图所示:
- (CAAnimation *)animationForKey:(NSString *)key;  //实现动画的添加,其中key不仅可以用来访问动画,还可以用来移除动画
- (void)removeAnimationForKey:(NSString *)key;  //移除Key指定的动画
- (void)removeAllAnimations;  //移动CALayer已经添加的所有动画
Layer Time(图层时间)

1、CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内控制逝去时间的属性集合。CALayer和CAAnimaition都实现了该协议,所以时间可以被任意图层或者一段动画的类所控制。

CAMediaTiming协议下定义的一些核心属性:
  • duration:定义一段动画的持续时间
  • repeatCount:定义一段动画的重复次数
  • repeatDuration:制定动画重复一段制定的时间
  • autoreverses:制定在每次间隔交替循环的过程中自动回放
可以通过将repeatCount或者repeatDuration设置为INFINITY来实现动画无限循环播放,但不能同时使用这两个属性。


2、相对时间
在Core Animation中,时间是相对的,每个动画都有自己的描述时间,可以独立的加速、延迟或者偏移,其中有以下关键属性:
  • beginTime:动画的开始时间,但不是绝对时间,而是一个相对时间,就是从该动画添加到可见层的那一刻开始测量,默认是零。
  • speed:时间倍速,默认是1.0,若设置为2.0而二倍速前进,那么对于一个duration为1.0的动画,实际上0.5s就已经完成。
  • timeOffset:让动画直接快进到某个时间点进行,使用它要注意是否有speed的参与而改变的duration
  • duration:动画的播放时间。

3、fillMode
当一个图层产生动画,实际上就是呈现层在执行动画,那么,当呈现层执行完动画是否要讲当前属性的值覆盖会图层,这种行为就称为fill mode,这个由开发者决定:
  • kCAFillModeForwards:(保持结束后的值)
  • kCAFillModebackwards:(保持结束前的值)
  • kCAFillModeBoth:(包括以上两种情况)
  • kCAFillModeRemoved:(默认,当不在播放动画时,则显示回图层的属性值)


4、全局时间与本地时间
CALayer或者CAGroupAnimation调整durationrepeatCount/repeatDuration属性并不会影响到子动画。但是beginTimetimeOffsetspeed属性将会影响到子动画。
每个CALayerCAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTimetimeOffsetspeed属性计算。

CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。
CACurrentMediaTime函数来访问马赫时间:
CFTimeInterval time = CACurrentMediaTime();
该值返回的是一个相对值,与现实的时间无关,但可以通过它来比对不同动画之间的时间差,iOS提供以下方法来进行不同图层之间本地时间的转化:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
通过以上知识可以实现同步不同图层的speed,timeOffset、beginTime的动画。


5、暂停、倒回和快进的实现
设置动画的speed属性为0可以暂停动画,但不能再添加后再修改,否则会报错。
但我们可以通过以下的设置来实现一些调试:
self.window.layer.speed = 100;

6、手动动画
将speed设置为0让动画停止播放,然后修改timeOffset来切换不时间的动画序列,从而实现手动切花动画的效果。

Easing(缓冲)——让动画更加平滑自然

1、CALayer、Animation的缓冲动画与CAMediaTimingFucntion
首先需要设置CAAnimationtimingFunction属性,是CAMediaTimingFunction类的一个对象。如果想改变隐式动画的计时函数,同样也可以使用CATransaction+setAnimationTimingFunction:方法。来实现缓冲函数,其中有以下默认的缓冲函数:
kCAMediaTimingFunctionLinear            //线性
kCAMediaTimingFunctionEaseIn            //缓进
kCAMediaTimingFunctionEaseOut           //缓出
kCAMediaTimingFunctionEaseInEaseOut     //缓进缓出
kCAMediaTimingFunctionDefault           //默认的效果与kCAMediaTimingFunctionEaseInEaseOut类似,但效果更佳不明显

2、UIView的缓冲动画
通过设置UIView的options参数添加如下常量也可以实现缓冲动画的效果:
UIViewAnimationOptionCurveEaseInOut 
UIViewAnimationOptionCurveEaseIn 
UIViewAnimationOptionCurveEaseOut 
UIViewAnimationOptionCurveLinear


3、缓冲和关键帧动画
CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。

4、自定义缓冲函数
暂时不讨论。

Timer-Based Animation(基于定时器的动画)——我们可以通过事务来实现动画,设置关键帧,让中间的过渡自动计算得出,也可以基于定时器来设置每一个绘制周期的变换来实现动画。

iOS提供一下两种形式来实现基于定时器的动画:
  • NSTimer  :并不是根据实际帧率来执行,所以CADispaly是更好的解决方案
  • CADispaly  :根据实际的帧率来执行,更加适合

但无论是NSTimer还是CADispaly本质上都是基于runloop,NSTImer把每个时间触发的事件注册到runloop里面,以待制定。而CADisplay则把任务直接嵌套进绘制操作之前。


Core Animation 之 性能优化
Tuning for Speed(性能调优)

1、CPU &  GPU
CPU(中央处理器)和GPU(图形处理器)都是能用来处理。
总的来说,我们可以用软件(使用CPU)做任何事情,但是对于图像处理,通常用硬件会更快,因为GPU使用图像对高度并行浮点运算做了优化。由于某些原因,我们想尽可能把屏幕渲染的工作交给硬件去处理。问题在于GPU并没有无限制处理性能,而且一旦资源用完的话,性能就会开始下降了(即使CPU并没有完全占用)。
性能优化的本质就会合理地利用CPU与GPU,使他们不会超出负荷。

Core Animation处于iOS的核心地位,无论是应用内还是应用外都会用到它。所以iOS特别设计了一个进程来执行渲染相关的任务,也叫渲染服务。渲染服务管理动画和屏幕上组合的图层。
当运行一段动画的时候,这个过程会被切分为六个阶段,包括应用内的四个阶段,与应用外的2个阶段,其中应用内的阶段如下:
  • 布局(CPU):这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
  • 显示(CPU):这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:-drawLayer:inContext:方法的调用路径。
  • 准备(CPU):这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
  • 提交(CPU):这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
当数据被打包到渲染服务进程,会将其反序列化形成另一个渲染树。使用这个渲染树对动画的每一帧做出如下工作:
  • 生成渲染树:(CPU)对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
  • 渲染(GPU):在屏幕上渲染可见的三角形

降低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在图层每次显示之前对图片预处理,同样也会降低性能。

降低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中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。
2、Instruments实现App性能优化,操作顺序通常如下:
  • 添加优化工具
  • 选择福选项,设置感兴趣的数据
  • 对比测试得结果
Efficient Drawing(高效绘图)

1、软件绘制
软件绘图意为不借助GPU的图形绘制,在iOS中通常就是由Core Graphics来实现,但其对比Core Animation和OpenGL,Core Graphics要慢不少,而且也十分消耗内存。
软件绘图不仅效率低,还会消耗可观的内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。
但是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽*图层高*4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。
软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。


2、矢量图形

我们用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:

  • 任意多边形(不仅仅是一个矩形)
  • 斜线或曲线
  • 文本
  • 渐变
文章中实现了如下效果的demo:
技术分享
其实现思路就是每当有touch event发生,则向UIBezierPath中添加一条直线。但在对于屏幕中每当有一个touch event事件发生,意味着整个屏幕都要重新绘制一遍,当需要重新绘制的内容越来越多,则会引起帧率的下降。这时候我们可以通过脏矩阵来对这个问题进行优化。

3、脏矩形
脏矩形是一个能制定重新绘制区域的机制。在程序中则是通过用setNeedDispalyInRect来替代setNeedDispaly方法来实现绘制具体的区域,具体实现见文章。

4、异步绘制
UIKit的单线程天性意味着寄宿图通常要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。
针对这个问题,core Animation提供了一下两种方案来实现异步绘制,提高界面的响应效率:
  • CATiledLayer(具体实现可见第六章)
  • drawsAsynchronously(????)



Image IO(图像IO)——研究如何优化从闪存或者网络中加载和显示图片

1、加载与潜伏
绘图实际消耗的时间通常并不是影响性能的因素,而且若把图片直接存储在内存,会损耗大量的资源,所以需要在应用运行的时候周期性地加载和卸载图片。
图片文件的加载速度同时受到CPU及IO(输入/输出)延迟的影响。iOS设备中的闪存已经比传统硬盘快很多了,但仍然比RAM慢将近200倍左右,这就需要谨慎地管理加载,以避免延迟。
有时候图片也需要从远程网络连接中下载,这将会比从磁盘加载要消耗更多的时间,甚至可能由于连接问题而加载失败(在几秒钟尝试之后)。你不能在主线程中加载网络,并在屏幕冻结期间期望用户去等待它,所以需要后台线程。

2、异步线程加载
由于图片的加载是十分耗时间的,若把该操作放置在主线程中执行会降低CPU的利用率甚至拖慢帧率。可以通过GCD或者NSOperationQueue来实现异步加载最后再在主线程中同步发起渲染即可。

3、延迟解压
一旦图片文件被加载使用,就必须要经过解码(解压)的过程,解码过程是一个相当复杂的任务,耗时长也占大量的内存。
对于PNG:加载相对长,文件相对更大,但解码比较快。
对于JPEG:加载快,图片小,但解码算法复杂耗时长。

由于iOS系统会让加载完成的图片不会立即解压,而是到需要用的时刻才正式解压,所以会影响性能。
  • iOS系统提供以下三种方式来实现绕过延迟解压的机制
    • imageName方法是能避免延迟加载,而且该方法在加载后会立即解压,但只对应用资源束有效。
    • 将加载图片设置为图层内容,如UIImageView的iamge属性,但这需要在主线程执行,所以不会对性能有大的提升
    • 使用ImageIO框架

4、使用CATiledLayer实现异步加载和显示大型图片
略。


5、分辨率交换
略。


6、使用imageNamed实现缓存
imageName方法是能避免延迟加载,而且该方法在加载后会立即解压,但只对应用资源束有效。所以网络图片无效。
之前我们提到使用[UIImage imageNamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[UIImage imageNamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。所以也要注意不能用于加载大图片,不然会占用大量的内存资源。

  • 对于以下场景不能使用imageName,需要自己实现缓存机制:
    • [UIImage imageNamed:]方法仅仅适用于在应用程序资源束目录下的图片,但是大多数应用的许多图片都要从网络或者是用户的相机中获取,所以[UIImage imageNamed:]就没法用了。
    • [UIImage imageNamed:]缓存用来存储应用界面的图片(按钮,背景等等)。如果对照片这种大图也用这种缓存,那么iOS系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。对传送器的图片使用一个单独的缓存机制就可以把它和应用图片的生命周期解耦。
    • [UIImage imageNamed:]缓存机制并不是公开的,所以你不能很好地控制它。例如,你没法做到检测图片是否在加载之前就做了缓存,不能够设置缓存大小,当图片没用的时候也不能把它从缓存中移除。

7、自定义缓存

  • 若需要实现自己的缓存机制,通常得从以下四个方面进行考虑:
    • 选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。如果实时创建图片,通常不太好生成一个字符串来区分别的图片。在我们的图片传送带例子中就很简单,我们可以用图片的文件名或者表格索引。
    • 提前缓存 - 如果生成和加载数据的代价很大,你可能想当第一次需要用到的时候再去加载和缓存。提前加载的逻辑是应用内在就有的,但是在我们的例子中,这也非常好实现,因为对于一个给定的位置和滚动方向,我们就可以精确地判断出哪一张图片将会出现。
    • 缓存失效 - 如果图片文件发生了变化,怎样才能通知到缓存更新呢?这是个非常困难的问题(就像菲尔 卡尔顿提到的),但是幸运的是当从程序资源加载静态图片的时候并不需要考虑这些。对用户提供的图片来说(可能会被修改或者覆盖),一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。
    • 缓存回收 - 当内存不够的时候,如何判断哪些缓存需要清空呢?这就需要到你写一个合适的算法了。幸运的是,对缓存回收的问题,苹果提供了一个叫做NSCache通用的解决方案


8、NSCache
NSCache和NSDictionary类似,都是直接通过键值进行访问,但不同的是,NSCache所持有的对象在内存不足的时候,会自动将其释放。
  • 当然开发者可以通过以下设置来粗颗粒度地进行缓存管理的约束
    • -setCountLimit:方法设置缓存大小
    • -setObject:forKey:cost:来对每个存储的对象指定消耗的值来提供一些暗示
    • -setTotalCostLimit:方法来指定全体缓存的尺寸

9、文件格式与加载性能
略。

Layer Performance(图层性能)


1、隐形绘制
  • 寄宿层可以通过以下方式显示绘制
    • Core Graphics
    • 给contents属性赋值图片
    • 在屏幕外事先绘制CGContext

  • 当发生以下场景会触发隐式绘制
    • 使用特性的图层属性
    • 特定的视图
    • 特定视图的子类

2、文本
CATextLayer与UILable都是直接将文本绘制在图层的寄宿层内,所以要避免频繁的改动,若该文本需要频繁改动,可以先将其放在一个子图层上,通过contentMode来等比例缩放寄宿层。

3、光栅化
我们提到了CALayershouldRasterize属性(光栅化),它可以解决重叠透明图层的混合失灵问题。
启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。
当我们使用得当时,光栅化可以提供很大的性能优势(如你在第12章所见),但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。
为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。
总结:会占用较多内存,要避免重复绘制,所以一旦应用要尽量减少重新绘制,而多利用快照缓存。

4、离屏渲染
  • 当图层的以下属性被修改会触发离屏渲染
    • 圆角(当和maskToBounds一起使用时)
    • masks(图层蒙板)
    • shadows(阴影)
    • shouldRasterize(光栅化)
    • edge antialiasing(抗锯齿)
    • group opacity(不透明)
    • 渐变

屏幕外渲染和我们启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能
对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用CAShapeLayercontentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。

5、混合和过度绘制
开发者应该尽量减少重叠图层的重复渲染,因为对于用户看来某些遮挡的图层的内容是无关紧要的,但渲染就会消耗资源,所以无论任何场景都建议如下操作:
  • 给视图的backgroundColor属性设置一个固定的,不透明的颜色
  • 设置opaque属性为YES
  • 明智地使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。
当然光栅化是是具体场景二选择使用,不然也会引入别的性能问题,如占用大量内存。

6、减少图层数量
初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。事实上,一次性能够在屏幕上显示的最大图层数量也是有限的。
确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。但是总得说来可以容纳上百或上千个,下面我们将演示即使图层本身并没有做什么也会遇到的性能问题。

7、对象回收
处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;UITableViewUICollectionView都有用到,MKMapView中的动画pin码也有用到,还有其他很多例子。




Q&A:
  • Q:图层没有寄宿层,是否等于无法显示内容?(寄宿层)
    • A:不是,还有通过矢量图绘制的方式来实现图形呈现,如通用的现在应该就是通过算来来绘制矢量图的,只有需要显示图片或者调用core graphics的时候才会生成寄宿层,所以若非必要不要实现draw方法,不然系统会默认添加一个寄宿层。
  • Q:组透明的点颜色计算公式?(视觉效果)
    • A:若某子view是透明的,那么它的颜色公式为:child_color*alpha + super_color*(1-alpha)。
  • Q:3D效果与共享灭点的具体操作?(变换)
    • A:在做3D效果时要尽量将所有图形的灭点设置在屏幕的中心,所以建议先在屏幕的中心创建图像,然后通过transform的形式来移动图层,那么就能保证灭点在屏幕中心。
  • Q:3D变化计算?(变换)
    • A:提供了平移、旋转、缩放,但每次操作都是基于二维的,所以注意当前的参考坐标系,剩下的计算与二维一样。
  • Q:立方体的光亮与阴影原理?(变换)
    • A:通过GLK库的函数计算垂直,并添加阴影层。
  • Q:3D点击事件处理?(变换)
    • A:在3D图层中,因为一个点击的点,实际上可能会穿过两个图层的点,这时事件拦截的顺序由subviews数组的顺序觉得,也就是谁最靠近用户视觉,谁有限响应。
  • Q:常规的界面绘制是通过矢量绘制还是寄宿图绘制实现?(专用图层)
    • A:常规界面是矢量绘制完成的。
  • Q:平面化3d层级结构的意思?(专用图层)
    • A:
  • Q:CATiledLayer解决大图加载(专用图层)
    • A:

参考文章:
iOS开发基础知识:Core Animation(核心动画)
《iOS Core Animation Advance Techniques》翻译版,gitbook
《iOS Core Animation Advance Techniques》书籍

Core Animation学习总结