CoreAnimation-拾遗

图层树

在UIKit中所有的视图都是基于UIView派生而来,UIView支持触摸时间,可以支持基于CoreGraphics绘图,可以做仿射变换(例如:旋转或缩小),或者简单的类似于滑动或者渐变的动画。

UIView和Layer的关系

CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同时也是可以包含一些内容(图片、文本或者背景色),管理子视图层的位置。也有一些放和属性用来动画便函。和UIView最大的不同就是CALayer不处理用户交互事件。
CALyer并不处理具体的响应事件(iOS通过视图层关系来传递触摸事件的机制),于是它并不能够响应事件,即使它提供了一些触点在图层的范围之内。

平行的层级关系

每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当前视图在层级关系中添加或被移除时,他们关联的图层在层级关系中也有相同的操作。
实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对Layer的一个封装,提供基于触摸事件的具体功能,以及Core Animation底层方法的高级接口。

但是为什么iOS要基于UIVIew和CALayer提供两个平行的层级关系呢,为什么不用一个简单的层级来处理所有的事情呢?

原因在于要做到职责分离,这样才能避免很多的代码重复。在iOS和MAC OS两个平台上,事件和用户交互的很多地方不同,基于多点触控的用户和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和UIView,但MAC OS有AppKit和NSView的原因,他们在功能上相似,但是在实际上有着显著的区别。
绘图、布局和动画,相比之下就是类似MAC book和桌面级系列一样应用于iPhone和iPad触屏的概念。把这种功能的逻辑分卡并应用到独立的Core Animation框架,苹果能够在iOS和Mac OS之间共享代码,是的对苹果自己的OS开发团队和第三方开发者去开发这两个平台的应用更加便捷。
实际上,这里并不是两个层级关系,而是四个,每一个都扮演着不同的角色,除了视图层级和图层树之外,还存在呈现树和渲染树。

图层的能力

如果说CALayer是UIView内部实现的细节,那么我们为什么要了解它呢?苹果当然为我们提供了更加街的UIView接口,那么我们是否就没必要直接去处理Core Animation的细节了呢?某种意义上是的,对于一些简单的需求来说,我们确实没有必要处理CALayer,因为苹果已经通过UIView的高级API间接的使得动画变的更简单。但是这种简单会不可便面的造成一些灵活性上的缺失,我们要在底层实现一些改变,或者使用一些苹果没有在UIView上实现的接口功能,这是除了介入Core Animation底层之外别无选择。我们已经证实了图层不能像视图那样处理触摸事件,那么它能做哪些视图不能做的?这里有以下UIView没有暴露的出来的CALayer的功能:

  • 阴影,圆角,带色边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

使用图层首先使用一些layer的属性。创建一个矩形在屏幕上,调整颜色使其与底色可以区分开。称其为layerView(使用超过8.2则可忽略)为layerView添加一个子图层,除了创建一个子控件外也可以创建CALayer再添加到layerView的子图层。尽管UIView暴露了图层的属性,但是标准的Xcode的模板并没有包含Core Animation相关头文件,因此需要添加QuarzCore框架,并引进头文件。 代码

CALayer *subLayer = ({
    CALayer * layer = [CALayer layer];           
    layer.frame = CGRectMake(100, 100, 25, 25);           
    layer.backgroundColor = [UIColor blueColor].CGColor;           
    layer;       
});   
[layerView.layer addSublayer:subLayer];

一个视图只有一个相关图层(自动创建),同时视图也支持添加多个子图层。不过一般情况下。单纯的处理图层对象,而不是需要添加额外的子图层。
在Mac OS平台上,一个性能的缺陷就是由于用了视图层级而不是单独在一个视图内使用CALayer树状层级。但是在iOS平台上,使用轻量级的UIView类没有有显著的性能影响。(10.8之后NSView的性能同样得到了提升)
使用图层关联的视图若不是CALayer的好处在于,您能在使用所有CALayer底层特性的同事,也可以使用UIView的高级API。
但出现下面状况是就需要操作CALayer
开发同时可以在Mac OS上进行的跨平台应用

使用多种CALayer的子类

做一些对性能特别挑剔的工作总体而言相对较少,处理视图会比较简单。`

寄宿图

图片胜过千言万语,界面抵得过上千图片

  • contents属性

CALayer的一个属性,类型是id(因为在MacOS上可以接收CGImage或NSImage)。实际上接受的CGImageRef对象,因此在ARC环境下需使用(__bridge id)进行桥接(当然MRC不用)。

layer.contents = (__bridge id)image.CGImage;
  • contentGravity

UIView的contentMode属性,接受NSInteger类型数据,调整寄宿图填充方式。具体类型如下:

UIViewContentModeScaleToFill,
UIViewContentModeScaleAspectFit, // contents scaled to fit with fixed aspect. remainder is transparent
UIViewContentModeScaleAspectFill, // contents scaled to fill with fixed aspect. some portion of content may be clipped.
UIViewContentModeRedraw, // redraw on bounds change (calls -setNeedsDisplay)
UIViewContentModeCenter, // contents remain same size. positioned adjusted.
UIViewContentModeTop,
UIViewContentModeBottom,
UIViewContentModeLeft,
UIViewContentModeRight,
UIViewContentModeTopLeft,
UIViewContentModeTopRight,
UIViewContentModeBottomLeft,
UIViewContentModeBottomRight,

CALayer与之相对应的属性是ContentGravity,接受NSString类型。具体类型如下:

kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
  • contentScale

CALayer中用于表示屏幕一个点绘制几个像素,适用于高分辨率屏幕(Retina)。如果设置contentsMode为拉伸则不会起效,单纯设置对齐方式时会对寄宿图的显示有很大影响。
UIView对应属性为contentsScaleFactor

  • maskToBounds

CALyer用于切割超出边界的部分,接收BOOL类型数据。
UIView与之对应的属性是clipsToBounds

  • contentsReck

CALayer用于表示绘制寄宿图的区域,接受CGRect,属于单位坐标,在iOS中一共有是标注坐标:
: — 最常使用的坐标系。使用点作为基础单位,也被成为逻辑像素。标准尺寸里面一个点就是一个像素,Retain中一个点事2*2个像素。iOS引入点是为了在不同分辨率的设备上达到相同的显示效果。
像素: — 物理像素,不会用来布局,仍与图片相对。UIImage就是一个屏幕解决方案,用点来来度量大小。但底层的CGImage仍是用像素表示的。
单位: — 对于图片大小或图层边界的显示,当大小改变不需要再次调整。在OpenGL和CoreAnimation中使用较多。
默认值是{0,0,1,1},意味着整个图层都是可见的。如果设置原点成为赋值或边界大于1则会把外界的像素拉进图层填充余下区域。

  • contentsCenter

类型CGRect,定义了一个固定的边框和图层上的可拉伸区域。改变这个区域的值时不会发生改变,当视图的大小改变是才会现象出来
与UIImage的resizableImageWithCapIntsets:类似。
比较方便的是,在Interface Builder窗口有对应的属性值,即Stretching设置。

  • Custom Drawing

设置CALayer的contents属性并不是创建寄宿图的唯一方法。仍有去如下方法可以绘制寄宿图。
-drawRect:
如果非必须则不要实现该方法,因为View会在调用该方法时为视图创建一个寄宿图。在该方法中可以使用Core Graphics 绘制寄宿图,虽然是UIView的方法,但实际绘制寄宿图的还是由CALayer绘制并保存的。
-displayLayer:(CALayer*)
CALayer的delegate方法,可以用与直接对CALayer进行操作,类似绘制contents属性等。
如果没未调用上述方法则调用
-displayLayer:(CALayer) inContext:(CGContextRef)
在调用上面的方法前,CALayer会创建一个尺寸合适的寄宿图,并创建图形上下文并传入代理方法。
当现实的调用display命令时,CALayer并不会自动绘制,而是交由开发者决定。
虽然没有设置maskToBounds属性,但在绘制CALayer时仍不会超出边界绘制。
一般不需要单独处理CALayer代理,因为在创建UIView时,会自动创建CALayer并代理设置给UIView。当需要绘制寄宿图时,可以通过UIView的drawRect:方法进行绘制即可。

图层几何学

布局

UIView主要的三种布局属性:frame,bounds和center。
CALayer与之对应的属性为frame,bounds和position。
其中center和position属性是针对于父图层的anchorPiont所在的位置。
在操作UIView的frame、bounds和center属性时,实际上对应修改的是CALayer的属性。
frame是一个虚拟属性,是根据bounds、position和transform计算出来的,修改frame是对应修改其他值,修改其他值时会影响frame。当frame进行了变换之后,frame的宽高对应变换之后的宽高,这时bounds的宽高与frame的宽高就不在一样了。
视图的center和position属性指定了anchorPoint相对于父视图的位置。图层的anchorPoint通过position来控制frame的位置,可以认为anchorPoint是一定的把柄。
默认anchorPoint在图层中间,这也是UIView把position对应center属性的原因。但图层的anchorPoint可以被移动。
和contentReck与contentCenter属性类似,anchorPoint也是单位坐标。默认值是{0.5,0.5},也可以放在图层之外。

坐标系

和视图一样,图层在图层树中也是相对于父图层按层级关系放置,一个图层的position依赖于他父图层的bounds,如果父图层发生移动子图层也会跟着移动。
这样的层级关系当处理整体移动时,相当遍历。但当需要知道一个图层相对于另一个图层的相对或绝对位置时,CALayer提供了转换工具方法。

-(CGPoint)convertPoint:(CGPoint) fromLayer:(CALayer*)
-(CGPoint)converPoint:(CGPoint) toLayer:(CALayer*)
-(CGRect)converRect:(CGRect) fromLayer:(CALayer*)
-(CGRect)converRect:(CGRect) toLayer:(CALayer*)

这些方法用于将指定图层下的点或区域转换成另一个图层下的点或区域

翻转的几何结构

通常,在iOS上position位与父图层的左上角,而MAC位于左下角。Core Animation通过geometryFlipped属性来适配这种情况,决定是否相对于父视图垂直翻转。如果在iOS上设置为YES,则会导致该视图的所有子视图,都会以底部排版而不是顶部排版。

Z坐标轴

与UIView严格的二维坐标不同,CALayer存在于三维空间中,CALayer的另外2个属性,zPosition和anchorPointZ都是在Z轴上描述位置的浮点数。
zPosition并不常用,除了变换之外,最实用的功能就是改变视图的顺序。

Hit Testing

不建议创建独立的图层,还有一个重要的原因就是不用处理额外的复杂触摸事件。CALayer一般不处理响应者链事件,所以不能处理事件或手势。但还是提供了方法用于可能的需求

-(BOOL)containtPoint:(CGPoint)
-(CALayer*)hitTest:(CGPoint)

containtPoint当前图层是否包含该点,而hitTest用于该点坐在的CALayer对象。

自动布局

不建议创建独立图层的另一个原因,CALayer在iOS上不能自动适配。CALayerManger协议和CAConstrainLayerManger类仅限MacOS,重写

-layoutSublayerOfLayer:(CALayer*)

方法,当调用setNeedLayout时会调用该方法,来重新布局。
因此建议使用UIView,UIView提供了autoresizingMaskconstrains属性来使用屏幕旋转。

视觉效果

  • 圆角

CALayer的属性connerRadius控制着圆角的曲率(默认为0,即直角)。该属性只影响背景颜色,不影响子图层和背景图,当设置masksToBounds为YES是,图层内所有都系都会被截取。

  • 图层边框

CALayer的borderWidthborderColor属性,定义了边框的绘制样式。这条线会沿着图层的bounds绘制,当然也包括圆角。
borderColorCGColorRef类型,非Cocoa内置对象,虽然在持有和释放上和NSObect类似,但是只能使用assgin声明。边框是跟随着图层边界变化的而非图层的内容。

  • 阴影

shadowOpacity阴影不可见度(默认是指0),数值在0.0 ~ 1.0间的浮点数。如果要对阴影进行编辑可以操作:shadowColorshadowOffsetshadowRadiusshadowPath四个属性。
shadowOffset默认值是{0,-3},所以默认阴影会向上偏移。由于Core Animation是从图层套装演变而来(可以认为是iOS的私有动画框架),但确是由Mac先问世的。
shadowRadius控制着阴影的模糊度(默认值0),数值越大阴影和图层的边界就越不明显。
shadowPath控制隐形的形状,是一个CGPathRef类型,属于Core Graphics框架。

  • 图层蒙板

可以通过masksToBounds裁剪图层,可以通过borderCoor等属性控制阴影。可以通过图片展示图形,但是都不能动态的绘制生成图层蒙板,也不能让子图层被剪裁成同样的形状。
mask属性,类型就是CALayer,致使父图层根据mask的轮廓绘制。
因为本身是动态属性,可以被做成动画。

  • 拉伸和过滤

minificationFiltermaginficationFilter属性。总的来说,当显示一个图片的时候应该是1:1,即等比显示。原因如下:

  • 能够显示更好的画质,像素没有被压缩也没有被拉伸。
  • 能更好的使用内存。
  • 不消耗CPU进行额外计算。

但需求可能需要我们显示一张不按照比例的图片。CALayer提供了三种拉伸过滤方法:

kCAFilterLinear
kCAFilterNearest
kCAFilterTrilinear

默认都是kCAFilterLinear,这个方法采用双线性率波算法。(双线性滤波算法通过对多个像素采样,最终生成新的值,得到一个不错的拉伸)但是,当放大太多倍时图片就会模糊不清。
kCAFilterLinearkCAFilterTrilinear非常相似,大部分时候看不出区别。(但较双线性滤波而言,三线性滤波算法,存储了多个大小情况下的图片,并三维取样,同时结合大图和小图的存储进而得到最后的结果。这个算法的优点在于,在一系列接近最后大小的图片中取样,也就是不会更多像素同步取样。这样子做提高了性能,也避免了因小概率取舍错误引起的取样失灵问题)。
对于大图来说,双线性滤波和三线性滤波表现的更好。
kCAFilterNearest方法,取样就近原则。
优点:很快
缺点:压缩图片效果不好,方法图片会出现马赛克或像素块。
针对于没有斜线的小图来说,最近过滤算法很好
总结:线性算法保留了形状,就近算法保留了像素差异

  • 组透明

相对于UIView的alpha属性,CALayer的属性是opacity,这两个属性会影响子图层。
但是如果视图包含子视图,子视图的背景色会对透明造成困扰。这时候可以配置组透明。
1、首先在plist文件中添加UIViewGroupOpacity属性设置为YES
2、CALayer的属性shouldRasterize设置成YES。(设置前,图层和子图层会被视为一个整体,从而不会有透明混合问题)如果设置了该属性,就需要调整rasterizationScale属性匹配屏幕(默认值1.0),否则在Retain屏幕上可能出现像素化问题。

  • 变换
    • 反射变换
      UIView的transform属性是一个CGAffineTransform类型,可用于二维空间坐旋转、缩放和平移等。
      CALayer也有transform属性但是接收的是CATransform3D。对应二维变换的属性是affineTransform
      二维变换主要函数如下:
      生成CAffinetransform对象
      CGAffineTransformIdentity
    • 主要变换
      CGAffineTransformMakeRotation(CGFloat angle)
      CGAffineTransformMakeScale(CGFloat x,CGFloat y)
      CGAffineTransformMakeTranslation(CGFloat tx,CGFloat ty)
    • 组合变换
      CGAffineTransformRotate(CGAffineTransform t,CGFloat angle)
      CGAffineTransformScale(CGAffineTransform t,CGFloat x,CGFloat y)
      CGAffineTransformTranslation(CGAffineTransform t,CGFloat x,CGFloat y)
    • 混合
      CGAffineTransformConcat(CGAffineTransform t1,CGAffineTransform t2)

变换的顺序影响变换结果

  • 3D变换

CATransform3DCGAfineTransform不同,前者由Core Animation 框架提供的,相对于后者提供了z轴接口。后者是由Core Graphices提供严谨的2D绘制。
常用函数:

CATransitionform3DMakeRotation(CGFloat angle,CGFloat x,CGFloat y,CGFloat z)
CATransitionform3DMakeScale(CGFloat sx,CGFloat sy,CGFloat sz)
CATranstionform3DMakeTranslation(CGFloat tx,CGFloat ty,CGFloat tz)

虽然属于3D变换,但是转换之后看起来和2D转换没有区别,依然是扁平化的。原因在于当视图转换之后我们屏幕展示视图的角度随之改变,所以看起来和2D转换没有区别。

  • 透视投影

为了展示出3D变换效果,需要调整3D变换参数中的改变摄像机距离。在CATransition3D中的z轴参数,m34(默认值0)。在实际取值时,按照常规习惯m34 = - 1.0 / d。d的范围在500 - 1000 左右,过大透视不明显,过小会显得失真。

  • 灭点

透视视角绘图时,离相机视角越远物体越小,到达极限距离后所有的视图都会缩成一个点。
Core Animation定义这个点位于变换图层的anchorPoint(锚点是可修改的)。
在做转换前需要谨记,应保证灭点和屏幕的灭点相同,通过函数进行变换操作,而不是改变其position属性。

  • sublayerTransform属性

一个Transiform3D类型属性,所有子视图都将继承该属性,当变化时不需要逐个设置。

  • 背面
    当图层被旋转180度后,GPU还会绘制背面图(反向绘制)。可以通过CALayer的doubleSided属性控制是否绘制背面。

专用图层

  • CAShapeLayer

CAShapeLayer是通过一个矢量图而不是bitmap绘制绘制图层子类。可以指定颜色和线宽等属性,使用CGPath定义绘制路径,最后通过CAShapeLayer自定渲染。当然也可以使用Core Graphics 进行绘制,但使用CAShapeLayer有如下优点:

  • 渲染速度快。启用硬件加速。
  • 更小的内存消耗。不需要创建寄宿图。
  • 不会被边缘裁掉。
  • 不会像素化。

性能更优越的圆角方案,通过BezierPath更具当前寄宿图绘制圆角矩形,把路径赋值更CAShapeLayer的path属性,不创建寄宿图,没有优化问题。😁

  • CATextLayer

UILabel的核心绘制方法,UILabel在iOS6之前是使用WebKit实现的绘制的。
优点:

  • 绘制速度更快
  • 更多样的绘制特性

绘制间距差异:
由于绘制机制不同导致的绘制间距和字体可能有细微的差异。

注意事项:
由于contentScale属性默认为1.0,所以在Retain屏幕上可能会失真,需要设置。
字体是CTFontRef类型,需要使用CGFontRef作为中间值进行转换。
string接收NSStringNSAttributedString
替换UILabel的绘制方案
实现一个UILabel的子类,修改其寄宿图类型(UIView会通过+layerClass方法获取寄宿图类型),利用UIView和CALayer的平行层级关系,只要重写部分赋值代码,即可。

  • CATransformLayer

不同于其他的CALayer子类,他不会扁平化子图层。即不会将Z = 0,子图层将包有原来的层级和绘制3D关系。
相对于其他CALayer,不会被实现的属性有:
backgroundColor,contents,border style,stroke style等属性
2D图像中:filters,backgroundFilters,compositingFilter,mask,masksToBoundsshadow style等属性
不会实现组头名策略和图层不透明策略
不应调用hitTest:方法 ,因为可能该图层没有2D坐标对应。

  • CAGradientLayer

用于绘制一或多种渐变色的图层。使用Core Graphics 绘制一个CAGradientLayer实现渐变色也是可以的,但是CAGradientLayer绘制开启了硬件加速所以更为快速。
常用属性:
colors 接收渐色彩数组(接收颜色为CGColorRef类型)
locations 接收NSNumber类型数组,用于保持阶梯绘制时停止绘制梯度。
startPoint/endPoint 起/止点 单位坐标
type绘制类型

  • CAReplicatorLayer
    用于绘制指定数量并复制几何变化,颜色和时间参数等属性的Layer。
    常用属性:
    instanceCount创建数量
    instanceDelay指定延迟
    instanceTransform指定变换
    preservesDepth 是否减少属性和CATransformLayer类似,默认是NO
    InstanceColor 默认颜色
    instanceRedOffset/instanceGreenOffset/instanceBlueOffset/instanceAlphaOffset 对应较少的颜色或透明度的值

  • CAEmitterLayer
    用于绘制粒子效果的图层

    • CAEmitterCell
      绘制粒子效果时的子图层
      常用属性有:
      +emitterCell 构造方法
      enabled 是否显示
      emitterCells 可选属性包含emittercell的子类
      color 每个粒子的颜色
      redRange/redSpeech/blueRange/blueSpeech/greenRange/greenSpeech/alphaRange/alphaSpeech 颜色和透明度的变化范围和速度
      magnificationFilter 方法滤波
      minificationFilter 缩小滤波
      minificationFilterBias 缩小过滤偏好
      scale /scaleRange 显示比例和比例范围
      name cell的名字
      style 默认为nil,一个键值对形式的字典,通过键赋值。也可通过暴露的方法defaultValueForKey:进行设置(如果该值不能被保存在字典内时)
      spin/spinRange 旋转速度和旋转速度范围
      emissionLatitude/emissionLongitude/emissionRange 横向/纵向发射角度和锥形发射角度
      lifetime/lifetimeRange 生存时间和生存时间范围
      birthRate 每秒生成数量
      scaleSpeech 比例改变速度
      velocity/velocityRange 初始速度和速度范围
      xAcceleration/yAcceleration/zAcceleration x/y/z轴加速度
      -shouldArchiveValueForKey: 检查时候包含这个键
      +defaultValueForKey: 设置键值

常用属性有:
emitterCells 包含的cell类型
renderMode 定义了颗粒细胞呈现层
emitterPosition 效果位置点
emitterShape 效果形状
emitterZPosition z轴形状
emitterDept 深度
emitterSize 大小
Emitter Shape 定义了三个方向上的大小 相关属性emitterPostion/emitterzPosition/emitterDept/emitterSize
Emitter Mode 定义了发射模式 相关属性emitterMode
Emitter Render Order 发射顺序 对应属性renderMode

  • CAScrollLayer

类似于ScrollView的图层,可用于显示实际大小大于自身的视图。
系统提供了:

-scrollPoint:(CGPoint)
-scrollRectToVisible:(CGRect)

visibleRect 可视范围
scrollMode NSString类型用于显示当前滚动状态

  • CATiledLayer

区域化加载图层,自带淡入淡出效果。用于解决过大视图导致的加载性能问题。
常用方法
+fadeDuration 淡入效果加载时间,单位秒。
titleSize 每个单元的绘制大小,默认{256,256}
levelsofDetail 等级区分,每个等级的的详细程度是前者的一半,如果有多个数自返回,默认返回最大值。最大级别屏幕将只显示一个像素点。
levelOfDetailBias 可放大倍数,默认为0。(如果为2则可放大至2x和4x)

动画

  • 隐式动画

当修改CALayer可动画属性是默认由CATransation默认执行了0.25秒的动画,CATransation没有创建方法,仅提供+commit和+begin方法。(而UIView的beginAnimation:/commitAnimation还有block动画等方法都是对CATransation的封装。)
隐式动画执行步骤:
1.图层检测是否包含代理,是否实现-actionForLayer:forKey,有就调用代理方法。
2.如果没有,则检查包含属性名称的Action字典。
3.如果仍没有,则调用style字典搜索属性名。
4.还是没有的话,调用defaultActionForKey:方法。
还可以CATransation提交之前设置setDisableAnimation:方法,也可关闭所有隐式动画。
因为UIView默认为CALayer的代理,当不在动画提交块内时,对应的actionForLayer:forKey方法返回nil,有动画block时返回对应的值。

  • 开启隐式动画

    1.继承UIView实现对应的代理方法
    2.设置CALayer的代理,自己实现该方法
    3.设置action字典,供调用时获取

  • 呈现与模型
    CALayer的默认设置很不正常,改变一个值为什么会有一个过程。是因为CoreAnimation扮演了一个控制器的角色,根据图层行为和事务设置刷新屏幕的显示状态。这是一个典型的MVC模型,CALayer负责显示和存储视图模型。
    因为屏幕每秒刷新60次,只要动画时间比60分1秒长,CoreAnimation需要绘制中间值,意味着CALayer需要存储中间值,每个图层的显示值都会被存放在一个叫呈现图层独立图层中,可以通过-presentationLayer进行访问,这个呈现层实际上是模型图层的复制,但它的属性值代表了任意时刻下当前图层的外观效果,也就是说该图层是当前图层的显示出来的值。
    呈现树由当前图层中所有的呈现图层所形成。呈现图层仅仅当首次被提交是才存在,之前调用时不存在的。-modelLayer返回呈现所依赖的Layer通常是self。

  • 利用呈现图层:

需要实现类似定时器的动画时,需要知道每时间点图层的具体位置。
当需要在变换过程中进行交互时,由presnetationLayer来处理交互,展示更为合理。

  • 取消动画
    CALayer的方法可以获取和移除对应的动画
-(CAAnimation*)animationForKey:
-removeAnimationForKey:
-removeAllAnimations

当动画被移除,图层的外观将立刻更新到当前模型图层的值。除非在设置动画对象的属性removedOnCompletionNO,否则动画将一直保存,直到图层被销毁。

  • 图层时间
    CAMediaTiming协议,定义了一段动画内用来控制逝去时间的属性的集合。CALayerCAAnimationCAEmitterCell都实现了该协议,所以时间可以被任意基于基于一个图层或这一段动画的类控制。
  • 持续和重复

协议中durationrepeatCount属性分别是单词持续时间和重复次数,假设一个动画的duration为2,repeatCount位2.5次那么动画的播放时间就是5秒。
假设durationrepeatCount都为0,不表示不执行动画。仅代表默认值,0.25秒和1次。还有2个比较有意思的控制属性,repeatDurationautoreverses,repeatDurationrepeatCount相似当设置为INFINITY时都将无限重复下去,而autoreverses则表示方向动画,即方向重复动画效果,因此,可以设置一些连续的非重复动画。

  • 相对时间

Core Animation中定义了时间,所有的时间都是相对的。每个动画都有自己的描述时间,可以单独加速或者设置偏移。
beginTime 动画开始的延迟,默认0.即加载到视图上立刻开始动画
timeOffset 快进到某一位置
speed 动画播放速度

  • fillMode
    对于beginTime非0的动画会出现,加载到图层上什么都没发生的状态,在autoreverses是默认值NO的图层上在动画结束时保持之前的状态。在动画开始之前和结束之后,被设置的动画属性将会是什么。
    一种可能属性和动画没被添加之前保持一致,也就是模型图层定义的值。
    另一种可能保持动画开始的那一帧和结束的那一帧,这就是所谓的填充,用动画开始的值和结束的值,填充动画开始之前和结束之后的时间。
    fillMode属性如下
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved (默认值)

注意事项:
需要将animation对象设置为键值非空,可以在动画完成后手动移除。
修改removedOnCompletionNO(默认为YES)

  • 手动动画

通过修改timeOffset来控制动画的进度,当speed为0是,动画不自动播放,可以通过设置timeOffset的值来达到执行动画的目的。在使用复杂动画组的时候尤为明显。

  • 缓冲

Core Animation 使用缓冲使动画更为平缓和自然。

  • 动画速度

动画实际就是一段时间内的变换,也暗示着动画会随着某一特定的速率进行。
速度速率计算如下
velocity = change/time
这里的速度可以时移动的距离,也可以是背景颜色等可设置动画效果的属性。
为了达到更为真是的动画效果,即非线性动画效果。Core Animation 提供了CAMediaTimingFunction类对象,通过修改CAAnimation的timingFunction属性或通过CATransation的 +setAnimationTimingFunction:方法。
通常调用+timingFunctionWithName:传入对应字段,创建CAMediaTimingFunction对象,常用字段如下

kCAMediaTimingFunctionLinear 线性函数
kCAMediaTimingFunctionEaseIn 缓慢开始线性结束
kCAMediaTimingFunctionEaseOut 线性开始缓慢结束
kCAMediaTimingFunctionEaseInEaseOut 缓慢结束缓慢开始
kCAMediaTimingFunctionDefault 相对于上面的选项开始速度快和结束的速度更为缓慢,因此也更为真实(UIView动画默认函数)
  • UIView的动画缓冲

UIKit动画也有缓冲函数,option选项如下:

UIViewAnimationOptionCurveEaseInOut (默认值)
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionLinear
  • 缓冲和关键帧动画

CAKeyframeAnimation有一个NSArray类型的属性timeFunctions。当设置时,其数量一定要是keyframes数组的元素减一个,因为该函数式描述2个属性动画速度的函数。
自定义缓冲函数
除了functionWithName:外,CAMediaTimingFunction还提供给了+functionWithControlPoints::::-initWithControlPoint::::函数,用于提供自定义缓冲函数,使用三次贝塞尔曲线将输入的时间转化为起点和终点之间的成比例改变。
可以通过CAMediaTimingFunction提供的-getControlPointAtPoint:Values:方法(point参数必须介于0 ~ 3之间),获取的Value是个float[]数组,传入对应指针可以获取这个点的值。
因为本身是范围坐标,开始点{0,0}结束点{1,1}。可以通过UIBezierPath和CAShapeLayer绘制出来,这也许就是为什么返回值是float数组的原因(方便取值...)。

  • 更为复杂的缓冲动画

当时需要实现类似球体落地缓冲效果时,单纯依靠三次贝塞尔曲线可能无法完成,可以借助CAKeyframeAnimation设置多次动画过度函数和对应的值,进行仿真函数展示。

  • 自动化流程

如果将复杂函数中的划分为更为细小的步骤,利用关键帧动画就可以实现动画的自定义变换(不用去自己绘制贝塞尔曲线)。
value = (endvalue - startvalue) * time + startvalue
代码示例:

float interpolate(float from,float to,float time) { 
return (to - from) * time + from;
}
- (id)interpolateFromeValue:(id)fromeValue ToValue:(id)toValue Time:(float)time { 
      const char *type = [fromValue objCType];
       if (strcmp(type,@encode(CGPoint))== 0) {  
           CGPoint from = [fromeValue CGPointValue];   
           CGPoint to = [toValue CGPointValue];   
           CGPoint result = CGPointMake(interpolate(from.x,to.x,time), interpolate(from.y,to.y,time));   
            return [NSValue valueWithPoint:result];
       } 
      return time < 0.5?formValue:toVlaue;
}
- animation { 
      self.ballView.center = CGPointMake(150,32); 
      NSValue *fromeValue = [NSValue valueWithPoint:CGPointMake(150,32)]; 
      NSValue *toValue = [NSValue valueWithPoint:CGPointMake(150,268)]; 
      CFTimeInterval duration = 1; 
      NSInteger numberCount = duration * 60;//每秒60帧 
      NSMutableArray * frame = [NSMutableArray       arrayWithCapacity:numberCount]; 
      for(int i = 0 ,i < numberCount,i ++) {   
            float time = i / (float)numberCount;    
            [frame addObject:[self interpolateFromeValue:fromValue ToValue:toValue Time:time]]; 
      } 
      CAKeyframeAnimation *animation = [CAKeyframeAniamtion animation]; 
      animation.keyPath = @"position"; 
      animation.duration = 1.0; 
      animation.values = frame; 
      [self.ballView.layer addAniamtion:animation forKey:nil];
}

因为函数可能展示结果不是很友好,如果需要完成非常复杂的缓冲。需要借助数学功底,还好我们有罗伯特 彭纳。包含了大多数缓冲动画函数,下面展示是一个缓冲进入缓冲退出的函数

float quardraticEaseInOut(float t){ 
      return t < 0.5 ? (2 * t * t) :(-2 * t * t) + (4 * t) - 1;
}
//对于Spring动画
float boundsEaseOut(float t) {
      if(t < 4/11.0) {  
             return (121 * t * t) / 16.0;
       } else if (t < 8/11.0) {   
            return (365 / 40.0 * t * t) - (99 /10.0 * t) + 17/5.0; 
      } else if (t < 9/11.0) {   
            return (4365 /361.0 * t * t) - (35442 / 1805.0 *t) +     16061/1805.0;
      } else {   
            return (54/5.0 * t * t) - (513 / 25.0 * t) + 268 /25.0; 
      }
}

替换实现函数,即可修改动画效果。

  • 基于定时器的动画
    • 定时帧
      iOS按照每秒60次刷新屏幕,然后CAAnimation需要每次都计算新的帧并计算差值与缓存。需要一个定时器,以便按照屏幕刷新次数计算需要绘制需要绘制的值。当选取定时器时CADisplayLink比NSTime更为合适,有点如下:
      • 在有延迟时,默认忽略延迟帧。
      • 定时器会根据屏幕刷新频率计算。

性能优化

  • CPU VS GPU
    绘图和动画有2中处理方式:通过CPU或GPU。CPU所有的工作都在软件层面上,而GPU在硬件层面上。
    通常CPU可以处理所有事情,但是对于图像处理使用硬件的话会更快,因为GPU绘制图像时对高度浮点运算进行了优化。但是因为GPU没有无限制处理性能,当资源消耗加剧后,性能就会开始下降。
    要处理好动画性能优化,应先了解Core Animation 如何在2个处理器间工作。
    • 动画的舞台
      Core Animation 在iOS中是核心库之一,应用内和应用间多会用到。一个单独的动画可能会涉及多个APP的内容,如程序切换,分屏显示等(iOS 中不能实现,因为APP都有独立的沙盒)。
      动画和屏幕组合的图层,被一个单独的进程控制而非当前的APP,这就是渲染服务。iOS5之前是SpringBoard,iOS6之后是BackBoard
      当运行一个动画时,该动画会被分离成独立的四部分。
    • 布局 — 准备视图/图层的层级关系和图层属性的等。
    • 显示 — 图层图片被绘制,可能涉及drawRect:drawLayer:inCentext:
    • 准备 — 准备发送动画数据到渲染服务,同事CoreAnimation将要执行以下其他操作,解码动画过程将要显示的图片的时间点。
    • 提交 — 打包所有的图层和动画属性,通过IPC发送到渲染服务进行显示。

当打包图层和动画属性到渲染服务进程,他们会被发序列化成另一个叫做渲染树的图层树。通过渲染树,渲染服务对每一帧做如下工作:
对所有图层属性计算中间值,设置OpenGL几何形状来执行渲染。

  • 在屏幕上渲染可见的三角形

最后2个阶段在动画过程中不断重复,前5个阶段CPU处理,最后GPU执行。开发者能控制的只有前2个阶段:布局和显示。CoreAnimation在内部处理其他事物,无法控制。在布局和显示阶段,可以决定哪些由操作由哪个U处理。

  • GPU相关操作
    GPU进行的优化:通过采集图片和形状,运行变化,应用纹理和混合把他们输送到屏幕上。除非自己实现OpenGL着色器,从根本上解决硬件加速问题,但其他处理还是需要CPU进行。
    宽泛地说,大多数的CALayer都是由GPU绘制的。如绘制边框和底色或是设置contents属性切割图片等。
    但是太多的事情会降低(基于GPU)图层绘制:
    1.太多的几何结构
    其瓶颈不在GPU绘制能力而在于CPU通过IPC提交到渲染进程时,太多的图层会引发CPU瓶颈。
    2.重绘(每一帧用相同的像素多次填充)
    主要由重叠的半透明图层引起。GPU的填充比率是有限的,所以应避免重绘。因为iOS设置中GPU有很高的重绘比率,因此不必过分担心。
    3.离屏绘制
    发生在当不能直接在屏幕上绘制,必须绘制到离屏图片的上下文中。离屏渲染发生在基于CPU或是GPU渲染,或是为离屏图片分配内存,以及切换上下文,这些都会降低GPU性能。对于特定图层的使用如圆角、图层遮罩、阴影或是图层光栅化都会强制CoreAnimation 提前渲染图层。
    4.过大的图片
    如果图片的绘制超过了GPU支持的2048 x 2048 或是 4096 x 4096纹理,GPU每次需要在显示图片前进行预处理,同样会降低GPU性能。

  • CPU相关操作
    于CoreAnimation相关的大部分CPU操作都发生在动画之前,这意味着很少会因为CPU操作印象动画帧频,但是肯能造成动画延迟提交,导致显示有迟钝感。
    造成CPU延迟动画操作:
    1.布局计算
    过于复杂的布局逻辑,需要消耗CPU大量的计算时间。iOS6之后的自动布局尤为明显。
    2.视图懒加载
    iOS只会在视图控制器的视图显示到屏幕上时才会加载它。因此当加载完成前,都不能相应对应的操作。
    3.Core Graphics
    对视图实现了drawRect:drawLayer:inContext:方法,通过CoreGraphics绘制时,需要创建等大小的寄宿图,绘制完成后通过IPC提交到绘制服务。
    4.解压图片
    PNG和JPEG压缩技术会使图片小于等质量的位图,但在展示时需要解压操作。iOS会在真正绘制图片的时候才解压,过大的图片会消耗CPU解压时间。
    5.视图层级
    当图层被成功打包发送到渲染服务后,CPU仍要进行如下操作:
    为了显示屏幕上的图层,CoreAnimation必须渲染树中每个可见图层通过OpenGL循环转化为纹理三角板。由于GPU不知道渲染树层级关系,因此是由CPU处理转换操作。CPU转换工作和处理的图层数量程正比,如果图层关系过于复杂 ,会导致CPU渲染每一帧。
    6.IO操作
    还有一项涉及到的就是IO相关操作,上下文中的IO指的是内存或网络接口的硬件访问。2个控制器间的过度可能需要从nib文件中懒加载,或是旋转一个占用内存过大的图片,需要动态的滚动加载。
    IO加载相对内存操作更慢,如果涉及动画就会是个很大的问题,之后了解到多线程、缓存和投机加载等解决方案。

  • 高效绘图

    • 软件绘制
      通常在Core Animation中使用绘图代替软件绘图(不通过GPU协作的绘图)。iOS中绘图通常由Core Graphics 框架来完成。在特定的场景下相比CoreAnimation和OpenGL,Core Graphics运行速度会慢一下。
      Core Graphics 不仅效率低下,还会消耗可观的内存。已全屏绘制为例,屏幕像素乘以放大倍数就是绘制的图片大小。每次绘制都要擦除内存,然后重新分配。
      软件绘制代价高昂,除非必要。尽量避免重绘视图。提高性能的秘诀
      尽量避免去绘制

    • 矢量图形
      在使用Core Graphics进行绘制时,不能单纯的使用图片或图层绘制完成的
      如下:
      1.任意多边形
      2.斜线或曲线
      3.文本
      4.渐变
      当实现简单绘制入画板工作时,可使用特殊的layer层,代替Core Graphics达到节省内存提高绘制效率

    • 脏矩形
      为了减少不必要的绘制,Mac OS和iOS设备会把屏幕分成需要重绘和不需要重绘的区域。需要重绘的区域称为脏区域 。在实际应用中,鉴于区分非矩形区域边界的剪裁和混合复杂性,通常会区分出包含指定视图的矩形位置,这个位置就是脏矩形 。
      因此可以通过脏矩形减少图形重绘的区域,已缓解内存和CPU的压力。
      调用setNeedDispayInRect:传入需要重绘的区域,在drawRect:drawLayer:InContext:方法中对该区域进行绘制,CPU会忽略超出部分。可以通过CGContextGetClipBoundingBox()函数从上下文中获取到具体的绘制区域的大小。
      相对于CoreGraphics的重绘,裁剪出来的区域可避免不必要的操作。但是如果重绘机制过于复杂,还是应该借助CoreGraphics进行绘制。为了避免在重绘区域内重绘已存在的绘制,避免重复,可以通过CGRectIntersectsRect()确定是否包含在进行绘制

    • 异步绘制
      UIKit单线程的本质,要求绘制一定在主线程,因此绘制可能会影响到用户交互。UIKit提供了2种方式绘制:CATiledLayerdrawsAsynchronously

      • CATiledLayer
        每个CATiledLayer的区块,都支持同时调用drawLayer:InContext:方法,避免了绘制影响用户交互,是很优秀的异步绘制方案。
      • drawsAsychronously
        drawsAsychronously不同于异步绘制,只是延迟提交。绘制仍处于主线程,但是Context对象不会立刻绘制,而是通过队列逐步绘制。也可以避免绘制影响用户交互操作。
    • IO操作
      关于图片操作中最消耗CPU时间的就是解压,一般其他方式达到延迟解压已环节CPU压力
      1.延迟解压
      一般使用三种方式延迟解压
      调用imageWithName:方法,和imageWithContentsOfFile:不同。会在加载图片之后立即进行解压。(但只对存在在应用资源束内的对象管用)
      把图片设置为图层内容,类似于UIimageView的image。但是采用主线程,几乎没有提升。
      2.提前解压
      使用UIKit加载图,并立刻在CGContext绘制。图片需要在绘制前解压完成,因此强调了解压的及时性和有效性。绘制图片可以在后台绘制,不必要阻塞线程。
      一般使用如下两种方式:

      • 通过CoreGraphics绘制图片的一个像素,因为绘制几乎不花时间,并且解压了图片。强调绘制的任意性。可以通过单纯依靠系统的内存管理。
      • 通过CoreGraphics绘制整张图片,用以保存和替代原有图片。可以对绘制进行优化。但是需要持有图片,而不是单纯依靠系统内存管理。
        3.CATiledLayer
        利用CACATiledLayer异步绘制,避免阻塞线程。但是缺点也很明显
        缓存算法和异步绘制机制不可控制,没有预留接口
        每次都需要调用绘制机制
        使用时的注意点:
        使用的时候titleSzie是像素,而不是屏幕上的点。因此需要匹配屏幕像素
        由于是相同的代理方法,因此需要在drawLayer:inContext:中进行区分
    • 分辨率交换
      高分辨屏幕能展示更好的效果,但是对于动图区别不大,因此可以在滚动视图的代理中用低分辨率的图片代替高分辨率的图片已达到优化效果。

      • 缓存
        缓存的实质:就是存储昂贵的计算结果在内存中,以便访问,因为读取内存很快。问题的本质就是权衡性能和有限的内存。
        +imageNamed:方法
        优点很明显,会立刻进行解压,并在内存中保存缓存。
        缺点:
      • 仅能加载资源束中的资源
      • 缓存大图后,不可能释放
      • 缓存机制不可控
    • 自定缓存
      构建一个缓存机制很困难,缓存和命名是最难的
      自定义缓存应注意的点

      • 合适的缓存空间,避免重复
        合理的分类缓存和解压分配,当加载比较大的耗时资源时,应提前绘制和解压
      • 缓存失效机制
    • 内存回收机制

      • NSCache
        UIKit提供的和NSDictionary相似的集合类,可设置最大缓存,在内存吃紧时会自动丢弃对象等优点。
        所有使用NSDictionary的地方都是用NSCache。
  • 图层性能

  • 隐式绘制
    除了利用CoreGraphics直接在Context显示绘制寄宿图,还可以通过三种方式隐式绘制
    1.使用特定的图层属性
    2.特定的视图
    3.特定的图层子层

  • 文本
    CATextLayer相对于UILabel有更好的绘制性能,因此大文本应优先考虑CATextLayer。应尽量避免修改文本的frame,修改frame会在导致文本重绘。

  • 光栅化
    启用CALayer的shouldRasterize属性,可以解决透明图层的混合失灵问题。
    启用shouldRasterize后,会在屏幕外绘制并缓存,会消耗更多的内存和计算时间,但是会是展示更为顺畅,相对于重绘来说更为合理。应避免在不断变化的图层上使用,否则会使内存更为不合理。

  • 离屏渲染
    当图层的混合体被指定为在预合成前不能直接在屏幕上绘制时,就会出现离屏渲染。但是离屏渲染相对于光栅化来说,没有很大的内存和绘制消耗,结果没有缓存,并且会被释放。因此可以开启光栅化作为优化方式,但前提是图层不会被重复绘制。对应那些需要动画和离屏绘制的图层来说,可以使用CAShapeLayercontentCentershadowPath来到相同的效果并提高性能。

  • 常见引发离屏渲染的方式
    contentRadiusmaskToBounds一起使用

  • 图层蒙版

  • 阴影

    • CAShapeLayer
      相对于contentRadiusmaskToBounds,使用CAShapeLayerUIBezierPath不会造成离屏渲染问题。
    • 可伸缩的图片
      使用contentCenter可以通过绘制拉伸圆角图片达到更为良好的性能,因为绘制一个图片仅需要3*3的网格绘制,仅仅需要18个三角形。甚至可以使用contentCenter实现阴影效果。
    • shadowPath
      在绘制一个简单的几何矩形或圆角矩形时,假设不包含任何透明图层,创建阴影相对容易,CoreAnimation创建阴影也相对简单。但是如果是复杂图形可以考虑先生成对应的阴影图片。可以节约性能。
  • 混合和过度绘制
    GPU每一帧可绘制的像素都有一个最大限制(即所谓的fill rate),这个情况下可以绘制整个屏幕的所有像素。由于重叠图层的关系需要不停绘制的同一区域时,就会出现掉帧现象。
    GPU会放弃完全被遮蔽的图层,合并透明的重叠图层会很消耗计算时间,为了加速处理进程,尽量不使用用透明图层。
    可以通过优化如下属性进行加速优化
    - 给视图的backgroundColor设置一个固定的不透明色
    - 设置不透明属性opaque为true
    - 可以通过shouldRasterize属性将一个不透明区域设置为图片,以减少重绘提高效率。
    - 减少图层数量
    由于绘制使用过IPC打包渲染,最后由OpenGL转换为三角形,由于每次绘制的图层数量也有最大图层上限,因此应尽量减少图层数量,较少计算时间。
    - 裁剪
    在进行优化之前,需要确认不是在创建一些不可见图层。图层在以下情况不可见。
    - 图层在屏幕边缘或父图层边缘之外
    - 在一个完全不透明的图层之后
    - 完全透明

CoreAnimation很擅长处理对视觉效果无意义的图层,但是创建时应该可以确定图层是否可见。应避免创建和配置不可见图层。

  • 对象回收
    基于UITableViewUICollectionViewMKMapView都利用了对象池概念,即重复利用而不是创建加回收机制,可以有效的避免重复创建节约计算时间。
  • CoreGraphics
    虽然绘制现对于GPU图层数来说更为耗时,但是如果图层数量过度,通过绘制较少图层数量,也可以减少子图层的关系。
    • renerInContext方法
      通过CALayer的renerInContext方法,可以获取对应图层的静态图片,可以将它赋值layer的contents完全遮蔽后面的图层既可以,相对于CoreAnimation维护的复杂图层树,这样子客观节约了性能。

推荐阅读更多精彩内容