iOS UIView和CALayer

前言

在 iOS 中,所有的 view 都是由一个底层的 layer 来驱动的。view 和它的 layer 之间有着紧密的联系,view 其实直接从 layer 对象中获取了绝大多数它所需要的数据。layer侧重于图形的显示,而view相当于layer的管理者。本文将从几个不同方面来比较view和layer的区别和联系。

响应事件

UIView的定义:

1
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>

CALayer的定义:

1
2
CA_CLASS_AVAILABLE (10.5, 2.0, 9.0, 2.0)
@interface CALayer : NSObject <NSCoding, CAMediaTiming>

从UIView和CALayer的定义可以看出UIView是继承于UIResponder,而CALayer是继承于NSObject。在iOS中,UIKit使用UIResponder作为响应对象,来响应系统传递过来的事件并进行处理。所以UIView可以响应事件,而CALyer则不能响应事件。

初始化和Frame

一个 Layer 的 frame 是由它的 anchorPoint,position,bounds,和 transform 共同决定的,而一个 View 的 frame 只是简单的返回 Layer的 frame,同样 View 的 center和 bounds 也是返回 Layer 的一些属性。我在另一篇文章中看到笔者自定义了view和layer做了实验来做测试,为了一探究竟我也做了类似的测试。
自定义两个类MMView和MMLayer分别集成UIView和CALyer。
在MMView中重写以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@">>>>>>>>>>> MMView init");
}
return self;
}

+ (Class)layerClass{
return [MMLayer class];
}

- (void)setFrame:(CGRect)frame{
[super setFrame:frame];
}

- (void)setCenter:(CGPoint)center{
[super setCenter:center];
}

- (void)setBounds:(CGRect)bounds{
[super setBounds:bounds];
}

在MMLayer中重写以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@">>>>>>>>>>> MMLayer init");
}
return self;
}

+ (Class)layerClass{
return [MMLayer class];
}

- (void)setFrame:(CGRect)frame{
[super setFrame:frame];
}

- (void)setPosition:(CGPoint)position{
[super setPosition:position];
}

- (void)setBounds:(CGRect)bounds{
[super setBounds:bounds];
}

在两个类的初始化方法中分别打断点然后调用,结果如下:

我们发现在创建view的时候会先调用- [MMLayer init],然后调用- [UIView _createLayerWithFrame]来创建layer。

如果在创建 View 的时候,在 Layer 和 View 中Frame 相关的所有方法中都加上断点,可以看到大致如下的调用顺序如下:

1
2
3
4
5
[MMLayer setBounds:];
[MMView setFrame:];
[MMLayer setFrame:];
[MMLayer setPosetion:];
[MMLayer setBounds:];

从调用顺序可以发现在创建的过程只有调用了 Layer 的设置尺寸和位置的然而并没有调用View 的 SetCenter 和 SetBounds 方法。

如果修改了 view的 bounds.size 或者 bounds.origin 的时候也只会调用上边 Layer的一些方法。所以我们可以做如下猜测:View 的 Center 和 Bounds 只是直接返回layer 对应的 Position 和 Bounds.

内容管理和绘制

UIView主要是对显示内容的管理,而CALayer主要是对显示的绘制

分别重写UIView的drawRect和CALayer的display方法
在MMView中重写drawRect:

1
2
3
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
}

在MMLayer中重写display:

1
2
3
- (void)display{
[super display];
}

在drawRect方法中打断点并调用得到如下结果:

可以看到 UIView 是 CALayer 的CALayerDelegate,由此可以猜测是在代理方法内部[UIView(CALayerDelegate) drawLayer:inContext]调用 UIView 的drawRect方法,从而绘制出了 UIView 的内容。

隐式动画

每个view都有一个layer,但是也有一些不依附view单独存在的layer,如CAShapelayer。它们不需要附加到 view 上就可以在屏幕上显示内容。
基本上你改变一个单独的 layer 的任何属性的时候,都会触发一个从旧的值过渡到新值的简单动画(这就是所谓的隐式动画)。然而,如果你改变的是 view 中 layer 的同一个属性,它只会从这一帧直接跳变到下一帧。尽管两种情况中都有 layer,但是当 layer 附加在 view 上时,它的默认的隐式动画的 layer 行为就不起作用了。

在 Core Animation 编程指南的 “How to Animate Layer-Backed Views” 中,对为什么会这样做出了一个解释:

UIView默认情况下禁止了layer动画,但是在 animation block 中又重新启用了它们。

是因为任何可动画的 layer 属性改变时,layer 都会寻找并运行合适的 action来实行这个改变。在 Core Animation 的专业术语中就把这样的动画统称为动作 (action,或者 CAAction)。

layer 通过向它的 delegate 发送actionForLayer:forKey: 消息来询问提供一个对应属性变化的 action。delegate 可以通过返回以下三者之一来进行响应:

  1. 它可以返回一个动作对象,这种情况下 layer 将使用这个动作。
  2. 它可以返回一个 nil, 这样 layer 就会到其他地方继续寻找。
  3. 它可以返回一个 NSNull 对象,告诉 layer 这里不需要执行一个动作,搜索也会就此停止。

当 layer 在背后支持一个 view 的时候,view 就是它的 delegate。

总结

  • 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint

  • 在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容取决于内部的 CALayer 的 display

  • CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)

  • layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer

  • 两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以

参考

详解 CALayer 和 UIView 的区别和联系