iOS,Core Animation
简介
图层树
寄宿图
图层几何学
视觉效果
变换
专有图层
隐式动画
显示动画
图层时间
缓冲
基于定时器的动画
性能调优
高效绘图
图像IO
图层性能
简介
UIKit API
UIKit是一组Objective-C API,为线条图形、Quartz图像和颜色操作提供Objective-C 封装,并提供2D绘制、图像处理及用户接口级别的动画。
UIKit包括UIBezierPath(绘制线、角度、椭圆及其它图形)、UIImage(显示图像)、UIColor(颜色操作)、UIFont和UIScreen(提供字体和屏幕信息)等类以及在位图图形环境、PDF图形环境上进行绘制和 操作的功能等, 也提供对标准视图的支持,也提供对打印功能的支持。
UIKit中UIView类本身在绘制时自动创建一个图形环境(对应Core Graphics层的CGContext类型)作为当前的图形绘制环境。在绘制时可以调用UIGraphicsGetCurrentContext 函数获得当前的图形环境。
Core Animation
Core Animation是一套Objective-C API,实现了一个高性能的复合引擎,并提供一个简单易用的编程接口,给用户UI添加平滑运动和动态反馈能力。
Core Animation 是 UIKit实现动画和变换的基础,也负责视图的复合功能。使用Core Animation可以实现定制动画和细粒度的动画控制,创建复杂的、支持动画和变换的layered 2D视图。
Core Animation不属于绘制系统,但它是以硬件复合和操作显示内容的基础设施。这个基础设施的核心是layer对象,用来管理和操作显示内容。在ios 中每一个视图都对应Core Animation的一个层对象,与视图一样,层之间也组织为层关系树。一个层捕获视图内容为一个被图像硬件容易操作的位图。在多数应用中层作为管理视图的方式使用,但也可以创建独立的层到一个层关系树中来显示视图不够支持的显示内容。
Core Graphics 与Quartz 2D API
Core Graphics是一套C-based API, 支持向量图形,线、形状、图案、路径、剃度、位图图像和pdf 内容的绘制。
Quartz 2D 是Core Graphics中的2D 绘制呈现引擎。Quartz是资源和设备无关的,提供路径绘制,anti-aliased(抗锯齿处理)呈现,剃度填充图案,图像,透明绘制和透明层、遮蔽和阴影、颜色管理,坐标转换,字体、offscreen呈现、pdf文档创建、显示和分析等功能。
Quartz 2D能够与所有的图形和动画技术(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。
Quartz采用paint模式进行绘制。
Quartz 中使用的图形环境也由一个类CGContext表示。
Quartz 中可以把一个图形环境作为一个绘制目标。当使用Quartz 进行绘制时,所有设备特定的特性被包含在你使用的特定类型的图形环境中,因此通过给相同的图像操作函数提供不同的图像环境你就能够画相同的图像到不同的设备上,因此做到了图像绘制的设备无关性。
Core Animation
Core Animation是iOS与OS X平台上负责图形渲染与动画的基础设施。Core Animation可以动画视图和其他的可视元素。Core Animation为你完成了实现动画所需的大部分绘帧工作。你只需在配置少量的动画参数(如开始点位置和结束点位置)就可启动Core Animation。Core Animation将大部分实际的绘图任务交给了图形硬件处理,图形硬件会加速图形渲染的速度。这种自动化的图形加速让动画具有更高的帧率且更加平滑,但这并不会增加CPU的负担而导致影响你应用的运行速度。
*图层树
CoreAnimation是一个复合引擎,它的职责就是尽可能快的组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。
※图层和视图
一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套, 一个视图可以管理它所有子视图的位置。在iOS当中,所有的视图都从一个叫做UIView的基类派生而来,UIView可以处理触摸事件,可以支持基于CoreGraphics绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。
(一种典型的iOS屏幕(左边)和形成视图的层级关系(右边))
※CALayer
CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互,CALayer并不清楚具体的响应链(iOS 11通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,但是它提供了一些方法来判断是否一个触点在图层的范围指内(图层几何学)。
※平行的层级关系
每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也能同样对应在层级关系树当中有相同的操作。实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及CoreAnimation底层方法的高级接口。
iOS基于UIVie和CALayer提供两个平行的层级关系做职责分离,这样也能避免很多重复代码。在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit何UIView,但是Mac OS有AppKit何NSView的原因。他们功能上很相似,但是实现上有着显著的区别。
绘图,布局和动画,相比之下就是类似Mac笔记本和桌面系列一样应用于iPhone和iPad触屏的改了。吧这种功能的逻辑分开并应用到独立的CoreAnimation框架,苹果就能够在iOS和mac OS之间共享代码,使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。
实际上,这里并不是两个层级关系,而是四个,每一个都扮演了不同的角色,除了图层级和图层树之外,还存在呈现树和渲染树。见“隐式动画”和“性能调优”分别讨论。
※图层的能力
如果你略微想在底层做一些改变,或者使用一些苹果没有在UIView上实现的接口功能,这时除了介入CoreAnimation底层之外别无选择,下面有些UIView没有暴露出来的CALayer功能:阴影,圆角,带颜色的边框;3D变换;非矩形范围;透明遮罩;多级非线性动画
※使用图层
一个视图只有一个相关联的图层(自动创建),同时它也可以支持添加无数多个子图层,使用图层关联的视图而不是CALayer的好处在于,你能使用所有CALayer底层特性的同时,也可以使用UIView的高级API(比如自动排版,布局和事件处理)。下面情况可能更需要使用CALayer而不是UIView:开发同时可以在MacOS上运行的夸平台应用;使用多种CALayer的子类,并且不想创建额外的UIView去包封装它们所有;做一些对性能特别挑剔的工作,比如对UIView一些可忽略不计的操作都会引起显著的不同(尽管这种情况下,你可能会想直接使用OpenGL来绘图)
//示例
运行效果
*寄宿图
※contents属性
CALayer有一个属性叫做contents,这个属性的类型被定义为id,但实际给contents赋的不是CGImageRef得到的图层将是空白的。它之所以被定义为id类型是因为在Mac OS系统上,这个属性对CGImage和NSImage类型的值都起作用。UIimage有一个CGImage属性,它返回一个“CGImageRef”,但这里并不是一个真正的Cocoa对象,而是一个CoreFoundation类型,可以通过bridged关键字转换(如果没有使用ARC就不需要__bridge这部分)。如果要给图层的寄宿图层赋值可以按照下面这个方法
layer.contents = (__bridge id)image.CGImage;
※contentsGravity
CALayer与contentMode对应的属性叫做contentsGravity,它是一个NSString类型,不像对应UIKit部分,那里面是枚举。和contentMode一样,contentsGravity的目的是为了解决内容在图层的边界中怎么对齐。contentsGravity可选的常量值有一下一些:kCAGravityCenter;kCAGravityTop;kCAGravityBottom;kCAGravityLeft;kCAGravityRight;kCAGravityTopLeft;kCAGravityTopRight;kCAGravityBottomLeft; kCAGravityBottomRight;kCAGravityResize;kCAGravityResizeAspect;kCAGravityResizeAspectFil。下面等比拉伸以适应图层边界。
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
※contentsScale
contentsScale属性定义寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数(如果contentsScale设置为1.0,将会以每个点2个像素绘制图片,这就是Retina屏幕)。它并不是总会对屏幕上的寄宿图有影响,因为contents由于设置了contentsGravity属性,它已经被拉伸以适应图层的边界。UIView有一个类似功能但是非常少用到contentsScaleFactor属性。下面设置图层contentsScale属性:
layer.contentsScale = [UIScreen mainScreen].scale;
※maskToBounds
UIView有一个clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做maskToBounds。
※contentsRect
CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域,和bouns,frame不同,它使用了单位坐标,指定在0到1直接,是一个相对值(而像素和点是绝对值)。所以它们是相对于寄宿图的尺寸的。默认的contentsRect是{0, 0, 1, 1}
一个自定义的contentsRect和显示的内容
contentsRect最有趣的用处之一就是它能够使用image sprites(图片拼合),通常多张图片可以拼合后打包整合到一张大图上一次性载入。相比多次载入不同图片,这样做能够带了很多方面的好处:内存使用,载入时间,渲染性能等等。
※contentsCenter
contentsCenter其实是一个CGRect,它定义了图层中的可拉伸区域和一个固定的边框。改变contentsCenter的值并不会影响到技术图的显示,除非这个图层的大小改变了,你才看的到效果。默认情况下,contentsCenter是{0,0,1,1},这以为这layer的大小改变了,那么寄宿图将会根据contentsGravity均匀地拉伸开。但是如果增加原点的值并减小尺寸,将会在周围创造一个边框。
contentsCenter设置为{0.25, 0.25, 0.5, 0.5}的效果。
※Custome Drawing
给contents赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用CoreGraphics直接绘制寄宿图,CALayer有一个可选的delegate属性,实现了CALayerDelegate协议
//示例
运行效果
*图层几何学
※布局
UIView有三个比较重要的布局属性frame,bounds,center;CALayer对应地叫做frame,bounds和position。
frame代表了图层外部坐标(也就是在父图层上占据的空间),bounds是内部坐标({0,0}通常是图层的左上角),center和position都代表了相对于父图层anchorPoint所在的位置,现在可以把它想象成图层的中心点就行。
下面显示了这些属性是如何相互依赖的,UIView和CALayer的坐标系的坐标系
视图的frame,bounds和center属性仅仅是存取方法,当操纵视图的frame,实际上是在改变位于视图下方CALayer的frame,不能够独立于图层之外改变视图的frame。对于视图或者图层来说,frame并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据bounds,position和transform计算而来,所以当其中任何一个值发生改变,frame都会变化,相反改变frame的值同样会影响到他们当中的值。
当对图层做变换的时候,比如旋转或者缩放,frame实际上代表了覆盖在图层旋转之后整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高不再一致了;下面是旋转一个视图或者图层之后的frame属性
※锚点
前面说视图的center属性和图层的position属性都指定了anchorPoint相对于父图层的位置。图层的anchorPoint通过position来控制它的fram的位置。可以认为anchorPoint是用来移动图层的把柄。默认来说anchorPoint位于图层的中点,所以图层将会以这个点为中心放置。anchorPoint属性并没有被UIView接口暴露出来,这也是视图的position属性被叫做“center”的原因。但是图层的anchorPoint可以被移动,比如你可以把它置于frame的左上角,于是图层的内容将会向右下角的position方向移动,而不是居中了。anchorPoint用代为坐标来描述,也就是图层的相对坐标,图层左上角是{0,0},右下角是{1,1},因此默认坐标是{0.5,0.5},anchorPoint可以通过制定x和y值小于0或者大于1,使它放置在图层范围之外。下图改变了anchorPoint,position属性保持固定的值并没有发生改变,但是fram却移动了。
※坐标系
和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,如果父图层发生了移动,它的所有子图层也会跟着移动。这样对于放置图层会更加方便,因为你可以通过移动根视图来将它们的子图层作为一个整体来移动,但是有时候你需要知道一个图层的绝对位置,或者是相对于另一个图层的位置,而不是它当前父图层的位置。CALayer给不同坐标系之间的图层转换提供了一些工具类方法:(这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形)
※翻转的几何结构
在iOS上一个图层的position位于父图层的左上角,在Mac OS上通常是位于左下角。CoreAnimation可以通过geometryFlipped属性来视频这两种情况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个BOOL类型。在iOS上通过设置它为YES意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是通常的顶部(它的所有子图层也同理,除非把它们的gemetryFlipped属性也设为YES)。
※Z坐标轴
和UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。除了position和anchorPoint属性之外,CALayer还有另外两个属性,zPosition和anchorPointZ,二者都是在Z轴上描述图层位置的浮点类型。zPosition最实用的功能就是改变图层显示顺序了。通常图层是根据它们子图层的sublayers出现的顺序来绘制的,这就是所谓的画家算法--就像一个画家在墙上作画--后被绘制上的图层将会遮盖住之前的图层,但是通过增加图层的zPosition,就可以把图层向相机(这里相机相对于用户是视角,和iPhone的内置相机没有任何关系)方向前置,于是它就在所有小于它zPosition图层值的的前面了。一般给zPosition提高一个像素就可以让视图前置,当然0.1或者0.0001也能够做到,但是最好不要这样,因为浮点类型四舍五入的计算可能会造成一些不便的麻烦。
※Hit Testing
CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势,但是它又一系列方法帮你处理事件:
-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。
-hitTest:方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子节点图层。
※自动布局
在Mac OS平台,CALayer有一个叫做layoutManager的属性可以通过CALayoutManager协议和CAConstraintLayoutManager类来实现自动排版机制。但是由于某些原因,这在iOS上并不适用。最好使用视图而不是单独的图层而不是单独图层来构建应用程序的另一个重要原因之一。
//示例
运行效果
*视觉效果
※圆角
圆角矩形是iOS的一个标志性审美特性,CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率,它是一个浮点数默认为0(为0的时候就是直角),默认情况下这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果吧maskToBounds设置成YES的话,图层里面的所有东西都会被截取。
※图层边框
CALayer另外两个非常有用属性就是borderWidth和borderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。
borderWidth是以点为单位的定义边框粗细的浮点数,默认为0,borderColor定义了边框的颜色,默认为黑色。
borderColor是CGColorRef类型,而不是UIColor。所以它不是Cocoa的内置对象。
※阴影
iOS的另一个常见特性就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候他们只是单纯的装饰目的。
shadowOpactiy(控制阴影透明度)属性一个大于默认值(也就是0)的值(值必须在0.0和1.0之间的浮点数),阴影就可以显示在任意图层之下。
shadowColor(控制阴影颜色)默认是黑色,它的类型是CGColorRef。
shadowOffset(控制阴影的方向和距离)它是一个CGSize的值,宽度控制着阴影横向的位移,高度控制着纵向位移。shadowOffset的默认值是{0,-3}表示相对于Y轴有3个点的向上位移。
shadowRadius(控制阴影的模糊度),当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候边界线看上去就会越来越模糊和自然。苹果自家的应用设计更偏向于自然的阴影,所以一个非零值再合适不过了
※阴影裁剪
图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。为了计算出阴影的形状,CoreAnimation会将寄宿图(包括子视图)考虑在内,然后通过这些完美搭配图层形状而创建一个阴影
※shadowPath属性
shadowPath是一个CGPathRef类型,可以通过这个属性单独于图层形状之外指定阴影的形状。
※图层蒙版
CALayer有一个属性叫做mask,就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他则会被抛弃,如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。CALayer蒙版图层不局限与静态图,任何有图层构成的都可以作为msk属性,这意味着你的蒙版可以通过代码甚至是动画实时生成。下面是把图片和蒙版图层作用在一起的效果:
//示例
※拉伸过滤
当视图显示一个图片的时候,都应该正确的显示这个图片(意既:以正确的比例和正确的1:1像素显示在屏幕上)。原因如下:能够更好的显示最好的画质,像素既没有被压缩,也没有被拉伸;能更好的使用内存,因为这就是所有你要存储的东西;最好的性能表现,CPU不需要为此额外的计算。
不过有时候,显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图,再比如说一个可以被拖拽和伸缩的大图。当图片需要显示不同大小的时候,有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。
minification(缩小图片)和magnification(放大图片)默认过滤器都是kCAFiliterLinear(双线性滤波算法)通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸图。但是当放大倍数比较大的时候图片就模糊不清了。
kCAFilterTrilinear(三线性滤波算法)和kCAFiliterLinear非常相似,大部分情况下二者都看不出来有什么差别。但是三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同事结合大图和小图的存储进而得到最后的结果。这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题。下面对于大图来说,双线性滤波和三线性滤波表现的更出色:
kCAFilterNearest(最近过滤)是一种比较武断的方法,就是取最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊,但是最明显的效果就是会使压缩图片更糟,图片放大之后也显得块状或是马赛克严重。下面对于没有斜线的小图来说,最近过滤算法要好很多:
总的来说,线性过滤保留了形状,最近过滤则保留了像素的差异。
※组透明
UIView有一个叫做alpha的属性来确定视图的透明度。CALayer有一个等同的属性叫做opacity,这两个属性都是影响子层级的。当你显示一个50%透明度的图层时,图层的每个像素都会一般显示自己的颜色,另一半显示图层下面的颜色,这是正常的透明度表现。但是如果图层包含一个同样的50%透明的子图层时,这时所看到视图50%来自子视图,25%来自图层本身的颜色,另外的25%则来自背景色。
一般情况下,设置了一个图层的透明度,会希望它包含的整个图层树像一个整体一样的透明效果,可以通过设置CALayer的shouldRasterize属性来实现组透明的效果,如果它被设置为YES,在应用透明度自前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了。为了启用shouldRasterize属性,设置了图层的rasterizationScale属性,默认情况下,所有图层拉伸都是1.0,需要确保设置了rasterizationScale属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。
//实例
//ClockView.m
//图片
//VisualEffectViewController.m
运行效果
*变换
※仿射变换
UIView的transform属性是一个CGAffineTransform类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform是一个可以和二维空间向量(例如CGPoint)做乘法的3x2的矩阵。用CGPoint的每一列和CGAffineTransform矩阵的每一行对应元素相乘再求和,就形成了一个新的CGPoint类型的结果。要解释一下图中显示的灰色元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。因此,通常会用3×3(而不是2×3)的矩阵来做二维变换,你可能会见到3行2列格式的矩阵,这是所谓的以列为主的格式。下面是矩阵表示图:
CGAffineTransform中“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,下面是展示图:
※创建一个CGAffineTransform
下面几个函数都创建了一个CGAffineTransform实例:
UIView可以通过设置transform属性做变换,实际上它只是封装了内部图层的变换,CALayer同样也有一个transform属性,但它的类型是CATransform。CALayer对应于UIView的transform属性叫做affineTransform。
//对图层旋转45度
※混合变换
CoreGraphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,比如做一个既要缩放又要旋转的变换,例如下面几个函数
初始生成一个什么都不做的变换CGAffineTransform类型的空值,矩阵论中称作单位矩阵,CoreGraphics同样也提供了一个方便的常量:
如果需要混合两个已经存在的变换矩阵,就可以使用下面方法,在两个变换矩阵的基础上创建一个新的变换:
//下面函数创建一个复杂的变换,变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。
※剪切变换
斜切变换是仿射变换的第四种类型,较于平移,旋转和缩放并不常用(这也是CoreGraphics没有提供相应函数的原因),用“倾斜”描述更加恰当
//实现一个斜切变换
※3D变换
transform属性(CATransform3D类型),可以让图层在3D空间内移动或者旋转,CATransform3D是一个可以在3纬空间内做变换的4x4的矩阵,下面是矩阵表示图:
CoreAnimation提供了一系列的方法创建和组合CATransform3D类型的矩阵,和CoreGraphics的函数类似,3D的仿射变换多了一个z参数。
X轴以右为正方形,Y轴以下为正方形,Z轴和这两个轴分别垂直,指向视角外为正方向,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。下面是X,Y,Z轴,以及围绕它们旋转的方向:
//绕Y轴旋转图层
※透视投影
在真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边更短,但实际上并没有发生,而我们当前的视角是等距离的,也就是在3D变换中任然保持平行,和之前的仿射变换类似。为了做一些修正,需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,通过CATransform3D修改矩阵值,达到透视效果通过矩阵中一个很简单的元素来控制:m34。用于按比例缩放X和Y的值来计算到底要视角多远。m34的默认值是0,可以通过设置M34为-1.0/d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了(因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它的放置的位置。通常500-1000就已经很好了,但是对于特定的图层有时候更小或者更大的值会看起来更舒服,减少距离的值会增加透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果)。
※灭点
当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们就可能缩成了一个点,于是所有的物体最后都汇聚消失在同一个点,这个点通常是视图的中心,Core Animation定义了这个点位于变换图层的anchorPoint。这就是说,当图层发生变换时,这个点永远位于图层变换之前anchorPoint的位置。当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。下图创建拟真效果的透视:
※sublayerTransform属性
如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34只,并且确保在变换之前都在屏幕中央共享一个position,有更好的方法:CALayer有一个属性叫做sublayerTransform,它是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层。表示可以一次性对包含这些图层的容器做变换,所有的子图层都自动继承了这个变换方法。
※背面
图层是双面绘制的,反面显示的是正面的一个镜像图片,但这并不是一个很好的特性,因为如果图层包含文本或者其他控件,那用户看到这些内容的镜像图片当然会感到困惑,另外也有可能造成资源的浪费(想象用这些图层形成一个不透明的固态立方体,既然永远都看不见这些图层的背面,那为什么浪费GPU来绘制它们呢?)。
CALayer有一个叫做doubleSided的属性来控制图层背面是否要被绘制。这是一个BOOL类型,默认为YES,如果设置为NO,那么当图层正面从相机视角消失的时候,它将不会被绘制。
※扁平化图层
对包含已经做过变换的图层的图层做反方向的变换:二维图层会恢复原状,3D变换不会恢复原状()。
※固体对象
可以用3D空间创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现),这里用六个独立的视图来构建一个立方体的各个面。
※光亮和阴影
CoreAnimation可以用3D显示图层,但是它对光线并没有概念。如果需要动态的创建光线效果,可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但为了计算阴影图层的不透明度,你需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量叉乘结果。叉乘代表了光源和图层之间的角度,从而决定了它有多大程度上的光亮。
这里用GLKit框架来做向量的计算,每个面的CATransform3D都被装换成GLMatrix4,然后通过GLMatrix4GetMatrix3函数得出一个3x3的旋转矩阵,这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。
※点击事件
这里点击第三个表面顶部看见的按钮,什么都没有发生,因为点击事件的处理有视图在父视图中的顺序决定的,并不是3D空间中Z轴顺序,所以这里按照视图/图层顺序来说,4,5,6在3的前面这就和普通的2D布局在按钮上覆盖物体一样。你也许认为把doubleSided设置成NO可以解决这个问题,因为它不再渲染视图后面的内容,但实际上并不起作用。因为背对相机而隐藏的视图仍然会响应点击事件(这和通过设置hidden属性或者设置alpha为0而隐藏的视图不同,那两种方式将不会响应事件)。所以即使禁止了双面渲染仍然不能解决这个问题(虽然由于性能问题,还是需要把它设置成NO)。这里有几种正确的方案:把除了表面3的其他视图userInteractionEnabled属性都设置成NO来禁止事件传递。或者简单通过代码把视图3覆盖在视图6上。无论怎样都可以点击按钮了
//示例
运行效果
*专有图层
※CAShapeLayer
CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就会自动渲染出来了。也可以用CoreGraphics直接向原始的CALayer的内容中绘制一个路径,相比之下使用CAShapeLayer有几个有点:渲染快速,CAShapeLayer使用了硬件加速,绘制同一图形会比用CoreGraphics快很多;高效使用内存,一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多内存;不会被图层边界剪裁掉,一个CAShapeLayer可以在边界之外绘制,图层路径不会像在使用CoreGraphics的普通CALayer一样被剪裁掉;不会出现像素化,给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。
※创建一个CGPath
CAShapeLayer可以用来绘制所有能够通过CGPath来表示的形状。下面使用UIBeizerPath帮助类创建图层路径,这样不用考虑人工释放CGPath。
//示例
运行效果
※圆角
使用UIBezierPath单独制定每个角,绘制圆角矩形
//示例
运行效果
※CATextLayer
CALayer的子类CATextLayer,它以图层的形式包含了UILabel几乎所有的绘制特性,并且额外提供了一些新的特性,CATextLayer(使用Core Text)也要比UILabel(通过WebKit实现绘制)渲染得快得多,
※富文本
CATextLayer支持属性化字符串,使用NSAttributedString,NSTextArributeName实例来设置字符串属性
//示例
运行效果
※行距和字距
由于绘制的实现机制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和子距也不是不尽相同的。二者的差异程度(由使用的字体和字符决定)总的来说挺小的。
※UILabel的替代品
CATextLayer比UILabel有着更好的性能表现,可以继承UILabel,用CATextLayer作为宿主图层的UILabel子类重写+layerClass方法,这样可以随着视图自动调整大小,而且也没有冗余的寄宿图了,把CATextLayer作为宿主图层的另一个好处就是视图自动设置了contentsScale属性。下面是CATextLayer的UILabel子类(可以在一些线上的开源项目中找到):
//LayerLabel.m文件
//使用
运行效果
※CATransformLayer
CATransformLayer不同于普通的CALayer,因为它不能显示它自己的内容,只有当存在了一个能作用于子视图的变换它才真正存在。CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构
//实例
运行效果
※CAGradientLayer
CAGrdientLayer是用来生成两种或更多颜色平滑渐变的。用CoreGraphics赋值一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。
※基础渐变
将从一个简单的红变蓝的对角线渐变开始。这些渐变色彩放在一个数组中,并赋给colors属性。这个数组成员接受CGColorRef类型的值(并不是从NSObject派生而来),所以要用通过bridge转换以确保编译正常。CAGradientLayer也有startPoint和endPoint属性,它们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标是{1, 1}。
※多重渐变
colors属性可以包含很多颜色,默认情况下,这些颜色在空间上均匀的被渲染,但是可以用locations属性来调整空间,locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个不同颜色的为准,也是以单位坐标系进行标定。0.0代表渐变的开始,1.0代表结束。locations数组并不是强制要求的,但是给它赋值了就一定要确保locations的数组大小和colors数组大小一定要相同,否则将会得到一个空白的渐变。
//示例
运行效果
※CAReplicatorLayer
CAReplicationLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换
※重复图层(Repeating Layers)
在屏幕的中间创建了一个小白色方块图层,然后用CAReplicatorLayer生成十个图层组成一个圆圈。instanceCount属性指定了图层需要重复多少次。instanceTransform指定了一个CATransform3D变换(这种情况下,下一图层的位移和旋转将会移动到圆圈的下一个点)。变换是逐步增加的,每个实例都是相对于前一实例布局。这就是为什么这些复制体最终不会出现在同一位置上。可以用instanceBlueOffset和instanceGreenOffset属性实现颜色变化,通过逐步减少蓝色和绿色通道,组件将图层颜色转换成了红色
//示例
运行效果
※反射
使用CAReplicatorLaye并应用一个负比例变换于一个图层图层,就可以创建指定视图(或整个视图层次)内容的镜像图片,这样就创建了一个实时的反射效果。指定一个继承于UIVie的ReflectionView,它会自动产生内容的反射效果。
开源代码ReflectionView完成了一个自适应的渐变淡出效果(用CAGradientLayer和图层蒙板实现)
//示例
//ReflectionView.m文件
运行效果
※CAScrollLayer
CAScrollLayer有一个-scrollToPoint:方法,它自动适应bounds的原点以便图层出现在滑动的地方。CAScrollLayer并没有等同于UIScrollView中contentSize的属性,所以当CAScrollLayer滑动的时候完全没有一个全局的可滑动区域的概念,也无法自适应它的边界原点至你指定的值。它子所以不能自适应边界大小是因为它不需要,内容完全可以超过边界来实现滑动。那么CAScrollLayer的意义到底何在,因为可以简单地用一个普通的CALayer然后手动适应边界原点,其实UIScrollView并没有用CAScrollLayer,是简单的通过直接操作图层边界来实现滑动。
※CATiedLayer
当你需要绘制一个很大的图片像是一个高像素的照片或者地球表面的详细地图。由于iOS应用通常运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。载入大图可能会相当的慢。能高效绘制在iOS上的图片也有一个大小限制,所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048x2048,或4096x4096,这个取决于设备型号)。
CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。
※小片裁剪
用CATiledLayer将图片裁剪成许多小一些的图片,但是不要在运行时读入整个图片并裁剪,那CATiledLayer的所以性能有点就损失殆尽了。
※Retina小图
上面裁剪的小图并不是以Retina的分辨率显示的,为了以屏幕的原生分辨率来渲染CATiledLayer,需要设置图层的contentsScale来匹配UIScreen的scale属性,增大了contentsScale就自动有了默认的小图尺寸(
※CAEmitterLayer
CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画如:烟雾,火,雨等等这些效果。它看上线像是许多CAEmitterCell的容器,这些CAEmitterCell定义了一个粒子效果。为不同的粒子效果定义一个或多个CAEmitterCell作为模板,同时CAEmitterLayer负责基于这些模板实例化一个粒子流。一个CAEmitterCell类似于一个CALayer:它有一个contents属性可以定义为一个CGImage,另外还有一些可以设置属性控制着表现和行为。
//示例
运行效果
※CAEAGLLayer
在iOS 5中,苹果引入了一个新的框架叫做GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做CLKView的UIView的子类,处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层可配置项任然需要你用CAEAGLLayer完成,它是CALayer的一个子类,用了显示任意的OpenGL图形。
//示例
运行效果
※AVPlayerLayer
AVPlayerLayer是由AVFoundation框架来提供的,它和CoreAnimation紧密地结合在一起,提供了一个CALayer的子类来显示自定义的内容类型。AVPlayerLayer是用来在iOS上播放视频的。它是高级接口例如MPMoviePlayer的底层实现,提供了显示视频的底层控制。
//示例
运行效果
*隐式动画
※事务
CoreAnimation基于一个假设:屏幕上任何东西都可以(或者可能)做动画。所以并不需要在CoreAnimation中手动打开动画,但是你需要明确的关闭它,否则它会一直存在。
当你改变CALayer一个可做动画的属性时,这个改变并不会立刻在屏幕上体现出来。相反,该属性会从先前的值平滑过渡到新的值,而不是跳变。这一切都是默认的行为,你不需要做额外的操作。这就是所谓的隐式动画。之所以叫做隐式是因为我们并没有指定任何动画的类型。仅仅改变了一个属性,然后CoreAnimation来决定如何并且何时去做动画。
CoreAnimation通过事务用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发送变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,二手管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。而是用类方法+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取时长值(默认0.25秒)。CoreAnimation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理未完成的定时器或者网络事件,最终重新绘制屏幕的东西),即使你不显式的使用[CATransaction begin]开始一次事务,在一个特定run loop循环中的任何属性的变化都会被收集起来,然后做一次0。25秒的动画。明白这些之后,我们就可以轻松修改变色动画的时间了。我们当然可以用当前事务的+setAnimationDuration:方法来修改动画时间,但在这里我们首先起一个新的事务,于是修改时间就不会有别的副作用。因为修改当前事务的时间可能会导致同一时刻别的动画(如屏幕旋转),所以最好还是在调整动画之前压入一个新的事务。
※完成块
基于UIView的block的动画允许在动画结束的时候提供一个完成的动作。CATransaction接口提供的+setCompletionBlock:方法也有同样的功能。
※图层的行为
Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。
我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:
- 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
- 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
- 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
- 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。当属性在动画块之外发生改变,UIView直接通过返回nil来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性。
当然返回nil并不是禁用隐式动画唯一的办法,CATransaction有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]之后添加下面的代码,同样也会阻止动画的发生:
[CATransaction setDisableActions:YES];
总结一下,我们知道了如下几点
- UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第八章)。
- 对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。
※呈现与模型
CALayer的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。改变一个图层的属性,属性值的确是立刻更新的(如果读取它的数据,会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。
当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。是一个典型的微型MVC模式。CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。
在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
前面提到除了图层树,另外还有呈现树。呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。
你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。
//示例
运行效果
*显示动画
※属性动画
属性动画作用于图层的某个单一属性,并指定了它的一个目标值,或者一连串将要做动画的值。属性动画分为两种:基础和关键帧。
※基础动画
动画其实就是一段时间内发生的改变,最简单的形式就是从一个值改变到另一个值,这也是CABasicAnimation最主要的功能。CABasicAnimation是CAPropertyAnimation的一个子类,而CAPropertyAnimation的父类是CAAnimation,CAAnimation同时也是Core Animation所有动画类型的抽象基类。
作为一个抽象类,CAAnimation本身并没有做多少工作,它提供了一个计时函数(见“缓冲”),一个委托(用于反馈动画状态)以及一个removedOnCompletion,用于标识动画是否该在结束后自动释放(默认YES,为了防止内存泄露)。CAAnimation同时实现了一些协议,包括CAAction(允许CAAnimation的子类可以提供图层行为),以及CAMediaTiming(“图层时间”将会详细解释)。
CAPropertyAnimation通过指定动画的keyPath作用于一个单一属性,CAAnimation通常应用于一个指定的CALayer,于是这里指的也就是一个图层的keyPath了。实际上它是一个关键路径(一些用点表示法可以在层级关系中指向任意嵌套的对象),而不仅仅是一个属性的名称,因为这意味着动画不仅可以作用于图层本身的属性,而且还包含了它的子成员的属性,甚至是一些虚拟的属性(后面会详细解释)。
CABasicAnimation继承于CAPropertyAnimation,并添加了如下属性:
id fromValue (代表了动画开始之前属性的值)
id toValue (代表了动画结束之后的值)
id byValue (代表了动画执行过程中改变的值)
fromValue,toValue和byValue属性可以用很多种方式来组合,但为了防止冲突,不能一次性同时指定这三个值。例如,如果指定了fromValue等于2,toValue等于4,byValue等于3,那么Core Animation就不知道结果到底是4(toValue)还是5(fromValue + byValue)了。总的说来,就是只需要指定toValue或者byValue,剩下的值都可以通过上下文自动计算出来。
※CAAnimationDelegate
CAAnimationDelegate协议的delegate可以知道一个显式动画在何时结束,可以再CAAnimation头文件或者苹果开发者文档中找到相关函数,下面用animationDidStop:finished:方法在动画结束之后来更新图层的backgroundColor(当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的CABasicAnimation,另一次是因为隐式动画)。对CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,无法在在回调方法中区分。在一个视图控制器中创建动画的时候,通常会用控制器本身作为一个委托,但是所有的动画都会调用同一个回调方法,所以你就需要判断到底是那个图层的调用。
//示例
运行效果
※关键帧动画
CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确很有趣,但是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。
CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation类似,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)将由熟练的艺术家来完成。CAKeyframeAnimation也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插值。
CAKeyframeAnimation并不能自动把当前值作为第一帧(就像CABasicAnimation那样把fromValue设为nil)。动画会在开始的时候突然跳转到第一帧的值,然后在动画结束的时候突然恢复到原始的值。所以为了动画的平滑特性,我们需要开始和结束的关键帧来匹配当前属性的值。
※虚拟属性
如果想要对一个物体做旋转的动画,那就需要作用于transform属性,因为CALayer没有显式提供角度或者方向之类的属性,为了旋转图层,我们可以对transform.rotation关键路径应用动画,而不是transform本身。transform.rotation属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。
※动画组
CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。
//示例
运行效果
※过渡
有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。
于是就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个type和subtype来标识变换效果。
type属性是一个NSString类型,可以被设置成如下类型:
kCATransitionFade 平滑的淡入淡出效果
kCATransitionMoveIn 从顶部滑动进入,但不像推送动画那样把老图层推走
kCATransitionPush 把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。
kCATransitionReveal 它创建了一个新的图层,从边缘的一侧滑动进来,把旧图层从另一侧推出去的效果
subtype来控制它们的方向,提供了如下四种类型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
※隐式过渡
CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。
※对图层树的动画
CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。这些例子和我们之前的情况完全不同,因为它们不仅涉及到图层的属性,而且是整个图层树的改变--在这种动画的过程中手动在层级关系中添加或者移除图层。要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,只需要将动画添加到被影响图层的superlayer。
※自定义动画
通过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同。UIView过渡方法中options参数可以由如下常量指定:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve
UIViewAnimationOptionTransitionFlipFromTop
UIViewAnimationOptionTransitionFlipFromBottom
除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition类型完全没关系。
CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。
※在动画过程中取消动画
可以用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有动画:
- (void)removeAllAnimations;
*图层时间
※CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。
※持续和重复
duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。
repeatCount,代表动画重复的迭代次数。如果duration是2,repeatCount设为3.5(三个半迭代),那么完整的动画时长将是7秒。
duration和repeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次。
创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。
autoreverses的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它
//示例
运行效果
※相对时间
每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。
beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。
speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。
timeOffset和beginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。
和beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画中间的地方开始,虽然动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。
//示例
运行效果
※fillMode
对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?
一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。
另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。
这种行为就交给开发者了,它可以被CAMediaTiming的fillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值。剩下的三种类型分别为向前,向后或者既向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。
这就对避免在动画结束的时候急速返回提供另一种方案。但是当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。
※层级关系时间
每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用CAAnimationGroup实例)。
对CALayer或者CAAnimationGroup调整duration和repeatCount/repeatDuration属性并不会影响到子动画。但是beginTime,timeOffset和speed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayer和CAGroupAnimation的speed属性将会对动画以及子动画速度应用一个缩放的因子。
※全局时间和本地时间
CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用CACurrentMediaTime函数来访问马赫时间:
CFTimeInterval time = CACurrentMediaTime();
这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。
因此马赫时间对长时间测量并不有用。比如用CACurrentMediaTime去更新一个实时闹钟并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。
每个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:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。
如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。
一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
※手动动画
timeOffset一个很有用的功能在于它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。因为在动画添加到图层之后不能再做修改了,我们来通过调整layer的timeOffset达到同样的效果(清单9.4)。
//例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。
运行效果,手平滑控制
*缓冲
※动画速度
动画实际上就是一段时间内的变换,这就暗示了变化一定是随着某个特定的速率进行。速率计算公式:velocity=change/time
这里的变化可以指的是一个物体移动的距离,时间指动画持续的时长,用这样的一个移动可以更加形象的描述(比如position和bounds属性的动画),但实际上它应用于任意可以做动画的属性(比如color和opacity)。
上面的等式假设了速度在整个动画过程中都是恒定不变的,对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。
考虑一个场景,一辆车行驶在一定距离内,它会慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下来。那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。
现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,Core Animation内嵌了一系列标准函数提供给我们使用。
※CAMediaTimingFunction
使用缓冲方程式,首先需要设置CAAnimation的timingFunction属性,是CAMediaTimingFunction类的一个对象。如果想改变隐式动画的计时函数,同样可可以使用CATransaction的+setAnimationTimingFunction:方法。
创建CAMediaTimingFunction,最简单的方式是调用+timingFunctionWithName:的构造方法。这里传入如下几个常量之一:
kCAMediaTimingFunctionLinear 创建一个线性的计时函数,同样也是CAAnimation的timingFunction属性为空时候的默认函数
kCAMediaTimingFunctionEaseIn 创建一个慢慢加速然后突然停止的方法
kCAMediaTimingFunctionEaseOut 以一个全速开始,然后慢慢减速停止
kCAMediaTimingFunctionEaseInEaseOut 创建了一个慢慢加速然后在慢慢减速的过程
kCAMediaTimingFunctionDefault 它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。
//示例
※UIView的动画缓冲
UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
它们和CAMediaTimingFunction紧密关联,UIViewAnimationOptionCurveEaseInOut是默认值(这里没有kCAMediaTimingFunctionDefault相对应的值了)。
//示例
运行效果
※缓冲和关键帧动画
CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步奏指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。
//示例
运行效果
※自定义缓冲函数
CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。使用这个方法,我们可以创建一个自定义的缓冲函数。
※三次贝塞尔曲线
CAMediaTimingFunction有一个叫做-getControlPointAtIndex:values:的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为什么不简单返回一个CGPoint),但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPath和CAShapeLayer来把它画出来。
※更加复杂的动画曲线
当一个橡胶球掉落到坚硬的地面的场景,当开始下了落的时候,它会持续加速直到落到地面,然后经过几次反弹,最后停下来,如下图;
这种效果没法用一个简单的三次贝塞尔曲线表示,于是不能用CAMediaTimingFunction来完成。但如果想要实现这样的效果,可以用如下几种方法:
- 用CAKeyframeAnimation创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数(具体下节介绍)。
- 使用定时器逐帧更新实现动画
※基于关键帧的缓冲
为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。
※流程自动化
为了实现自动化,我们需要知道如何做如下两件事情:
* 自动把任意属性动画分割成多个关键帧
* 用一个数学函数表示弹性动画,使得可以对帧做遍历
为了解决第一个问题,需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1):
value = (endValue – startValue) × time + startValue;
那么如果要插入一个类似于CGPoint,CGColorRef或者CATransform3D这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就CGPoint中的x和y值,CGColorRef中的红,蓝,绿,透明值,或者是CATransform3D中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。
一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。
注意用到了60 x 动画时间(秒做单位)作为关键帧的个数,这时因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果每秒生成60个关键帧,就可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效果)。
在示例中仅仅引入了对CGPoint类型的插值代码。但是,从代码中很清楚能看出如何扩展成支持别的类型。作为不能识别类型的备选方案,仅仅在前一半返回了fromValue,在后一半返回了toValue。
//示例,使用关键帧实现反弹球的动画
运行效果
*基于定时器的动画
※定时帧
动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展示像素的时候并不能做到这一点。一般来说这种显示都无法做到连续的移动,能做的仅仅是足够快地展示一系列静态图片,只是看起来像是做了运动。
之前提到过iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。
所有的Core Animation实际上都是按照一定的序列来显示这些帧,那么可以自己做到这些么?
※NSTimer
用NSTimer来做定时动画,一秒钟更新一次,但是如果我们把频率调整成一秒钟更新60次的话,原理是完全相同的。
用NSTimer来修改”缓冲”章中弹性球的例子。由于现在在定时器启动之后连续计算动画帧,需要在类中添加一些额外的属性来存储动画的fromValue,toValue,duration和当前的timeOffset
NSTimer并不是最佳方案,为了理解这点,我们需要确切地知道NSTimer是如何工作的。iOS上的每个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:
处理触摸事件
发送和接受网络数据包
执行使用gcd的代码
处理计时器行为
屏幕重绘
当设置一个NSTimer,它会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。
屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。
可以通过一些途径来优化:
我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
基于真实帧的持续时间而不是假设的更新频率来做动画。
调整动画计时器的run loop模式,这样就不会被别的事件干扰。
※CADisplayLink
CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。
用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。
※计算帧的持续时间
无论是使用NSTimer还是CADisplayLink,仍然需要处理一帧的时间超出了预期的六十分之一秒。由于不能够计算出一帧真实的持续时间,所以需要手动测量。可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。
通过比较这些时间,就可以得到真实的每帧持续的时间,然后代替硬编码的六十分之一秒。
*Run Loop模式
注意到当创建CADisplayLink的时候,需要指定一个run loop和run loop mode,对于run loop来说,就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。
一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动,一些常见的run loop模式如下:
* NSDefaultRunLoopMode - 标准优先级
* NSRunLoopCommonModes - 高优先级
* UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画英雄联盟
在例子中,是用了NSDefaultRunLoopMode,但是不能保证动画平滑的运行,所以就可以用NSRunLoopCommonModes来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。
同样可以同时对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];
※物理模拟
即使使用了基于定时器的动画来复制“缓存”章关键帧的行为,但还是会有一些本质上的区别:在关键帧的实现中,提前计算了所有帧,但是在新的解决方案中,实际上实在按需要在计算。意义在于可以根据用户输入实时修改动画的逻辑,或者和别的实时动画系统例如物理引擎进行整合。
※Chipmunk
基于物理学创建一个真实的重力模拟效果来取代当前基于缓冲的弹性动画,但即使模拟2D的物理效果就已近极其复杂了,所以就不要尝试去实现它了,直接用开源的物理引擎库好了。
将要使用的物理引擎叫做Chipmunk。另外的2D物理引擎也同样可以(例如Box2D),但是Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合。Chipmunk有很多版本,包括一个和Objective-C绑定的“indie”版本。C语言的版本是免费的,所以就用它好了。
//示例
运行效果
*性能调优
※CPU VS GPU
关于绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器)。在现代iOS设备中,都有可以运行不同软件的可编程芯片,但是由于历史原因,可以说CPU所做的工作都在软件层面,而GPU在硬件层面。
总的来说,可以用软件(使用CPU)做任何事情,但是对于图像处理,通常用硬件会更快,因为GPU使用图像对高度并行浮点运算做了优化。由于某些原因,想尽可能把屏幕渲染的工作交给硬件去处理。问题在于GPU并没有无限制处理性能,而且一旦资源用完的话,性能就会开始下降了(即使CPU并没有完全占用)
大多数动画性能优化都是关于智能利用GPU和CPU,使得它们都不会超出负荷。于是首先需要知道Core Animation是如何在这两个处理器之间分配工作的。
※动画的舞台
Core Animation处在iOS的核心地位:应用内和应用间都会用到它。一个简单的动画可能同步显示多个app的内容,例如当在iPad上多个程序之间使用手势切换,会使得多个程序同时显示在屏幕上。在一个特定的应用中用代码实现它是没有意义的,因为在iOS中不可能实现这种效果(App都是被沙箱管理,不能访问别的视图)。
动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。在iOS5和之前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做BackBoard。
当运行一段动画时候,这个过程会被四个分离的阶段被打破:
* 布局 - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
* 显示 - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:和-drawLayer:inContext:方法的调用路径。
* 准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
* 提交 - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
但是这些仅仅阶段仅仅发生在你的应用程序之内,在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,它们会被反序列化来形成另一个叫做渲染树的图层树(在“图层树”章中提到过)。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:
* 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
* 在屏幕上渲染可见的三角形
所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。
这并不是个问题,因为在布局和显示阶段,你可以决定哪些由CPU执行,哪些交给GPU去做。那么改如何判断呢?
※GPU的相关操作
GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们输送到屏幕上。现代iOS设备上可编程的GPU在这些操作的执行上又很大的灵活性,但是Core Animation并没有暴露出直接的接口。除非你想绕开Core Animation并编写你自己的OpenGL着色器,从根本上解决硬件加速的问题,那么剩下的所有都还是需要在CPU的软件层面上完成。
宽泛的说,大多数CALayer的属性都是用GPU来绘制。比如如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个contents属性设置一张图片,然后裁剪它 - 它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。
但是有一些事情会降低(基于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相关的操作
大多数工作在Core Animation的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通常直到真正绘制的时候才去解码图片(“图片IO”章会更详细讨论)。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView)或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。
当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU每一帧的渲染,即使这些事情不是你的应用程序可控的。
※IO相关操作
还有一项没涉及的就是IO相关工作。上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能需要从闪存(甚至是远程URL)来加载。一个典型的例子就是两个视图控制器之间的过渡效果,这就需要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大,需要动态滚动来加载。
IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。
※测量而不是猜测
于是现在你知道有哪些点可能会影响动画性能,那该如何修复呢?好吧,其实不需要。有很多种诡计来优化动画,但如果盲目使用的话,可能会造成更多性能上的问题,而不是修复。
如何正确的测量而不是猜测这点很重要。根据性能相关的知识写出代码不同于仓促的优化。前者很好,后者实际上就是在浪费时间。
那该如何测量呢?第一步就是确保在真实环境下测试你的程序。
※真机测试,而不是模拟器
当你开始做一些性能方面的工作时,一定要在真机上测试,而不是模拟器。模拟器虽然是加快开发效率的一把利器,但它不能提供准确的真机性能参数。
模拟器运行在你的Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候。
这就是说在模拟器上的测试出的性能会高度失真。如果动画在模拟器上运行流畅,可能在真机上十分糟糕。如果在模拟器上运行的很卡,也可能在真机上很平滑。你无法确定。
另一件重要的事情就是性能测试一定要用发布配置,而不是调试模式。因为当用发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。你也可以自己做到这些,例如在发布环境禁用NSLog语句。你只关心发布性能,那才是你需要测试的点。
最后,最好在你支持的设备中性能最差的设备上测试:如果基于iOS6开发,这意味着最好在iPhone 3GS或者iPad2上测试。如果可能的话,测试不同的设备和iOS版本,因为苹果在不同的iOS版本和设备中做了一些改变,这也可能影响到一些性能。例如iPad3明显要在动画渲染上比iPad2慢很多,因为渲染4倍多的像素点(为了支持视网膜显示)。
※保持一致的帧率
为了做到动画的平滑,你需要以60FPS(帧每秒)的速度运行,以同步屏幕刷新速率。通过基于NSTimer或者CADisplayLink的动画你可以降低到30FPS,而且效果还不错,但是没办法通过Core Animation做到这点。如果不保持60FPS的速率,就可能随机丢帧,影响到体验。
你可以在使用的过程中明显感到有没有丢帧,但没办法通过肉眼来得到具体的数据,也没法知道你的做法有没有真的提高性能。你需要的是一系列精确的数据。
你可以在程序中用CADisplayLink来测量帧率(“基于定时器的动画”章中那样),然后在屏幕上显示出来,但应用内的FPS显示并不能够完全真实测量出Core Animation性能,因为它仅仅测出应用内的帧率。我们知道很多动画都在应用之外发生(在渲染服务器进程中处理),但同时应用内FPS计数的确可以对某些性能问题提供参考,一旦找出一个问题的地方,你就需要得到更多精确详细的数据来定位到问题所在。苹果提供了一个强大的Instruments工具集来帮我们做到这些。
※Instruments
Instruments是Xcode用来分析程序性能,分析消耗,Instruments的一个很棒的功能在于它可以创建自定义的工具集(在Xcode->Open Developer Tool->Instruments打开)。除了你初始选择的工具之外,如果在Instruments中打开右上角+号可以添加其他工具分析
这里说下面几个工具
※时间分析器
时间分析器工具用了检测CPU的使用情况。它可以告诉我们程序中的哪个方法正在消耗大量的CPU时间。使用大量的CPU并不一定是个问题 - 你可能期望动画路径对CPU非常依赖,因为动画往往是iOS设备中最苛刻的任务。但是如果你有性能问题,查看CPU时间对于判断性能是不是和CPU相关,以及定位到函数都很有帮助。
时间分析器有一些选项来帮助我们定位到我们关心的的方法。可以使用左侧的复选框来打开。其中最有用的是如下几点:
- 通过线程分离 - 这可以通过执行的线程进行分组。如果代码被多线程分离的话,那么就可以判断到底是哪个线程造成了问题。
- 隐藏系统库 - 可以隐藏所有苹果的框架代码,来帮助我们寻找哪一段代码造成了性能瓶颈。由于我们不能优化框架方法,所以这对定位到我们能实际修复的代码很有用。
- 只显示Obj-C代码 - 隐藏除了Objective-C之外的所有代码。大多数内部的Core Animation代码都是用C或者C++函数,所以这对我们集中精力到我们代码中显式调用的方法就很有用。
※Core Animation
Core Animation工具用来监测Core Animation性能。它给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画。
Core Animation工具也提供了一系列复选框选项来帮助调试渲染瓶颈:
* Color Blended Layers - 这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。
* ColorHitsGreenandMissesRed - 当使用shouldRasterizep属性的时候,耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就意味着栅格化可能会有负面的性能影响了(更多关于使用shouldRasterize的细节见第15章“图层性能”)。
* 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或者说通过增加缓存或者使用替代方案会有提升性能的空间。
※OpenGL ES驱动
OpenGL ES驱动工具可以帮你测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animation那样显示FPS的工具。
OpenGL ES驱动工具
侧栏的右边是一系列有用的工具。其中和Core Animation性能最相关的是如下几点:
* Renderer Utilization - 如果这个值超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。
* Tiler Utilization - 如果这个值超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。
*高效绘图
※软件绘图
术语绘图通常在Core Animation的上下文中指代软件绘图(意即:不由GPU协助的绘图)。在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core Animation和OpenGL,Core Graphics要慢了不少。
软件绘图不仅效率低,还会消耗可观的内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。
但是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽*图层高*4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。
软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。
※矢量图形
用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:
任意多边形(不仅仅是一个矩形)
斜线或曲线
文本
渐变
※脏矩形
有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如绘图应用:用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。
可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样很快就会超出的。这种情况下没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。
※异步绘制
//TouchPathDrawingView.m
运行效果
*图像IO
※加载和潜伏
绘图实际消耗的时间通常并不是影响性能的因素。图片消耗很大一部分内存,而且不太可能把需要显示的图片都保留在内存中,所以需要在应用运行的时候周期性地加载和卸载图片。
图片文件的加载速度同时受到CPU及IO(输入/输出)延迟的影响。iOS设备中的闪存已经比传统硬盘快很多了,但仍然比RAM慢将近200倍左右,这就需要谨慎地管理加载,以避免延迟。
只要有可能,就应当设法在程序生命周期中不易察觉的时候加载图片,例如启动,或者在屏幕切换的过程中。按下按钮和按钮响应事件之间最大的延迟大概是200ms,远远超过动画帧切换所需要的16ms。你可以在程序首次启动的时候加载图片,但是如果20秒内无法启动程序的话,iOS检测计时器就会终止你的应用(而且如果启动时间超出2或3秒的话,用户就会抱怨)。
有些时候,提前加载所有的东西并不明智。比如说包含上千张图片的图片传送带:用户希望能够平滑快速翻动图片,所以就不可能提前预加载所有的图片;那样会消耗太多的时间和内存。
有时候图片也需要从远程网络连接中下载,这将会比从磁盘加载要消耗更多的时间,甚至可能由于连接问题而加载失败(在几秒钟尝试之后)。你不能在主线程中加载网络,并在屏幕冻结期间期望用户去等待它,所以需要后台线程。
※线程加载
在“性能调优”章的联系人列表例子中,图片都非常小,所以可以在主线程同步加载。但是对于大图来说,这样做就不太合适了,因为加载会消耗很长时间,造成滑动的不流畅。滑动动画会在主线程的run loop中更新,它们是在渲染服务进程中运行的,并因此更容易比CAAnimation遭受CPU相关的性能问题。
这里提升性能唯一的方式就是在另一个线程中加载图片。这并不能够降低实际的加载时间(可能情况会更糟,因为系统可能要消耗CPU时间来处理加载的图片数据),但是主线程能够有时间做一些别的事情,比如响应用户输入,以及滑动动画。
为了在后台线程加载图片,可以使用GCD或者NSOperationQueue创建自定义线程,或者使用CATiledLayer。为了从远程网络加载图片,可以使用异步的NSURLConnection,但是对本地存储的图片,并不十分有效。
※GCD和NSOperationQueue
GCD(Grand Central Dispatch)和NSOperationQueue很类似,都提供了队列闭包块来在线程中按一定顺序来执行。NSOperationQueue有一个Objecive-C接口(而不是使用GCD的全局C函数),同样在操作优先级和依赖关系上提供了很好的粒度控制,但是需要更多地设置代码。
※延迟解压
一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。
用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。
当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。这就会在准备绘制图片的时候影响性能,因为需要在绘制之前进行解压(通常是消耗时间的问题所在)。
最简单的方法就是使用UIImage的+imageNamed:方法避免延时加载。不像+imageWithContentsOfFile:(和其他别的UIImage加载方法),这个方法会在加载图片之后立刻进行解压(就和本章之前谈到的好处一样)。问题在于+imageNamed:只对从应用资源束中的图片有效,所以对用户生成的图片内容或者是下载的图片就没法使用了。
另一种立刻加载图片的方法就是把它设置成图层内容,或者是UIImageView的image属性。不幸的是,这又需要在主线程执行,所以不会对性能有所提升。
第三种方式就是绕过UIKit,使用ImageIO框架,这样就可以使用kCGImageSourceShouldCache来创建图片,强制图片立刻解压,然后在图片的生命周期保留解压后的版本。
最后一种方式就是使用UIKit加载图片,但是需要立刻将它绘制到CGContext中去。图片必须要在绘制之前解压,所以就要立即强制解压。这样的好处在于绘制图片可以在后台线程(例如加载本身)中执行,而不会阻塞UI。
有两种方式可以为强制解压提前渲染图片:
* 将图片的一个像素绘制成一个像素大小的CGContext。这样仍然会解压整张图片,但是绘制本身并没有消耗任何时间。这样的好处在于加载的图片并不会在特定的设备上为绘制做优化,所以可以在任何时间点绘制出来。同样iOS也就可以丢弃解压后的图片来节省内存了。
* 将整张图片绘制到CGContext中,丢弃原始的图片,并且用一个从上下文内容中新的图片来代替。这样比绘制单一像素那样需要更加复杂的计算,但是因此产生的图片将会为绘制做优化,而且由于原始压缩图片被抛弃了,iOS就不能够随时丢弃任何解压后的图片来节省内存了。
需要注意的是苹果特别推荐了不要使用这些诡计来绕过标准图片解压逻辑(所以也是他们选择用默认处理方式的原因),但是如果你使用很多大图来构建应用,那如果想提升性能,就只能和系统博弈了。
如果不使用+imageNamed:,那么把整张图片绘制到CGContext可能是最佳的方式了。尽管你可能认为多余的绘制相较别的解压技术而言性能不是很高,但是新创建的图片(在特定的设备上做过优化)可能比原始图片绘制的更快。
同样,如果想显示图片到比原始尺寸小的容器中,那么一次性在后台线程重新绘制到正确的尺寸会比每次显示的时候都做缩放会更有效(尽管在这个例子中我们加载的图片呈现正确的尺寸,所以不需要多余的优化)。
//示例
//ImageIOViewController.h文件
运行效果
※CATiledLayer
如“专用图层”章中的例子所示,CATiledLayer可以用来异步加载和显示大型图片,而不阻塞用户输入。但是同样可以使用CATiledLayer在UICollectionView中为每个表格创建分离的CATiledLayer实例加载传动器图片,每个表格仅使用一个图层。
这样使用CATiledLayer有几个潜在的弊端:
* CATiledLayer的队列和缓存算法没有暴露出来,所以只能祈祷它能匹配需求
* CATiledLayer需要每次重绘图片到CGContext中,即使它已经解压缩,而且和单元格尺寸一样(因此可以直接用作图层内容,而不需要重绘)。
需要解释几点:
* CATiledLayer的tileSize属性单位是像素,而不是点,所以为了保证瓦片和表格尺寸一致,需要乘以屏幕比例因子。
* 在-drawLayer:inContext:方法中,需要知道图层属于哪一个indexPath以加载正确的图片。这里利用了CALayer的KVC来存储和检索任意的值,将图层和索引打标签。
结果CATiledLayer工作的很好,性能问题解决了,而且和用GCD实现的代码量差不多。仅有一个问题在于图片加载到屏幕上后有一个明显的淡入。
可以调整CATiledLayer的fadeDuration属性来调整淡入的速度,或者直接将整个渐变移除,但是这并没有根本性地去除问题:在图片加载到准备绘制的时候总会有一个延迟,这将会导致滑动时候新图片的跳入。这并不是CATiledLayer的问题,使用GCD的版本也有这个问题。
即使使用上述讨论的所有加载图片和缓存的技术,有时候仍然会发现实时加载大图还是有问题。就和“高效绘图”章中提到的那样,iPad上一整个视网膜屏图片分辨率达到了2048x1536,而且会消耗12MB的RAM(未压缩)。第三代iPad的硬件并不能支持1/60秒的帧率加载,解压和显示这种图片。即使用后台线程加载来避免动画卡顿,仍然解决不了问题。可以在加载的同时显示一个占位图片,但这并没有根本解决问题,可以做到更好。
//示例
运行效果
※分辨率交换
视网膜分辨率(根据苹果营销定义)代表了人的肉眼在正常视角距离能够分辨的最小像素尺寸。但是这只能应用于静态像素。当观察一个移动图片时,你的眼睛就会对细节不敏感,于是一个低分辨率的图片和视网膜质量的图片没什么区别了。
如果需要快速加载和显示移动大图,简单的办法就是欺骗人眼,在移动传送器的时候显示一个小图(或者低分辨率),然后当停止的时候再换成大图。这意味着需要对每张图片存储两份不同分辨率的副本,但是幸运的是,由于需要同时支持Retina和非Retina设备,本来这就是普遍要做到的。
如果从远程源或者用户的相册加载没有可用的低分辨率版本图片,那就可以动态将大图绘制到较小的CGContext,然后存储到某处以备复用。
为了做到图片交换,需要利用UIScrollView的一些实现UIScrollViewDelegate协议的委托方法(和其他类似于UITableView和UICollectionView基于滚动视图的控件一样):
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
可以使用这几个方法来检测传送器是否停止滚动,然后加载高分辨率的图片。只要高分辨率图片和低分辨率图片尺寸颜色保持一致,你会很难察觉到替换的过程(确保在同一台机器使用相同的图像程序或者脚本生成这些图片)。
※缓存
如果有很多张图片要显示,提前把它们全部都加载进去是不切实际的,但是,这并不意味着,你在遇到加载问题后,当其移出屏幕时就立刻将其销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了。
缓存其实很简单:就是将昂贵计算后的结果(或者是从闪存或者网络加载的文件)存储到内存中,以便后续使用,这样访问起来很快。问题在于缓存本质上是一个权衡过程 - 为了提升性能而消耗了内存,但是由于内存是一个非常宝贵的资源,所以不能把所有东西都做缓存。
何时将何物做缓存(做多久)并不总是很明显。幸运的是,大多情况下,iOS都做好了图片的缓存。
+imageNamed:方法
之前提到使用[UIImage imageNamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[UIImage imageNamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。
对于iOS应用那些主要的图片(例如图标,按钮和背景图片),使用[UIImage imageNamed:]加载图片是最简单最有效的方式。在nib文件中引用的图片同样也是这个机制,所以你很多时候都在隐式的使用它。
但是[UIImage imageNamed:]并不适用任何情况。它为用户界面做了优化,但是并不是对应用程序需要显示的所有类型的图片都适用。有些时候你还是要实现自己的缓存机制,原因如下:
* [UIImage imageNamed:]方法仅仅适用于在应用程序资源束目录下的图片,但是大多数应用的许多图片都要从网络或者是用户的相机中获取,所以[UIImage imageNamed:]就没法用了。
* [UIImage imageNamed:]缓存用来存储应用界面的图片(按钮,背景等等)。如果对照片这种大图也用这种缓存,那么iOS系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。对传送器的图片使用一个单独的缓存机制就可以把它和应用图片的生命周期解耦。
* [UIImage imageNamed:]缓存机制并不是公开的,所以你不能很好地控制它。例如,你没法做到检测图片是否在加载之前就做了缓存,不能够设置缓存大小,当图片没用的时候也不能把它从缓存中移除。
※自定义缓存
构建一个所谓的缓存系统非常困难。菲尔 卡尔顿曾经说过:“在计算机科学中只有两件难事:缓存和命名”。
如果要写自己的图片缓存的话,那该如何实现呢?让我们来看看要涉及哪些方面:
* 选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。如果实时创建图片,通常不太好生成一个字符串来区分别的图片。在图片传送带例子中就很简单,可以用图片的文件名或者表格索引。
* 提前缓存 - 如果生成和加载数据的代价很大,你可能想当第一次需要用到的时候再去加载和缓存。提前加载的逻辑是应用内在就有的,但是在我们的例子中,这也非常好实现,因为对于一个给定的位置和滚动方向,我们就可以精确地判断出哪一张图片将会出现。
* 缓存失效 - 如果图片文件发生了变化,怎样才能通知到缓存更新呢?这是个非常困难的问题(就像菲尔 卡尔顿提到的),但是幸运的是当从程序资源加载静态图片的时候并不需要考虑这些。对用户提供的图片来说(可能会被修改或者覆盖),一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。
* 缓存回收 - 当内存不够的时候,如何判断哪些缓存需要清空呢?这就需要到你写一个合适的算法了。幸运的是,对缓存回收的问题,苹果提供了一个叫做NSCache通用的解决方案
NSCache
NSCache和NSDictionary类似。你可以通过-setObject:forKey:和-object:forKey:方法分别来插入,检索。和字典不同的是,NSCache在系统低内存的时候自动丢弃存储的对象。
NSCache用来判断何时丢弃对象的算法并没有在文档中给出,但是你可以使用-setCountLimit:方法设置缓存大小,以及-setObject:forKey:cost:来对每个存储的对象指定消耗的值来提供一些暗示。
指定消耗数值可以用来指定相对的重建成本。如果对大图指定一个大的消耗值,那么缓存就知道这些物体的存储更加昂贵,于是当有大的性能问题的时候才会丢弃这些物体。你也可以用-setTotalCostLimit:方法来指定全体缓存的尺寸。
NSCache是一个普遍的缓存解决方案,我们创建一个比传送器案例更好的自定义的缓存类。(例如,可以基于不同的缓存图片索引和当前中间索引来判断哪些图片需要首先被释放)。但是NSCache对我们当前的缓存需求来说已经足够了;没必要过早做优化。
※文件格式
图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说PNG是iOS所有图片加载的最好格式。但这是极度误导的过时信息了。
PNG图片使用的无损压缩算法可以比使用JPEG的图片做到更快地解压,但是由于闪存访问的原因,这些加载的时间并没有什么区别。
PNG和JPEG压缩算法作用于两种不同的图片类型:JPEG对于噪点大的图片效果很好;但是PNG更适合于扁平颜色,锋利的线条或者一些渐变色的图片。相对于不友好的PNG图片,相同像素的JPEG图片总是比PNG加载更快,除非一些非常小的图片、但对于友好的PNG图片,一些中大尺寸的图效果还是很好的。
但JPEG图片并不是所有情况都适用。如果图片需要一些透明效果,或者压缩之后细节损耗很多,那就该考虑用别的格式了。苹果在iOS系统中对PNG和JPEG都做了一些优化,所以普通情况下都应该用这种格式。也就是说在一些特殊的情况下才应该使用别的格式。
※混合图片
对于包含透明的图片来说,最好是使用压缩透明通道的PNG图片和压缩RGB部分的JPEG图片混合起来加载。这就对任何格式都适用了,而且无论从质量还是文件尺寸还是加载性能来说都和PNG和JPEG的图片相近。
※JPEG 2000
除了JPEG和PNG之外iOS还支持别的一些格式,例如TIFF和GIF,但是由于他们质量压缩得更厉害,性能比JPEG和PNG糟糕的多,所以大多数情况并不用考虑。
但是iOS 5之后,苹果低调添加了对JPEG 2000图片格式的支持,所以大多数人并不知道。它甚至并不被Xcode很好的支持 - JPEG 2000图片都没在Interface Builder中显示。
但是JPEG 2000图片在(设备和模拟器)运行时会有效,而且比JPEG质量更好,同样也对透明通道有很好的支持。但是JPEG 2000图片在加载和显示图片方面明显要比PNG和JPEG慢得多,所以对图片大小比运行效率更敏感的时候,使用它是一个不错的选择。
但仍然要对JPEG 2000保持关注,因为在后续iOS版本说不定就对它的性能做提升,但是在现阶段,混合图片对更小尺寸和质量的文件性能会更好。
※PVRTC
当前市场的每个iOS设备都使用了Imagination Technologies PowerVR图像芯片作为GPU。PowerVR芯片支持一种叫做PVRTC(PowerVR Texture Compression)的标准图片压缩。
和iOS上可用的大多数图片格式不同,PVRTC不用提前解压就可以被直接绘制到屏幕上。这意味着在加载图片之后不需要有解压操作,所以内存中的图片比其他图片格式大大减少了(这取决于压缩设置,大概只有1/60那么大)。
但是PVRTC仍然有一些弊端:
* 尽管加载的时候消耗了更少的RAM,PVRTC文件比JPEG要大,有时候甚至比PNG还要大(这取决于具体内容),因为压缩算法是针对于性能,而不是文件尺寸。
* PVRTC必须要是二维正方形,如果源图片不满足这些要求,那必须要在转换成PVRTC的时候强制拉伸或者填充空白空间。
* 质量并不是很好,尤其是透明图片。通常看起来更像严重压缩的JPEG文件。
* PVRTC不能用Core Graphics绘制,也不能在普通的UIImageView显示,也不能直接用作图层的内容。你必须要用作OpenGL纹理加载PVRTC图片,然后映射到一对三角形中,并在CAEAGLLayer或者GLKView中显示。
* 创建一个OpenGL纹理来绘制PVRTC图片的开销相当昂贵。除非你想把所有图片绘制到一个相同的上下文,不然这完全不能发挥PVRTC的优势。
* PVRTC使用了一个不对称的压缩算法。尽管它几乎立即解压,但是压缩过程相当漫长。在一个现代快速的桌面Mac电脑上,它甚至要消耗一分钟甚至更多来生成一个PVRTC大图。因此在iOS设备上最好不要实时生成。
如果你愿意使用OpenGL,而且即使提前生成图片也能忍受得了,那么PVRTC将会提供相对于别的可用格式来说非常高效的加载性能。比如,可以在主线程1/60秒之内加载并显示一张2048×2048的PVRTC图片(这已经足够大来填充一个视网膜屏幕的iPad了),这就避免了很多使用线程或者缓存等等复杂的技术难度。
*图层性能
※隐式绘制
寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给contents属性,或事先绘制一个屏幕之外的CGContext上下文。在之前的两章中讨论了这些场景下的优化。但是除了常见的显式创建寄宿图,你也可以通过以下三种方式创建隐式的:1,使用特性的图层属性。2,特定的视图。3,特定的图层子类。
了解这个情况为什么发生何时发生是很重要的,它能够让你避免引入不必要的软件绘制行为。
※文本
CATextLayer和UILabel都是直接将文本绘制在图层的寄宿图中。事实上这两种方式用了完全不同的渲染方式:在iOS 6及之前,UILabel用WebKit的HTML渲染引擎来绘制文本,而CATextLayer用的是Core Text.后者渲染更迅速,所以在所有需要绘制大量文本的情形下都优先使用它吧。但是这两种方法都用了软件的方式绘制,因此他们实际上要比硬件加速合成方式要慢。
不论如何,尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,但是这个图层经常改动,你就应该把文本放在一个子图层中。
※光栅化
在『视觉效果』章中提到了CALayer的shouldRasterize属性,它可以解决重叠透明图层的混合失灵问题。同样在『速度的曲调』章中,它也是作为绘制复杂图层树结构的优化方法。
启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。
当我们使用得当时,光栅化可以提供很大的性能优势(如你在『性能调优』章所见),但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。
为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。
※离屏渲染
当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:
圆角(当和maskToBounds一起使用时)
图层蒙板
阴影
屏幕外渲染和启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能。
有时候可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。
对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用CAShapeLayer,contentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。
※CAShapeLayer
cornerRadius和maskToBounds独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不需要沿着圆角裁切,这个情况下用CAShapeLayer就可以避免这个问题了。
你想要的只是圆角且沿着矩形边界裁切,同时还不希望引起性能问题。其实你可以用现成的UIBezierPath的构造器+bezierPathWithRoundedRect:cornerRadius:这样做并不会比直接用cornerRadius更快,但是它避免了性能问题。
※可伸缩图片
另一个创建圆角矩形的方法就是用一个圆形内容图片并结合『寄宿图』章提到的contensCenter属性去创建一个可伸缩图片(见清单15.2).理论上来说,这个应该比用CAShapeLayer要快,因为一个可拉伸图片只需要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都需要渲染成一个顺滑的曲线。在实际应用上,二者并没有太大的区别。
使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。
※shadowPath
在“寄宿图”章有提到shadowPath属性。如果图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),创建出一个对应形状的阴影路径就比较容易,而且Core Animation绘制这个阴影也相当简单,避免了屏幕外的图层部分的预排版需求。这对性能来说很有帮助。
如果你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话可以考虑用绘图软件预先生成一个阴影背景图。
※ 混合和过度绘制
在“性能调优”章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可能发生了。
GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:
给视图的backgroundColor属性设置一个固定的,不透明的颜色
设置opaque属性为YES
这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。
如果用到了图像,尽量避免透明除非非常必要。如果图像要显示在一个固定的背景颜色或是固定的背景图之前,你没必要相对前景移动,你只需要预填充背景图片就可以避免运行时混色了。
如果是文本的话,一个白色背景的UILabel(或者其他颜色)会比透明背景要更高效。
最后,明智地使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。
※减少图层数量
※裁切
在对图层做任何优化之前,你需要确定你不是在创建一些不可见的图层,图层在以下几种情况下回事不可见的:
* 图层在屏幕边界之外,或是在父图层边界之外。
* 完全在一个不透明图层之后。
* 完全透明
Core Animation非常擅长处理对视觉效果无意义的图层。但是经常性地,你自己的代码会比Core Animation更早地想知道一个图层是否是有用的。理想状况下,在图层对象在创建之前就想知道,以避免创建和配置不必要图层的额外工作。
※对象回收
处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;UITableView和UICollectionView都有用到,MKMapView中的动画pin码也有用到,还有其他很多例子。
对象回收的基础原则就是你需要创建一个相似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你需要一个实例时,你就从池中取出一个。当且仅当池中为空时再创建一个新的。
这样做的好处在于避免了不断创建和释放对象(相当消耗资源,因为涉及到内存的分配和销毁)而且也不必给相似实例重复赋值。
本例中,我们只有图层对象这一种类型,但是UIKit有时候用一个标识符字符串来区分存储在不同对象池中的不同的可回收对象类型。
你可能注意到当设置图层属性时我们用了一个CATransaction来抑制动画效果。在之前并不需要这样做,因为在显示之前我们给所有图层设置一次属性。但是既然图层正在被回收,禁止隐式动画就有必要了,不然当属性值改变时,图层的隐式动画就会被触发。
※Core Graphics绘制
当排除掉对屏幕显示没有任何贡献的图层或者视图之后,长远看来,你可能仍然需要减少图层的数量。例如,如果你正在使用多个UILabel或者UIImageView实例去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用-drawRect:方法绘制出那些复杂的视图层级。
这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。
你可以自己实验一下这个情况,它包含了性能和栅格化的权衡,但是意味着你可以从图层树上去掉子图层(用shouldRasterize,与完全遮挡图层相反)。
※-renderInContex:方法
用Core Graphics去绘制一个静态布局有时候会比用层级的UIView实例来得快,但是使用UIView实例要简单得多而且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不应该。
幸好,你不必这样,如果大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被创建和配置之后)。
使用CALayer的-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。
当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。
//示例
运行效果
Copyright © 2024 虎扑电竞 - 最电竞的世界 - LOL虎扑社区 版权所有.
本文仅代表作者观点,不代表虎扑电竞 - 最电竞的世界 - LOL虎扑社区。
本文系作者授权虎扑电竞发表,未经许可,不得转载。