iOS UIView和CALayer是啥关系?

前言

在平时开发中,我们一般都是使用 UIView 或者其子类来构建视图,但是在设置圆角、阴影等效果的时候,会设置 UIView 中的 Layer 的属性。在如何选择 UIView 和 CALayer 之前,我们应该了解 UIView 跟 CALayer 是什么关系,它们之间是如何协同工作的。

UIView 负责响应事件,CALayer 负责绘制 UI

首先从继承关系来分析两者:UIView : UIResponderCALayer : NSObject

UIView 响应事件

UIView 继承 UIResponder,而 UIResponder 是响应者对象,实现了如下 API,所以继承自 UIResponder 的都具有响应事件的能力:

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches NS_AVAILABLE_IOS(9_1);

并且 UIView 提供了以下两个方法,来进行 iOS 中的事件的响应及传递(响应者链):

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  

CALayer 绘制 UI

CALayer 没有继承自 UIResponder,所以 CALayer 不具备响应处理事件的能力。CALayer 是 QuartzCore 中的类,是一个比较底层的用来绘制内容的类。

UIView 对 CALayer 封装属性

UIView 中持有一个 layer 对象,同时这个 layer 对象 delegate,UIView 和 CALayer 协同工作。

平时我们对 UIView 设置 frame、center、bounds 等位置信息,其实都是 UIView 对 CALayer 进一层封装,使得我们可以很方便地设置控件的位置;例如圆角、阴影等属性, UIView 就没有进一步封装,所以我们还是需要去设置 Layer 的属性来实现功能。

Frame 属性主要是依赖:bounds、anchorPoint、transform、和position。

我们这主要说一下 anchorPoint 和 position 如何影响 Frame 的:anchorPoint 锚点是相对于当前 Layer 的一个点,position 是 Layer 中 anchorPoint 锚点在 superLayer 中的点,即 position 是由 anchorPoint 来确认的。

这里有几个通用的公式:

position.x = frame.origin.x + anchorPoint.x * bounds.size.width;  position.y = frame.origin.y + anchorPoint.y * bounds.size.height;  frame.origin.x = position.x - anchorPoint.x * bounds.size.width;  frame.origin.y = position.y - anchorPoint.y * bounds.size.height;    

故有:

1、position 是 layer 中的 anchorPoint 在 superLayer 中的位置坐标。

2、单独修改 position 与 anchorPoint 中任何一个属性都不影响另一个属性。

UIView 是 CALayer 的代理

UIView 持有一个 CALayer 的属性,并且是该属性的代理,用来提供一些 CALayer 行的数据,例如动画和绘制。

//绘制相关- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; //动画相关- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;

动画相关

Layer 中很多属性都是 animatable 的,这就意味着修改这些属性会产生隐式动画。当是如果修改 UIView 主 Layer 的话,此时隐式动画会失效,因为:UIView 默认情况下禁止了 layer 动画,但是在 animation block 中又重新启用了它们。

当一个 animatable 属性变化时,Layer 会询问代理方法该如何处理这个动画,即需要在代理方法中返回合适的 CAAction 对象。

属性改变时 layer 会向 view 请求一个动作,而一般情况下 view 将返回一个 NSNull,只有当属性改变发生在动画 block 中时,view 才会返回实际的动作。

绘制相关

CALayer 在屏幕上绘制东西是因为 CALayer 内部有一个 contents (CGImage)的属性,contents 也被称为寄宿图。绘制相关的 API 如下:

- (void)displayLayer:(CALayer *)layer;- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; - (void)drawRect:(CGRect)rect

drawRect 方法实现

平时为自定义 View 添加空间或者在上下文画图都会使用到这个函数,但是如果当我们实现了这个方法的时候,这个时候会生成一张寄宿图,这个寄宿图的尺寸是 layer 的宽 * 高 * contentsScale,其实算出来的是有多少像素。然后每个像素占用 4 个字节,总共消耗的内存大小为:宽 * 高 * contentsScale * 4 字节。

这里跟我们图片显示是一个道理:一张图片需要解压成位图才能显示到屏幕上,图片的颜色空间一般是 RGBA,每个像素点需要包含 RGBA 四个信息,所以一张图片解压成位图需要占用内存大小为:像素宽 * 像素高 * 4 个字节。(PS:将图片解压成位图是比较耗时的,这就是为什么通常会在子线程解压图片,然后再到主线程中显示,避免卡主主线程)

所以在使用 drawRect 方法来实现功能之前,需要看看是否有替代方案,避免产生寄宿图增加程序的内存,使用 CAShapeLayer 来绘制是一个不错的方案。

UILabel 绘制文字占用内存的情况

这里讲到绘制占用内存的情况,我们简单来了解下 Label 绘制文字占用的内存情况,实例代码如下:

//绘制一个全屏的 Label        UILabel *label1 = [[UILabel alloc] initWithFrame:self.view.bounds];    label1.text = @"11111";    [self.view addSubview:label1];        UILabel *label2 = [[UILabel alloc] initWithFrame:self.view.bounds];    label2.text = @"11111";    [self.view addSubview:label2];

绘制一个全屏的 Label,按理由需要占用内存:宽 * 高 * 4,iPhone 6SP 像素为:1242 * 2208,全屏差不多是占用 10 M 左右,但是 label1 大概会占用 3 M左右, label2 会占用 10 M左右;其实这里是因为如果使用黑白位图,苹果会优化颜色空间,这里每个像素就只会占用 1 个字节,比 4 字节节省 75% 的空间。

总结

通过了解 UIView 与 CALayer 是如何相互协同工作的,在之后的开发也可以选择相应的技术来实现功能,如果确定是不需要交互的,可以将 UIView 替换成 CALayer,来省去 UIView 封装带来的损耗,AsyncDisplayKit 库利用 ASDisplayNode 来替代 UIView 来节省资源。

你可能感兴趣的:(iOS UIView和CALayer是啥关系?)