在 iOS 开发中,UIView 是我们每天都会接触的基础组件,无论是按钮、标签还是自定义视图,本质上都是 UIView 的子类。但很多开发者在使用 UIView 时,都会忽略一个与之紧密绑定的核心类------CALayer(核心动画层)。
你是否遇到过这样的疑问:为什么设置 UIView 的 backgroundColor,本质上是设置它的 layer.backgroundColor?为什么给 UIView 加圆角、阴影,需要操作它的 layer?为什么有些动画(比如缩放、旋转)操作 layer 比操作 UIView 更流畅?
其实,UIView 与 CALayer 的关系,是 iOS 视图渲染的核心逻辑。今天这篇博客,就带你彻底搞懂两者的关系、协同工作的渲染流程,以及容易混淆的坐标系,每一部分都搭配实战示例,帮你从"会用"升级到"懂原理"。
一、UIView 与 CALayer:到底是什么关系?(核心重点)
先给出核心结论,记住这句话,就能理清两者的定位:UIView 是"管理者",CALayer 是"渲染者" 。
UIView 本身并不负责视图的渲染,它的核心作用是管理和协调 CALayer,处理用户交互(比如点击、手势);而 CALayer 才是真正负责将内容绘制到屏幕上的"执行者",负责视图的显示、动画、渐变等所有视觉相关的操作。
1. 两者的绑定关系:一对一且不可分割
每一个 UIView 都有且仅有一个默认的 CALayer(通过 view.layer 属性获取),这个 layer 被称为"根层";反过来,一个 CALayer 可以没有对应的 UIView(比如独立的 CALayer 用于渲染非交互内容),但如果有 UIView,那这个 layer 一定是 UIView 的根层。
可以这样类比:UIView 就像一个"项目经理",它不亲自干活,但负责统筹安排、对接需求(用户交互);CALayer 就像"技术工人",听从项目经理的安排,负责把具体的内容(颜色、图片、文字)绘制出来,呈现给用户。
2. 各自的核心职责(对比清晰,一眼看懂)
| UIView(管理者) | CALayer(渲染者) |
|---|---|
| 处理用户交互(点击、手势、触摸事件) | 负责视图的渲染(绘制内容、颜色、图片) |
| 管理子视图(addSubview、removeFromSuperview) | 管理子层(addSublayer、removeFromSuperlayer) |
| 响应布局(layoutSubviews、约束适配) | 控制视觉效果(圆角、阴影、边框、渐变) |
| 提供坐标系(frame、bounds、center) | 提供渲染相关属性(anchorPoint、contents、opacity) |
3. 实战示例:从代码看两者的关联
下面这个简单的示例,能直观看到 UIView 与 CALayer 的协同工作,注释里标注了关键关联点:
swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1. 创建 UIView(管理者)
let redView = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
view.addSubview(redView)
// 2. 操作 UIView 的属性,本质是操作它的 layer
redView.backgroundColor = .red // 等价于 redView.layer.backgroundColor = UIColor.red.cgColor
redView.layer.cornerRadius = 20 // 圆角只能通过 layer 设置,UIView 没有这个属性
redView.layer.shadowColor = UIColor.black.cgColor // 阴影也只能通过 layer 设置
redView.layer.shadowOffset = CGSize(width: 5, height: 5)
redView.layer.shadowOpacity = 0.5
// 3. 给 UIView 的 layer 添加子层(独立渲染,不影响 UIView 交互)
let blueLayer = CALayer()
blueLayer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
blueLayer.backgroundColor = UIColor.blue.cgColor
redView.layer.addSublayer(blueLayer)
// 注意:blueLayer 是 CALayer,没有用户交互能力,点击蓝色区域,不会触发 redView 的点击事件
}
}
示例说明:
- UIView 的 backgroundColor,本质是给它的 layer 设置 backgroundColor(注意:UIView 的 backgroundColor 是 UIColor,而 layer 的 backgroundColor 是 CGColor,系统会自动转换);
- 圆角、阴影等视觉效果,UIView 本身没有对应的属性,必须通过 layer 来设置,因为这些都是"渲染相关"的操作;
- 给 layer 添加子层(blueLayer),可以实现更灵活的渲染,但子层没有用户交互能力,所有交互都由 UIView 统一管理。
二、渲染流程:UIView 与 CALayer 如何协同绘制?
当我们把 UIView 添加到屏幕上时,系统会触发一系列渲染操作,这个过程中 UIView 和 CALayer 各司其职,协同完成从"数据"到"屏幕显示"的转换。整个渲染流程分为 4 个核心步骤,按顺序执行:
1. 步骤1:布局(Layout)------ UIView 主导,确定位置和大小
这一步由 UIView 负责,核心是确定每个视图(包括子视图)的 frame、bounds、center 等位置信息,以及 layer 的 frame 信息。
具体过程:系统会调用 UIView 的 layoutSubviews 方法,UIView 会根据自身的约束、frame 等,计算出所有子视图和自身 layer 的位置、大小,并将这些信息同步给对应的 CALayer。
注意:如果我们自定义 UIView,重写 layoutSubviews 方法,可以手动调整子视图或 layer 的布局,这是自定义布局的核心方式。
2. 步骤2:绘制(Display)------ CALayer 主导,绘制内容
布局完成后,就进入绘制阶段,这一步由 CALayer 负责,核心是将视图的内容(颜色、图片、文字等)绘制到"后备存储"(backing store,本质是一块内存缓冲区)。
具体过程:
- 如果 CALayer 的 contents 属性有值(比如设置了 UIImage 的 CGImage),直接使用这个内容绘制;
- 如果没有 contents,就会调用 CALayer 的 draw(:) 方法(或 UIView 的 draw(:) 方法,因为 UIView 实现了 CALayer 的 delegate,会代理 draw 操作),手动绘制内容。
3. 步骤3:合成(Compositing)------ 系统主导,合并所有图层
一个页面中,会有很多 UIView 和 CALayer(比如父视图、子视图、子层),每个 layer 都会生成自己的后备存储。系统会将所有 layer 的后备存储合并成一个"最终图像",这个过程称为"图层合成"。
这里有个关键优化点:如果只是调整 layer 的位置、缩放、旋转等"非内容"属性(不改变 layer 的 contents),系统不会重新绘制 layer,只会重新合成图层,这就是为什么操作 layer 做动画比操作 UIView 更流畅------因为跳过了"绘制"步骤,节省了性能。
4. 步骤4:显示(Render)------ 系统主导,呈现到屏幕
合成后的最终图像,会被系统发送到 GPU(图形处理器),由 GPU 渲染到屏幕上,完成整个显示过程。
实战示例:自定义绘制,看渲染流程的实际应用
下面我们自定义一个 UIView,重写 draw(_:) 方法,手动绘制一个圆形,直观感受绘制步骤的作用:
swift
import UIKit
// 自定义 UIView,重写绘制方法
class CircleView: UIView {
// 重写 draw 方法,手动绘制内容(本质是代理 CALayer 的 draw 操作)
override func draw(_ rect: CGRect) {
super.draw(rect)
// 1. 获取绘图上下文(与 CALayer 的后备存储关联)
guard let context = UIGraphicsGetCurrentContext() else { return }
// 2. 设置绘制属性
context.setFillColor(UIColor.orange.cgColor) // 填充色
context.setStrokeColor(UIColor.black.cgColor) // 边框色
context.setLineWidth(2) // 边框宽度
// 3. 绘制一个圆形(基于 UIView 的 bounds 坐标系)
let circleRect = bounds.insetBy(dx: 10, dy: 10) // 内缩10pt,留出边框空间
context.addEllipse(in: circleRect)
// 4. 执行绘制(填充+描边)
context.fillPath()
context.strokePath()
}
}
// 在 ViewController 中使用
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let circleView = CircleView(frame: CGRect(x: 100, y: 350, width: 200, height: 200))
view.addSubview(circleView)
// 注意:如果我们修改 circleView 的 bounds,会触发 layoutSubviews,进而重新调用 draw(_:) 方法,重新绘制
circleView.bounds = CGRect(x: 0, y: 0, width: 250, height: 250)
}
}
示例说明:
- 我们重写的是 UIView 的 draw(:) 方法,但本质上是代理它的 layer 完成绘制------UIView 是 CALayer 的 delegate,当 layer 需要绘制时,会调用 delegate 的 draw(:) 方法;
- 当我们修改 circleView 的 bounds 时,会触发 UIView 的 layoutSubviews 方法(布局步骤),布局完成后,系统会发现 layer 的内容需要重新绘制,进而调用 draw(_:) 方法,重新绘制圆形;
- 如果我们只是修改 circleView.layer 的 position(位置),不会触发绘制,只会触发合成步骤,性能更优。
三、坐标系:UIView 与 CALayer 的"位置规则"(最容易混淆的点)
在 iOS 开发中,坐标系的混淆是很多 bug 的根源------比如设置 layer 的 position 后,视图位置不符合预期;设置 anchorPoint 后,视图突然偏移。其实核心问题是:UIView 和 CALayer 共享一套坐标系,但有两个容易混淆的属性:anchorPoint 和 position。
1. 基础坐标系:UIKit 坐标系(与屏幕的关系)
首先明确 iOS 的基础坐标系:
- 原点(0,0)在屏幕的左上角;
- x 轴向右为正,y 轴向下为正;
- UIView 和 CALayer 的 frame、bounds、center 都是基于这个坐标系。
补充两个基础属性的区别(必记):
- frame:视图在父视图坐标系中的位置和大小(相对于父视图的左上角);
- bounds:视图在自身坐标系中的位置和大小(原点默认是自身左上角,size 与 frame.size 一致,修改 bounds 会缩放视图内容);
- center:视图在父视图坐标系中的中心点位置(等价于 layer 的 position 属性)。
2. 关键混淆点:anchorPoint(锚点)------ 图层的"旋转/缩放中心点"
anchorPoint 是 CALayer 独有的属性,UIView 没有这个属性,但它会影响 UIView 的位置表现,这是最容易踩坑的地方。
核心定义:anchorPoint 是 CALayer 的"锚点",即 layer 进行旋转、缩放、平移等变换时的中心点,它的坐标是基于 layer 自身的 bounds 坐标系(范围是 0~1,默认值是 (0.5, 0.5))。
默认情况下,anchorPoint = (0.5, 0.5),即 layer 的中心点,此时:
layer.position = layer.frame.origin + layer.bounds.size / 2 → 也就是 UIView 的 center。
3. 实战示例:修改 anchorPoint,看视图位置变化
下面这个示例,通过修改 anchorPoint,直观感受它的作用(注释标注关键逻辑):
swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1. 创建一个红色视图
let redView = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
redView.backgroundColor = .red
view.addSubview(redView)
// 2. 打印默认值(重点看 anchorPoint 和 position)
print("默认 anchorPoint:(redView.layer.anchorPoint)") // 输出 (0.5, 0.5)
print("默认 position:(redView.layer.position)") // 输出 (200, 200) → 中心点(100+200/2, 100+200/2)
print("默认 center:(redView.center)") // 输出 (200, 200) → 与 position 一致
// 3. 修改 anchorPoint 为 (0, 0)(layer 自身左上角)
redView.layer.anchorPoint = CGPoint(x: 0, y: 0)
// 4. 再次打印,观察变化
print("修改后 anchorPoint:(redView.layer.anchorPoint)") // 输出 (0, 0)
print("修改后 position:(redView.layer.position)") // 输出 (200, 200) → 不变!
print("修改后 frame:(redView.frame)") // 输出 (200, 200, 200, 200) → 位置偏移了!
// 原因:position 不变,anchorPoint 改变,导致 frame 自动调整(frame = position - anchorPoint * bounds.size)
// 计算过程:frame.origin = (200 - 0*200, 200 - 0*200) = (200, 200),所以视图向右下方偏移了 100pt
}
}
示例说明(核心坑点):
- 修改 anchorPoint 时,系统会保持 layer 的 position 不变,进而自动调整 frame 的位置------这就是为什么修改 anchorPoint 后,视图会"突然偏移";
- 如果想修改 anchorPoint 但不改变视图的位置,需要先保存 position,修改 anchorPoint 后,再恢复 position:
ini
// 正确做法:修改 anchorPoint 不偏移视图
let oldPosition = redView.layer.position
redView.layer.anchorPoint = CGPoint(x: 0, y: 0)
redView.layer.position = oldPosition // 恢复 position,frame 就不会变了
4. 补充:UIView 与 CALayer 坐标系的关联
总结3个关键结论,避免混淆:
- UIView 的 frame、bounds、center 与 layer 的 frame、bounds、position 是一一对应的,修改 UIView 的 center,本质是修改 layer 的 position;
- anchorPoint 是 layer 独有的属性,影响 layer 的变换中心点,不影响 UIView 的交互区域;
- 所有基于 layer 的变换(旋转、缩放、平移),都是以 anchorPoint 为中心点进行的。
四、总结:核心要点回顾(必记)
-
关系:UIView 管交互和布局,CALayer 管渲染和视觉效果,一对一绑定,UIView 是管理者,CALayer 是渲染者;
-
渲染流程:布局(UIView)→ 绘制(CALayer)→ 合成(系统)→ 显示(GPU),操作 layer 非内容属性可跳过绘制步骤,提升性能;
-
坐标系:共享 UIKit 左上角原点坐标系,anchorPoint 是 layer 独有的锚点,修改时需注意 frame 偏移问题;
-
实战技巧:圆角、阴影、渐变等视觉效果用 layer,用户交互用 UIView,自定义绘制重写 UIView 的 draw(_:) 方法。
理解 UIView 与 CALayer 的关系,不仅能帮你避开很多开发中的坑,还能让你更灵活地实现复杂的视觉效果和动画。比如自定义控件、优化动画性能、实现复杂的图层叠加,都离不开对这两个类的深入理解。