- 每一个UIView背后都有一个CALayer
- View的布局就是layer的布局
-drawRect:
方法将图形绘制到CALayer的backing store中- Layer的属性的设置及动画的处理是借由render server完成的
- Changes总是在CA::Transaction::commit()执行时时发生的

UIView
介绍
UIView类定义了屏幕上的一个矩形区域并提供一系列接口管理这片区域的内容。在运行的时候,视图对象对这片区域内显示的内容进行渲染,并对该片区域内的交互操作进行处理。UIView类提供了基本的行为利用背景色对该片区域进行填充。更多精彩的内容你可以通过添加子视图,并实现必要的自定义绘制和事件处理来实现。UIKit framework还包含一系列的标准子类,从button到各种复杂的tables。比如:UILabel 对象在contents中绘制字符串;UIImageView对象在contents中绘制图片。
由于视图对象是与用户交互的主要方式,它包括以下多种职责:
- 绘制和动画
- 视图利用UIKit,Core Graphics和OpenGL ES技术在矩形区域内绘制内容
- 视图的一些属性可以以动画的形式进行转变
- 布局和子视图管理
- 视图中可以包括0-N个子视图
- 每一个视图都定义了自己相对于父视图的默认重新布局行为
- 视图可以定义它的子视图的大小和位置
- 事件处理
- 视图是一个事件的响应者,可以处理touch等其它形式的在UIResponder class中定义的事件
- 视图还可以通过addGestureRecognizer: 方法添加手势来处理相应的事件
drawRect
视图的绘制在需要的时候才会发生。当一个视图第一次显示或当它全部或部分内容因为布局的改变而需要现实的时候,系统会告诉视图去完成它自身内容的重绘。对于利用UIKit或Core Graphics对内容进行自定义绘制的视图来说,系统会通过调用视图的-drawRect:
方法来告诉视图进行绘制操作。在执行该方法之前系统会为你准备好图形上下文,以便于在该方法中绘制自定义内容。这将生成一个展现视图内容的静态可视化图片并最终显示在屏幕上。
图形上下文(Graphics Contents):
图形上下文用CGContext结构所表示,它代表一个Quartz 2D的绘图目标。一个图形上下文包括在目标上进行渲染图形所必须的参数和设备相关的信息,绘图目标包括应用的视窗,位图,PDF文档或打印机。一个图形上下文定义了基本的绘图属性,比如绘图颜色,裁剪区域,线条宽度,风格信息,字体信息,组合模式以及一些其他信息。Quartz提供了可以创建各种类型图形上下文的接口,以便开发者调用。
当视图的真实内容发生变化的时候,你有责任通知系统视图需要重绘。你可以通过调用视图的setNeedsDisplay或setNeedsDisplayInRect:方法来通知系统。这些方法会使得系统知道应该在下次绘制循环中更新该视图的内容。由于视图的更新是在下次绘制循环中进行的,你可以同时对多个视图发送该消息,接到消息的这些视图会在下一个绘制循环一同进行更新。
注意:
如果你不需要对视图进行自定义绘制就不要在视图的子类中写一个空的-drawRect:方法这样会造成CPU资源和内存资源的浪费。一点重载了该方法系统首先会为我们准备一个图形上下文环境,待该方法执行完之后,系统会将我们绘制的内容”拷贝”到backing store中以供使用。
CALayer
介绍
CALayer类管理着基于图像(屏幕上一切我们能看到)的内容,并能够对图像内容添加动画。layer可以用于给视图提供backing store(通常在视图需要自定义绘制的时候,即自定义-drawRect:
方法时),当然也可以在不需要显示内容的视图中被使用。layer的主要工作就是管理你提供的可视内容,同时layer也有自己的可视属性可以被设置,如backgourd color,border,shadow等。除了管理可视内容,layer还维护着可视内容的几何信息(如position,size,transform)。对于没有关联视图的layer,修改它的属性后就会以动画的形式进行动态的改变,这就是隐式动画。视图在创建的时候会默认与一个CALayer进行关联,修改该这个与视图相关联的layer的属性的时候是不会触发动画的。从某种角度来说,layer只是动画信息的一个载体,它记录了动画执行的起始值和最终值以及其它信息,CATransaction::commit将这些信息通过IPC传递给render server后就由系统完成动画的渲染。
上面有关视图的功能的介绍中,绘制和动画还有布局和子视图管理其实都是由layer来完成的,由此我们可以看出UIView区别于layer的真正的职责之一是对手机屏幕touch事件的处理,那么为什么会有这样的职责分工呢,这里主要区别于基于鼠标点击来触发事件的Mac OS,我们可以推测每一个NSView在创建的时候也会关联一个layer用来处理有关内容显示的操作,而NSView则只需专心处理鼠标的点击事件。虽然layer已经向UIView暴露了一些影响视图显示的属性,如backgourd color,frame,size等。此外它还隐藏了一些未暴露的功能:
- 阴影,圆角,带颜色的边框
- Transform 3D变换
- 非矩形范围
- 多级非线性动画
在iOS平台上,虽然UIView是对layer的简单封装,但是使用UIView并不会显著的影响性能,而且UIView还封装了许多简单易用的接口便于开发者对视图内容进行复杂的操作,基于这点苹果还是鼓励开发者使用UIView的,但是也有一些例外的情况可能更适合使用CALayer,而不是UIView:
- 开发同时可以再Mac OS上运行的跨平台应用
- 针对不同情况使用不同的CALayer子类,并且不想创建额外的UIView去封装它们
- 做一些对性能非常挑剔的工作,比如对UIView一些可以忽略不计的操作都会引起性能的显著的不同(虽然在这种情况下,我们更倾向于使用OpenGL)
contents
细心地同学可能会发现CALayer的contents的类型为id,意味着我们可以传任意类型的值给它。在iOS平台上如果传的不是CGImage,那么你将会得到一个空白的图层。事实上这是为了兼容Mac OS平台上给contents传NSImage类型,CALayer还提供了一些其他的与UIView功能上相似的属性如:contentGravity,contentsScale,maskToBounds,contentsRect,contentsCenter,等等。
之所以说contents属性比较特殊是因为它还有其他的用途,除了通过赋值CGImage来设置contents,还可以通过重载视图的-drawRect:
方法来设置contents属性,不过这个设置过程是由系统或UIKit来自动完成,当视图重载该方法后,系统在调用该方法之前会为我们准备好绘图上下文,执行完我们自定义的绘制操作后,系统会将我们所绘制的内容绘制到一个CABackingStore结构中,并将该结构赋值给contents,以便在不需要重绘视图内容是直接读取使用而不需要再去重新绘制。
Backing Store:
在执行完-drawRect:
方法后,系统将会创建一个自定义内容的“备份存储(Backing Store)”即CABackingStore结构,然后将其赋值给与该视图相关联的layer的contents属性,该结构代表了一块内存缓冲区(大小由视图的size和layer的contentsScale决定),你可以把这块区域当成是一个呈现绘图结果的画布,图形上下文中所做的操作会被绘制到该块画布上,以供使用。
之所以说backing store是在-drawRect:
方法之后创建的,是因为我们可以在视图第一次显示的时候在-drawRect:
方法打印一下视图layer的contents属性,即在-drawRect:
方法中插入以下代码:NSLog(@"%@", self.layer.contents);
我们发现输出的是null。当我们延迟一会儿再输出的时候就会发现该属性指向了一个CABackingStore的结构。
猜测:
绘制完自定义内容后为什么要多此一举的将内容备份到一个Backing Store呢?
猜测一:当不需要重绘的时候可以直接拿过来用,而不必再去重绘,以减少性能上的消耗,从这一点就可以看出,苹果是不鼓励对于自定义绘图内容进行频繁的重绘操作的。
猜测二:最终显示在屏幕中的内容并不是绘制在图形上下文中的内容,而是backing store中存储的内容。
presentationLayer and modelLayer
presentationLayer :
返回与接收者在presentation tree中相对应的layer的拷贝,该方法只有在动画发生过程中才会返回正确的值,它的各种状态值就是屏幕上呈现的layer的状态值。如果我们想捕捉在动画执行过程中的layer,会用到此方法。
modelLayer :
处于preentation tree中的layer调用此方法时,会返回与该layer相对应的model tree中的layer。这个方法只有在动画发生的工程中才会有返回值。
我们知道视图可以包含自己的子视图,以此类推,各个视图之间有组织的形成了一种树状的结构,而视图的布局及层次实际上就是与视图相关联的layer的布局及层次,例如:

上图的层次结构为:

我们暂且称这样一种层次结构为图层树(layer tree),当我们修改LayerC的frame属性之后,会自动触发动画,LayerC的子图层LayerD和LayerE也会跟着父图层做出相应的动画,动画的渲染目标的状态值是由CATrasaction通过IPC向渲染服务进程提交的,但这个所谓的目标状态值并不是单一的某个layer的属性,而是由多个图层组成的一组层级结构,我们称之为模型树(model tree),当修改LayerC的frame属性后,LayerC会作为模型树的一部分提交给渲染进程,这个模型树记录了各个layer的各个属性的目标值,渲染进程按照该模型树中layer的属性渲染出每个修改过状态值的layer在每一帧动画中所要呈现的图形,渲染完成后的层次结构称为呈现树(presentation tree),presentation layer 与model layer 分别对应呈现树与模型树中的特定layer。
隐式动画:
自动触发的动画就是所谓的隐式动画,CALayer的属性改变都会以动画的方式呈现,除非采用某种方式去禁掉,比如拥有backing layer的UIView,或直接通过setDisableActions:方法,我们在后面会讲到。
渲染服务进程(render server):
渲染服务进程是由iOS系统控制的,它与应用共用CPU资源,在iOS5和之前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做BackBoard。玩儿过越狱的同学应该熟悉这个名字,系统启动完成后会首先启动该进程进行屏幕渲染,所有的应用都可以通过事务向该进程提交动画请求,之后的工作就由该进程借助GPU来完成。
Animation
Transaction
事务(Transctions)是CoreAnimation的将多个对图层树的操作批量转换成对渲染树的原子级更新操作的实现机制。每一个对图层树的修改操作都需要一个事务的参与,任何动画都是由事务提交给渲染进程来完成的。
CoreAnimation支持两种形式的事务,显式(explicit)的事务和隐式(implicit)的事务:
显式的事务是指在修改图层树之前插入[CATransaction begin]
代码,之后插入[CATransation commit]
代码。
隐式事务是在图层树的属性被其他线程(非执行中的事务线程)修改时,CoreAnimation自动创建的。隐式的事务会在线程的下一次run-loop循环被提交。然而在没有启动runloop或runloop被阻塞的线程中,有必要利用显示的事务去对渲染树进行及时的更新,以保证动画能够流畅的运行。
猜测:
任何形式的创建动画的方式(CAAnimation及其子类的动画实现或UIView的animateWithDuration
方法),都会被转换为事务,然后提交给渲染服务进程去渲染,最终每一帧的渲染结构显示到屏幕上,就完成了一个animation。
隐式动画
Core Animation基于一个假设:屏幕上的任何东西都可以(或者可能)做动画的。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。
这就是当我仅仅修改一个layer的属性的时候,就会触发一个动画的原因,但是当我们修改一个视图的layer的属性的时候我们发现并没有触发任何动画,所以可以推测UIView肯定是采取了某种方式屏蔽调了隐式事务的执行。事实上当我们修改该一个layer的属性后,它会调用actionForKey:
方法传递属性的名称,接下来的过程在CALayer头文件中有详细说明:
- 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
- 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
- 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
- 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。基于这点我们做个实现:
1 | - (void)viewDidLoad |
我们发现控制台的输出为:
1 | $ Outside: null |
于是我们可以预言,当属性在动画块之外发生改变,UIView直接通过返回nil来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation。
显式动画
除了UIView的animateWithDuration
方法可以添加基本的动画,我们还可以利用CAAnimation的子类创建各种不同类型的动画,如图

更详细介绍参阅这篇文章
动画的执行过程
大概分为以下几个阶段(来源于WWDC-2012):
- 创建动画对象并更新视图树的布局和显示
- 准备向渲染服务线程提交打包的动画数据
- 渲染服务进程对每一帧内容进行渲染并显示在屏幕上

当运行一段动画的时候,具体过程可以分为以下几步:
- Layout - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
- Display - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的
-drawRect:
和-drawLayer:inContext:方法的调用路径。 - Prepare - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
- Commit - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
但是这些仅仅阶段仅仅发生在你的应用程序之内,在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树(在第一章“图层树”中提到过)。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:
- 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
- 在屏幕上渲染可见的三角形
所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。
Reference
https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques