编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

    • README
    • ObjC编程入门

    • Swift编程入门

    • 专栏博客

      • iOS系统架构概览
      • Runtime消息转发机制
      • RunLoop运行循环揭秘
      • UIView生命周期与渲染
        • 1. 案例引入
          • 1.1 一个不会刷新的Label
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 UIView与CALayer双重身
          • 2.2 为什么分层设计
        • 3. UIView生命周期
          • 3.1 init→addSubview→layout→draw→dealloc
          • 3.2 各回调的调用时机与顺序
          • 3.3 ViewController生命周期联动
        • 4. 布局系统
          • 4.1 frame与bounds的经典陷阱
          • 4.2 layoutSubviews触发条件
          • 4.3 AutoLayout与手动布局的桥梁
        • 5. 显示与绘制
          • 5.1 setNeedsDisplay脏标记机制
          • 5.2 drawRect全流程剖析
          • 5.3 contents的位图本质
        • 6. CoreAnimation渲染管线
          • 6.1 四大阶段:Commit→Layout→Display→Render
          • 6.2 CATransaction隐式事务
          • 6.3 Render Server进程外渲染
        • 7. 离屏渲染
          • 7.1 什么是离屏渲染
          • 7.2 七大触发条件
          • 7.3 圆角优化的正确姿势
          • 7.4 Instruments调试离屏渲染
        • 8. 性能优化实战
          • 8.1 异步绘制原理(ASDK/Texture)
          • 8.2 光栅化与shouldRasterize
          • 8.3 图层混合与不透明优化
        • 9. 动画与渲染
          • 9.1 UIView动画与CALayer动画的分工
          • 9.2 隐式动画的触发条件
          • 9.3 presentationLayer与modelLayer
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一个像素的一生
          • 10.3 设计哲学回扣
          • 10.4 渲染速查表
      • 事件响应与手势识别
      • AutoLayout布局系统
      • 自定义View绘制实践
      • CoreAnimation动画实战
      • ViewController容器管理
      • 转场动画与导航设计
      • 网络层架构设计
      • 数据持久化方案选型
      • 多线程与GCD实践
      • 内存管理与ARC原理
      • Block底层原理与应用
      • KVO与KVC实现机制
      • 性能优化与启动加速
      • 组件化与模块设计
      • 安全加密与逆向防护
      • 跨平台与混编方案
  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • iOS开发和进阶
  • 专栏博客
杨充
2026-06-20
目录

UIView生命周期与渲染

# UIView生命周期与渲染

深入篇 | init→layout→draw→dealloc全链路,CALayer背板原理,CoreAnimation渲染管线,离屏渲染触发与优化实战。

# 目录介绍

  • 1. 案例引入
    • 1.1 一个不会刷新的Label
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 UIView与CALayer双重身
    • 2.2 为什么分层设计
  • 3. UIView生命周期
    • 3.1 init→addSubview→layout→draw→dealloc
    • 3.2 各回调的调用时机与顺序
    • 3.3 ViewController生命周期联动
  • 4. 布局系统
    • 4.1 frame与bounds的经典陷阱
    • 4.2 layoutSubviews触发条件
    • 4.3 AutoLayout与手动布局的桥梁
  • 5. 显示与绘制
    • 5.1 setNeedsDisplay脏标记机制
    • 5.2 drawRect全流程剖析
    • 5.3 contents的位图本质
  • 6. CoreAnimation渲染管线
    • 6.1 四大阶段:Commit→Layout→Display→Render
    • 6.2 CATransaction隐式事务
    • 6.3 Render Server进程外渲染
  • 7. 离屏渲染
    • 7.1 什么是离屏渲染
    • 7.2 七大触发条件
    • 7.3 圆角优化的正确姿势
    • 7.4 Instruments调试离屏渲染
  • 8. 性能优化实战
    • 8.1 异步绘制原理(ASDK/Texture)
    • 8.2 光栅化与shouldRasterize
    • 8.3 图层混合与不透明优化
  • 9. 动画与渲染
    • 9.1 UIView动画与CALayer动画的分工
    • 9.2 隐式动画的触发条件
    • 9.3 presentationLayer与modelLayer
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一个像素的一生
    • 10.3 设计哲学回扣
    • 10.4 渲染速查表

# 1. 案例引入

# 1.1 一个不会刷新的Label

先看一段在视频剪辑 App 里真实出现的 UI bug。开发同学写了一个显示当前播放进度的 UILabel,逻辑是每隔 0.1 秒用 GCD Timer 更新文字。代码很简单:

// ProgressView.m —— 播放进度显示
@interface ProgressView : UIView
@property (nonatomic, strong) UILabel *timeLabel;
@property (nonatomic, assign) CGFloat currentTime;
@end

@implementation ProgressView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        _timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
        [self addSubview:_timeLabel];
        // 启动高精度计时器
        [self startTimer];
    }
    return self;
}

- (void)startTimer {
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
                                                      dispatch_get_main_queue());
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC, 0.01 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        self.currentTime += 0.1;
        self.timeLabel.text = [NSString stringWithFormat:@"%.1f s", self.currentTime];
    });
    dispatch_resume(timer);
}

@end

现象:

  • 本地测试:Label 每 0.1 秒更新一次,流畅无误
  • 预生产:播放大视频文件时,Label 偶尔卡住 1-2 秒不更新,然后一次性跳到当前时间——但计时器本身没停,currentTime 的值是正确的

直觉怀疑:是不是 GCD Timer 在主线程被阻塞了?加日志发现 Timer 的 block 每 0.1 秒都执行了,self.timeLabel.text = ... 也确实被赋值了——但屏幕上就是没刷新。

# 1.2 顺藤摸到根因

带着这条线往下挖:

  • 假设 1:是不是 UI 更新被 RunLoop Mode 切换跳过了?—— Timer 用的 dispatch_get_main_queue(),GCD 跟 Mode 无关。否定。
  • 假设 2:是不是 Label 的 frame 为 0 导致不可见?—— frame 打印出来一直是 (0,0,200,44),没有问题。
  • 假设 3:是不是视频解码把主线程 CPU 打满了?—— Time Profiler 显示 CPU 确实高,但 GCD 主队列的 block 也执行了,text 也确实改动了——只是没渲染到屏幕。
  • 假设 4:那为什么 "赋值了文字,屏幕不刷新"?—— 查阅 UILabel 文档后找到关键点:设置 text 属性会调用 setNeedsDisplay(标记为 "需要重绘"),但真正的重绘发生在下一个 RunLoop 周期 CoreAnimation 提交事务时。如果某个 RunLoop 周期耗时过长(比如连续解码多个视频帧),那 Label 的 "需要重绘" 标记就会堆积到下一次事务提交时才统一执行。
  • 假设 5:那为什么 setNeedsLayout 也有同样的行为?—— 布局更新也是 "标记脏→RunLoop Observer 提交事务时统一布局"。

顺着这条线挖下去,发现整条渲染链路远比想象复杂——从 UILabel.text = @"xxx" 到像素亮起来,中间隔着 SetNeedsDisplay 标记 → CoreAnimation 事务提交 → Render Server 进程外合成 → GPU 绘制四层抽象。每一层都有独立的时机控制。

这一段事故里至少藏着 7 个原理点:

① UIView 和 CALayer 是什么关系?为什么修改 Layer 属性能直接生效?→ 第 2 章
② layoutSubviews 到底什么时候被调用?为什么有时连续调用多次?     → 第 3、4 章
③ setNeedsDisplay 不是立刻绘制,那"立刻"到底指什么时机?           → 第 5 章
④ CoreAnimation 的事务什么时候提交?谁负责提交?                   → 第 6 章
⑤ Render Server 是什么?为什么渲染在 App 进程外?                  → 第 6.3 节
⑥ 离屏渲染为什么影响性能?如何发现和避免?                         → 第 7 章
⑦ drawRect 到底是 GPU 还是 CPU 在画?                             → 第 5 章

# 1.3 我们要回答什么

这个 Label 不刷新的小事故就是本篇的主线案例。每讲完一段原理就解开一两个问号;最后在第 10 章,把整条链路兜回到 ProgressView 上,回答清楚"为什么 Label 赋值了但不刷新,又怎样保证实时刷新"。

本篇路线:

架构概览 (第 2 章)
   ↓
UIView 生命周期 (第 3 章) ─→ 解开"什么回调节点什么时候调"
   ↓
布局 → 显示与绘制 (第 4-5 章) ─→ 解开"setNeedsDisplay 不是立刻画的"
   ↓
CoreAnimation 管线 → 离屏渲染 (第 6-7 章) ─→ 解开"像素到底怎么上屏"
   ↓
性能优化 → 动画 (第 8-9 章) ─→ 武器库
   ↓
综合案例 (第 10 章) ─→ 案例彻底剖开

📌 本篇定位:这是 iOS 专栏中 "看得见" 的基石篇。前面 Runtime 讲了消息怎么走,RunLoop 讲了时机怎么调度——这一篇要把 "像素怎么亮起来" 从 UIView API 一路追到 GPU 指令级。读完本篇,任何 UIView 的性能问题你都能立刻回答:瓶颈在 Layout、Display、Render 三层中的哪一层。

# 2. 架构概览

# 2.1 UIView与CALayer双重身

疑惑:为什么 UIView 有一个 .layer 属性?为什么 view.frame 和 view.layer.frame 总是一样?

论证:每一个 UIView 内部都持有一个 CALayer。UIView 负责事件响应和生命周期管理,CALayer 负责视觉内容和渲染合成。它们是"伴侣关系"——UIView 是 CALayer 的 delegate(严格说是 CALayer 的 delegate 指向 UIView)。

UIView (轻量级 ObjC 对象)
  ├── 属性: frame, bounds, center, backgroundColor, alpha, hidden
  ├── 方法: layoutSubviews, drawRect, addSubview, hitTest
  ├── 事件: touchesBegan, gestureRecognizer
  │
  └── 内部持有 ──────────────────────────────────────┐
                                                      │
  CALayer (轻量级 C 对象)                              │
  ├── 属性: frame, bounds, position, backgroundColor, opacity, hidden
  ├── 属性: contents (位图 CGImageRef,屏幕显示的本质内容)
  ├── 方法: setNeedsDisplay, display, drawInContext
  │
  └── 内部持有 ──────────────────────────────────────┐
                                                      │
  backing store (CABackingStore) ── GPU 可读的像素缓冲区
  └── contents (位图)

UIView 和 CALayer 的属性映射:

UIView 属性 CALayer 对应属性 说明
frame frame UIView 的 frame setter 调了 layer.frame = ...
bounds bounds 同上
center position view.center = layer.position
backgroundColor backgroundColor 颜色值直接传递给 layer
alpha opacity 0.0~1.0
hidden hidden YES / NO
transform transform 或 affineTransform CGAffineTransform / CATransform3D
clipsToBounds masksToBounds YES / NO
contentMode contentsGravity UIViewContentMode → 对应的 gravity 字符串

关键差异——CALayer 有但 UIView 没有的:

CALayer 属性 说明
cornerRadius 圆角半径(直接设会触发离屏渲染)
borderWidth / borderColor 边框
shadow* 系列 阴影(触发离屏渲染)
mask 遮罩层(触发离屏渲染)
contents 真正显示在屏幕上的位图
sublayerTransform 3D 透视变换

# 2.2 为什么分层设计

疑惑:为什么不把 UIView 和 CALayer 合并?一个对象管一切不更直接吗?

论证:

  1. 职责分离——UIView 负责 iOS 特有的 "触摸事件 + 响应链",CALayer 负责 "视觉效果 + 合成"。分开后,macOS 的 NSView 也能共用同一个 CALayer 底层(它们确实共用)。CALayer 是跨平台的渲染引擎,UIView 是 iOS 平台的事件封装。

  2. 性能分层——UIView 是 ObjC 对象,每次创建都有 ObjC 运行时开销(消息发送、引用计数);CALayer 是 C 对象(CA::Layer),更轻量。在不需要事件交互的场景(比如粒子系统、大量小元素),直接操作 CALayer 比创建 UIView 高效得多。

  3. 进程隔离——CALayer 的合成是由 Render Server(独立进程)完成的。如果 UIView 和 Layer 合并,就意味着 App 进程要直接跟 GPU 驱动通信——一旦 App 崩溃,整个渲染系统跟着挂。Render Server 进程外合成是 iOS 图形栈最精妙的安全设计(第 6.3 节展开)。

  4. 反向验证:Android 的 View 包含了测量、布局、绘制、事件全部责任——这就是为什么 Android 的 View 对象比 CALayer 重得多,inflate 一个复杂布局可能耗时几十毫秒。iOS 把 "绘制" 下沉到 Layer,"事件" 留在 View——一份重量由两个对象分摊。

# 3. UIView生命周期

# 3.1 init→addSubview→layout→draw→dealloc

UIView 从生到死的完整生命周期(以代码初始化的 View 为例):

alloc & init(frame:)
        │
        ▼
[superview addSubview:self]          ← 加入视图层级
        │
        ▼
willMoveToSuperview:                 ← 即将加入父视图
        │
        ▼
didMoveToSuperview                   ← 已加入父视图(此时 self.superview 非空)
        │
        ▼
┌─ layoutSubviews ──────────────────────────────┐
│  触发条件:                                     │
│  1. addSubview 后首次布局                       │
│  2. frame / bounds 改变                        │
│  3. setNeedsLayout 被调用                       │
│  4. UIScrollView 滚动时                        │
│  5. 旋转屏幕                                   │
│  6. 手动调用 layoutIfNeeded                    │
│                                                │
│  此时:frame 已确定,子视图位置应基于 self.bounds 设置│
└────────────────────────────────────────────────┘
        │
        ▼
drawRect:                            ← 首次显示时或 setNeedsDisplay 后
        │
        ▼
┌─ 正常显示阶段 ──────────────────────┐
│  重复 layoutSubviews → drawRect    │
│  App 运行期间的不断交互               │
└────────────────────────────────────┘
        │
        ▼
willMoveToSuperview:nil              ← 即将从父视图移除
        │
        ▼
[superview willRemoveSubview:self]
        │
        ▼
[self removeFromSuperview]           ← 真正移除
        │
        ▼
didMoveToSuperview                   ← superview 为 nil
        │
        ▼
dealloc                              ← 引用计数归零,释放

# 3.2 各回调的调用时机与顺序

@interface MyView : UIView
@end

@implementation MyView

// 1️⃣ 指定初始化方法(代码创建时)
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    // ✅ 此时可以设置属性、创建子视图
    // ❌ 不要依赖 self.frame.size(可能还没最终确定)
    return self;
}

// 1️⃣' xib/storyboard 反序列化时
- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    // ✅ 反序列化属性
    return self;
}

// 1️⃣'' xib/storyboard 加载完成
- (void)awakeFromNib {
    [super awakeFromNib];
    // ✅ xib 加载完毕,outlet 已连接,可以做一些 IB 中的额外配置
}

// 2️⃣ 即将加入父视图
- (void)willMoveToSuperview:(UIView *)newSuperview {
    [super willMoveToSuperview:newSuperview];
    // newSuperview 为 nil = 即将被移除
    // newSuperview 非 nil = 即将被添加
}

// 3️⃣ 已经加入父视图
- (void)didMoveToSuperview {
    [super didMoveToSuperview];
    // ✅ self.superview 可用,但 frame 可能还没最终确定
}

// 4️⃣ 即将加入窗口(可见)
- (void)willMoveToWindow:(UIWindow *)newWindow {
    [super willMoveToWindow:newWindow];
    // newWindow 为 nil = 即将离开屏幕
    // newWindow 非 nil = 即将显示在屏幕上
}

// 5️⃣ 已经加入窗口
- (void)didMoveToWindow {
    [super didMoveToWindow];
    // ✅ self.window 可用,视图真的可见了
}

// 6️⃣ 布局子视图(可能被多次调用!)
- (void)layoutSubviews {
    [super layoutSubviews];
    // ✅ 在这里设置子视图的 frame
    // ⚠️ 别在这里做耗时的计算——此方法可能高频调用
}

// 7️⃣ 绘制(自定义绘制时使用)
- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    // ✅ 使用 CoreGraphics API 在这里画
    // ⚠️ CPU 绘制,开销大——慎用
}

// 8️⃣ 析构
- (void)dealloc {
    // ✅ 移除通知、KVO、Timer
}

@end

关键点——layoutSubviews 可能被多次调用,但不一定每次调用后都调 drawRect。系统会尽量在同一个 RunLoop 周期内合并多次 setNeedsLayout 为一次 layoutSubviews。

# 3.3 ViewController生命周期联动

UIViewController 的生命周期与 UIView 紧密交织:

viewDidLoad()
    │
    └─ view 已创建(从 xib/code/loadView)但 frame 还未确定
       ✅ 初始化子视图、设置属性
       ❌ frame/bounds 不可靠

viewWillAppear:
    │
    └─ view 即将加入 window 层级
       ✅ 开始动画、注册通知

viewWillLayoutSubviews / viewDidLayoutSubviews
    │
    ├─ layoutSubviews 被调用前 / 后
    │  ✅ viewDidLayoutSubviews 中 frame 已确定
    │  ⚠️ 可能被多次调用(旋转屏幕、状态栏变化等)

viewDidAppear:
    │
    └─ view 已经可见
       ✅ 启动 Timer、开始网络请求

viewWillDisappear: / viewDidDisappear:
    │
    └─ view 即将/已经不可见
       ✅ 暂停 Timer、保存状态

dealloc
    │
    └─ VC 被释放

📌 面试常考题:viewDidLoad 和 viewDidLayoutSubviews 哪个能拿到正确的 frame?答:viewDidLayoutSubviews——因为在 viewDidLoad 时 view 的 frame 还是 xib/storyboard 里的原始值,尚未根据实际屏幕尺寸计算。

# 4. 布局系统

# 4.1 frame与bounds的经典陷阱

疑惑:frame 和 bounds 都表示矩形,到底有什么区别?

论证:

frame bounds
坐标系 父视图的坐标系 自身的坐标系
原点含义 在父视图中的位置 可视区域的偏移(默认 (0,0))
大小关系 frame.size 不一定等于 bounds.size(旋转时) —
何时变化 移动/缩放视图 滚动 UIScrollView 的 contentOffset
UIView *parent = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
UIView *child  = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
[parent addSubview:child];

child.frame  → {{50, 50}, {100, 100}}   // 在 parent 里的位置
child.bounds → {{0, 0},   {100, 100}}   // 自身坐标系原点始终是 (0,0)

经典陷阱——bounds 改变会影响子视图位置:

// 如果改了 parent.bounds.origin → child 在屏幕上的位置就变了
parent.bounds = CGRectMake(50, 50, 300, 300);
//   ↑ 相当于把 parent 的 "可视窗口" 向右下移动了 50pt
//   → child 会向左上移动 50pt(因为 parent 的坐标系原点的物理位置变了)

这就是 UIScrollView 滚动的底层原理——UIScrollView 在滚动时改变了 自身 bounds.origin,导致所有子视图看起来在移动。它从没改过子视图的 frame。

# 4.2 layoutSubviews触发条件

layoutSubviews 不是"你想让它调它就调"——它只在特定条件满足时由系统触发:

触发条件 示例 是否触发 layoutSubviews
addSubview: 往一个已有 frame 的父视图加子视图 ✅
removeFromSuperview 移除子视图 ✅(父视图)
setFrame: / setBounds: 改变尺寸 ✅
屏幕旋转 从竖屏到横屏 ✅
UIScrollView 滚动 每次滚动 ✅
setNeedsLayout 手动标记 ✅(下一个 RunLoop 周期)
layoutIfNeeded 立刻布局(如果有脏标记) ✅(立即)
设置 text 属性 UILabel 文字改了 ❌(只触发布局标记,不一定调 layoutSubviews)
改变 hidden 隐藏/显示 ❌
修改 backgroundColor 背景色 ❌

关键差异——setNeedsLayout vs layoutIfNeeded:

// setNeedsLayout:标记为"需要布局",在下一个 RunLoop 周期执行
[self.view setNeedsLayout];
NSLog(@"0. 标记布局了,但还没执行");

// layoutIfNeeded:如果有标记,立刻执行布局
[self.view layoutIfNeeded];
NSLog(@"1. 布局已经执行完了,frame 已更新");

动画中的布局:

// 方式 1:在动画 block 里调 layoutIfNeeded → 布局变更被动画化
[UIView animateWithDuration:0.3 animations:^{
    self.redView.frame = CGRectMake(100, 100, 200, 200);
    [self.view layoutIfNeeded];      // ← 立刻布局,变化过程会动画
}];

// 方式 2:只改约束 constant,不调 layoutIfNeeded → 无动画,下一个 RunLoop 才更新
self.redViewTopConstraint.constant = 100;

# 4.3 AutoLayout与手动布局的桥梁

- (void)layoutSubviews {
    [super layoutSubviews];       // ← AutoLayout 在这里执行约束计算
    
    // 然后在下面可以基于 AutoLayout 计算的结果做额外的手动布局
    self.shadowView.frame = CGRectInset(self.imageView.frame, -4, -4);
}

执行顺序:[super layoutSubviews] 内部 → 触发 updateConstraints → 求解 AutoLayout 约束 → 更新所有受约束视图的 frame → 返回 → 你写的自定义布局代码。

# 5. 显示与绘制

# 5.1 setNeedsDisplay脏标记机制

回到主线案例的核心疑问——为什么 label.text = @"xxx" 设置了但屏幕不立刻更新?

论证:UILabel 在 setText: 里做了两件事:

  1. 更新内部的 _text 属性
  2. 调用 [self setNeedsDisplay] —— 标记 "我需要重绘"

但 setNeedsDisplay 不是立刻触发 drawRect:——它只是在 CALayer 内部设置了一个脏标记(_displayDisabled 或类似的 flag)。真正的绘制发生在 下一个 RunLoop 周期,CoreAnimation 提交事务时。

主线程执行流:
RunLoop 周期 1:
  ├── Timer block: label.text = @"0.1 s"     → setNeedsDisplay(标记脏)
  ├── Timer block: label.text = @"0.2 s"     → setNeedsDisplay(再次标记,合并)
  ├── Timer block: label.text = @"0.3 s"     → setNeedsDisplay(再次合并)
  ├── ... 
  ├── RunLoop BeforeWaiting
  ├── CoreAnimation Observer: 提交事务!     ← 此时才真正执行 drawRect
  │   └── CALayer.display → UIView.drawRect  → 生成 0.3 s 的位图
  └── Render Server: 合成 → 显卡输出像素

最终屏幕上显示的始终是最后一个 RunLoop 周期结束时的那次绘制结果。

这就是案例中 "Label 卡住 1-2 秒然后一跳" 的完整解释:

时间线:
T=0.0s:   text = "0.0s"  → setNeedsDisplay(标记)
T=0.1s:   text = "0.1s"  → setNeedsDisplay(标记被覆盖)
T=0.2s:   text = "0.2s"  → setNeedsDisplay(再次覆盖)
T=0.3s:   ...RunLoop 来了个耗时操作(视频解码,300ms)...
T=0.6s:   耗时操作结束,RunLoop 继续
T=0.6s:   CoreAnimation 提交事务 → label 被重绘 → 显示 "0.6s"
           ↑ 中间 0.1~0.5s 的更新全部丢失——因为绘图线程没机会执行

解决方案:不要依赖 setNeedsDisplay 的延迟合批——如果对实时性要求极高,应在每个 Timer 回调里手动调用 CATransaction flush 强制提交当前事务(但代价是性能)。

# 5.2 drawRect全流程剖析

疑惑:drawRect: 里面到底发生了什么?为什么它跑在 CPU 上?

drawRect: 的完整流程:

[view setNeedsDisplay]
        │
        ▼
CALayer 设置脏标记
        │
        ▼
RunLoop BeforeWaiting → CoreAnimation 提交事务
        │
        ▼
CALayer 的 display 方法被调用
        │
        ▼
[self displayLayer:layer]    ← UIView 作为 CALayer 的 delegate 收到回调
        │
        ▼
[self drawRect:rect]          ← 你在子类里重写的这个方法
        │
        ▼
CoreGraphics 函数在此执行(CPU 运算)
  ├── CGContextRef ctx = UIGraphicsGetCurrentContext();  ← 系统在调用 drawRect 前创建好了上下文
  ├── CGContextSetFillColorWithColor(ctx, ...)
  ├── CGContextAddRect(ctx, ...)
  ├── CGContextDrawPath(ctx, ...)
  └── 其他 CG API...
        │
        ▼
绘制结果写入 CALayer 的 contents(CGImageRef 位图)
        │
        ▼
CALayer.contents 就绪 → Render Server 拿去合成 → GPU 输出

关键点:

  1. drawRect: 纯粹是 CPU 绘制(软件光栅化)——所有 CGContext* 函数在 CPU 上执行,不经过 GPU 的 shader 管线。
  2. 系统在 drawRect: 调用前自动创建了 CGContextRef——你是 "往画布上画",而不是 "创建画布"。
  3. drawRect: 只为你的视图创建 "它自己的位图"——子视图的样貌不会被画进来(子视图有自己的 layer 和自己的位图)。
  4. 不要手动调用 drawRect:——要触发重绘,调 setNeedsDisplay。

# 5.3 contents的位图本质

所有 UIView 在屏幕上显示的本质,是 CALayer 上指向的一块位图(contents 属性)。

// 直接设置 Layer 的 contents(绕过 drawRect)
UIImage *image = [UIImage imageNamed:@"cat.jpg"];
self.customView.layer.contents = (__bridge id)image.CGImage;
// ↑ 这相当于"直接把照片贴在 layer 上",没经过任何绘制

不同类型视图的 contents 来源:

视图类型 contents 来源 何时生成
UIImageView image.CGImage 设置 image 属性时(不解码,GPU 解码)
UILabel drawRect 生成的位图 setNeedsDisplay 后,事务提交时
自定义 drawRect CoreGraphics 在 CPU 画的 同上
UIButton 同 UILabel + 子 layer 叠加 同上
纯色背景 View 无 contents(GPU 直接填充颜色) 不需要 contents

⚠️ drawRect: 的最大代价:它生成一个与视图尺寸同大的位图。在 iPhone 14 Pro(3× 屏幕)上,一个 100pt × 100pt 的视图 = 300px × 300px × 4 字节(RGBA)= 360 KB 的位图。如果你滚动列表时每帧都触发 drawRect:——内存分配 + CPU 绘制的开销不容忽视。

# 6. CoreAnimation渲染管线

# 6.1 四大阶段:Commit→Layout→Display→Render

一次完整的渲染周期(从 setNeedsDisplay 到像素亮起来),CoreAnimation 将它分成四个阶段:

App 进程                                Render Server 进程           GPU
───────                                 ─────────────────           ───
                                                       
┌─ 阶段 1: Commit ─────────────────┐
│  CATransaction 提交               │
│  ├── 收集所有变更的 Layer 属性    │
│  ├── 构建 render tree(渲染树)   │
│  └── 通过 IPC 把树发给 Render ────►  Render Server 收到 render tree
│      Server                        │
└────────────────────────────────────┘
                                                       
┌─ 阶段 2: Layout ─────────────────────────────────────────────────────────┐
│  在 App 进程(非主线程!)                                              │
│  ├── 执行 layoutSubviews(如果需要)                                    │
│  └── frame/bounds 变更                                                        │
└─────────────────────────────────────────────────────────────────────────┘
                                                       
┌─ 阶段 3: Display ─────────────────┐
│  在 App 进程                       │
│  ├── 执行 drawRect(如果需要)     │
│  └── 生成 CALayer.contents 位图    │
└────────────────────────────────────┘
                               │
                               ▼
                    contents 就绪
                               │
                               ▼
┌─ 阶段 4: Render ────────────────────────────────────────────────────────┐
│  Render Server 把图层树转换为 OpenGL / Metal 指令                      │
│  ├── 图层合成(alpha 混合、mask)                                       │
│  ├── 发给 GPU                                                           │
│  │    ├── Vertex Shader                                                │
│  │    ├── Rasterization                                                │
│  │    └── Fragment Shader → 写入 Framebuffer → 扫描输出到屏幕 🖥        │
│  └── vsync 同步(跟着屏幕刷新率)                                         │
└─────────────────────────────────────────────────────────────────────────┘

耗时分布分析(以 iPhone 14 60FPS 为基准):

阶段 典型耗时 在哪执行 受什么影响
Commit < 1ms App 主线程 Layer 数量
Layout 0~10ms App 主线程(layoutSubviews) 布局复杂度、约束数量
Display 0~50ms App 任意线程 drawRect 复杂度、位图尺寸
Render 1~5ms Render Server + GPU 图层数量、混合、滤镜

# 6.2 CATransaction隐式事务

疑惑:我不写 [CATransaction begin],CoreAnimation 怎么知道什么时候 "该提交了"?

论证:CoreAnimation 在 每个 RunLoop 的 BeforeWaiting 阶段 自动提交事务。它通过一个 Observer 来监听这个时机。

// CoreAnimation 内部注册的 Observer(伪代码)
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault,
    kCFRunLoopBeforeWaiting | kCFRunLoopExit,  // ← 关注这两个时机
    YES, 0,
    &CA::Transaction::observer_callback,        // ← 回调里调用 commit
    NULL
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

CATransaction 的四条核心方法:

// 1. 开始一个事务(通常不需要手动写——每个 RunLoop 周期默认有一个)
[CATransaction begin];

// 2. 设置事务属性
[CATransaction setAnimationDuration:0.5];     // 动画时长
[CATransaction setDisableActions:YES];        // 关闭隐式动画
[CATransaction setCompletionBlock:^{          // 事务提交后的回调
    NSLog(@"动画完毕");
}];

// 3. 提交事务(立即把所有 Layer 变更推给 Render Server)
[CATransaction commit];

// 4. 强制 flush(不等 RunLoop,立刻提交)
[CATransaction flush];

[CATransaction flush] 就是主线案例的 "强制立刻渲染" 答案:

dispatch_source_set_event_handler(timer, ^{
    self.currentTime += 0.1;
    self.timeLabel.text = [NSString stringWithFormat:@"%.1f s", self.currentTime];
    [CATransaction flush];   // ← 不等 RunLoop,立刻提交渲染
});

⚠️ 警告:频繁 flush 会破坏 CoreAnimation 的合批优化——本来一次 RunLoop 周期可以合并 10 次 setNeedsDisplay 为一次 drawRect,你现在每次 flush 强制它画一次,CPU 开销暴涨。

# 6.3 Render Server进程外渲染

疑惑:为什么 iOS 需要一个单独的进程来做渲染合成?在 App 进程里合成不行吗?

论证:Render Server(进程名 backboardd)是 iOS 的 "图形合成器",类似于 macOS 的 WindowServer 进程。

三大理由:

  1. 安全隔离——App 进程是沙盒化的,不能访问其他 App 的图层数据。Render Server 拥有全局视角,收到所有 App 的 render tree 后统一合成 → 输出到 Framebuffer。一个 App 崩溃了,不影响其他 App 的显示。

  2. GPU 资源管理——GPU 是一个共享资源,由 Render Server 独占调度,避免多个 App 进程直接竞争 GPU(那会导致死锁和竞态)。Render Server 按 Z-order 把所有 App 的图层合成一张完整的帧。

  3. 系统 UI 特权——状态栏、控制中心、Home Indicator 这些系统 UI 的图层是 App 无权访问的。Render Server 把它们叠在 App 的图层之上——你对系统 UI 层的任何访问尝试都会被 Render Server 拒绝。

  App A 图层树          App B 图层树          系统 UI 图层
  ┌──────────┐        ┌──────────┐         ┌──────────┐
  │ ████████ │        │ ░░░░░░░░ │         │ 状态栏    │
  └──────────┘        └──────────┘         └──────────┘
        │                    │                   │
        └────────────────────┼───────────────────┘
                             │ (IPC)
                             ▼
                    Render Server
                    ┌──────────────────────┐
                    │ 按 Z-order 合成     │
                    │ 系统UI (最顶层)       │
                    │ App B               │
                    │ App A               │
                    └──────────────────────┘
                             │
                             ▼
                     OpenGL/Metal → GPU → Framebuffer → 屏幕

# 7. 离屏渲染

# 7.1 什么是离屏渲染

疑惑:GPU 不是天生就在 "屏幕上" 渲染吗?什么叫 "离屏"?

论证:正常渲染(On-Screen Rendering)——GPU 把计算结果直接写入 Framebuffer(帧缓冲区),然后显示器扫描 Framebuffer 把它输出到屏幕。

离屏渲染(Off-Screen Rendering)——GPU 不直接把结果写入 Framebuffer,而是先在另一块缓冲区(离屏缓冲区)算好,再作为纹理贴回到 Framebuffer。多了一次额外的 "纹理复制" 操作。

正常渲染(On-Screen):
  图层 A ──► GPU ──► Framebuffer ──► 屏幕
  图层 B ──► GPU ──► Framebuffer ──► 屏幕
  (并行、逐个画)

离屏渲染(Off-Screen):
  图层 A ──┐
  图层 B ──┼─► GPU ──► 离屏缓冲区 ──► 纹理 ──► Framebuffer ──► 屏幕
  (先合成在一个临时纹理里,再贴回去 → 多了一次内存拷贝)

为什么离屏渲染慢?

开销项 说明
创建临时缓冲区 需要分配一块与最终渲染区域同大的 GPU 内存
切换渲染目标 GPU 切换 Framebuffer → 离屏缓冲区 → Framebuffer,每切换一次都有状态开销
额外纹理读取 从离屏缓冲区读回数据并贴到最终画面
无法批量 离屏渲染破坏 GPU 的合批(batching)优化

# 7.2 七大触发条件

触发条件 代码 为什么触发离屏渲染
1. cornerRadius + masksToBounds layer.cornerRadius = 8; layer.masksToBounds = YES; GPU 不知道圆角外该丢弃什么(有子图层时无法直接裁剪)
2. mask layer.mask = maskLayer; 遮罩图层和被遮罩图层需要先合成为中间纹理
3. shadow* layer.shadowOpacity = 0.5; 阴影需要先算出完整图形才能模糊和偏移
4. group opacity layer.opacity = 0.5; layer.allowsGroupOpacity = YES; 多个子图层组合后再做半透明
5. shouldRasterize = YES layer.shouldRasterize = YES; 本质就是先渲染到离屏缓冲区再缓存(用空间换时间)
6. 抗锯齿边缘 layer.edgeAntialiasingMask != 0; 需要周围像素参与计算
7. maskedCorners(iOS 11+) layer.maskedCorners = kCALayerMinXMinYCorner; 部分圆角也需要离屏

验证——实现指定圆角但不触发离屏渲染:

// ❌ 触发了离屏渲染(cornerRadius + masksToBounds)
imageView.layer.cornerRadius = 8;
imageView.layer.masksToBounds = YES;

// ✅ 不触发(用 CoreGraphics 直接画出一个圆角图片)
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds
                                                cornerRadius:8];
[path addClip];
[originalImage drawInRect:imageView.bounds];
UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
imageView.image = roundedImage;   // contents 已经是圆角的了,GPU 不需要动态裁剪

# 7.3 圆角优化的正确姿势

方案 是否离屏渲染 适用场景 代价
cornerRadius + masksToBounds 是 包含子视图需要裁剪时 GPU 开销
cornerRadius 单独使用(无子视图、无 image) 否 纯色背景的 UILabel/UIView 无
CPU 预裁剪图片 否 UIImageView 显示本地图片 CPU 时间 + 内存
CAShapeLayer 作为 mask 是 不规则形状 GPU 开销
使用 UIGraphicsImageRenderer 画圆角图片 否 网络异步加载的图片 CPU 时间
// 最优方案——异步裁剪 + 缓存
- (void)setRoundedImage:(UIImage *)image forImageView:(UIImageView *)imageView {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
                                              initWithSize:imageView.bounds.size];
        UIImage *rounded = [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
            UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds
                                                            cornerRadius:8];
            [path addClip];
            [image drawInRect:imageView.bounds];
        }];
        dispatch_async(dispatch_get_main_queue(), ^{
            imageView.image = rounded;   // 一张已经圆角的图片,GPU 无额外开销
        });
    });
}

# 7.4 Instruments调试离屏渲染

方法 1:模拟器 Debug 菜单

Simulator → Debug → Color Off-screen Rendered
→ 黄色高亮 = 触发了离屏渲染的区域

方法 2:Instruments → Core Animation

Core Animation instrument → 勾选 "Color Offscreen-Rendered Yellow"

方法 3:代码调试

// 在任意 UIView 子类中
- (void)drawRect:(CGRect)rect {
    // 检查当前是否在离屏渲染
    // 这段代码只能用于调试,不要上生产
    NSLog(@"drawRect called");
}

# 8. 性能优化实战

# 8.1 异步绘制原理(ASDK/Texture)

drawRect: 默认在主线程执行——对于一个复杂 cell,drawRect 耗时 50ms,用户就会感知到一帧卡顿。Texture(前身 ASDK / AsyncDisplayKit)的核心优化就是 把 drawRect 挪到后台线程执行:

传统绘制:                          Texture 异步绘制:
主线程 setNeedsDisplay              主线程 setNeedsDisplay
   ↓                                    ↓
主线程 CALayer.display              主线程 CALayer.display
   ↓                                    ↓
主线程 drawRect (CPU 绘制,50ms)    后台线程 CGContext (CPU 绘制,50ms)
                                 ↑     ↓
                        ⚠️ 卡一帧      主线程把画好的位图设给 layer.contents
                                 ↑     ↓
                                    没有卡顿(主线程只做了赋值操作)

关键技术点:
1. 用 CGBitmapContext 在后台线程创建离屏上下文
2. 把 CoreGraphics API 调用直接画到后台上下文中
3. 拿到 CGImage 后 dispatch 回主线程设给 layer.contents

简化版实现:

- (void)displayLayer:(CALayer *)layer {
    // 重写 CALayerDelegate——跳过系统的 drawRect 路径
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
        format.scale = [UIScreen mainScreen].scale;
        UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
                                              initWithSize:layer.bounds.size
                                              format:format];
        UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
            // 后台线程绘制
            [self drawContentInContext:ctx.CGContext];
        }];
        dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)image.CGImage;
        });
    });
}

# 8.2 光栅化与shouldRasterize

shouldRasterize = YES 的语义:把当前图层及其子图层"拍一张快照"缓存起来,后续直接复用这张快照,不再重新合成子图层。

// 适用场景:结构复杂但不经常变化的图层
complexView.layer.shouldRasterize = YES;
complexView.layer.rasterizationScale = [UIScreen mainScreen].scale;
场景 是否开启 原因
静态复杂图层(不缩放、不旋转) ✅ 避免每次帧都重新合成
频繁变化的图层 ❌ 每帧都要失效→重新光栅化,更慢
图层正在动画 ❌ 动画期间每帧都重画
内存敏感的设备 ❌ 光栅化缓存占用额外的 GPU 内存

# 8.3 图层混合与不透明优化

**图层混合(Blending)**是 GPU 最常见的性能杀手——当一个像素点上有多个半透明图层时,GPU 需要从 Framebuffer 读回当前像素颜色(这很慢!),与新颜色做 alpha 混合计算,再写回去。

正常(不透明):
  GPU 直接写入颜色值 → 快

混合(透明):
  GPU 读取当前颜色 → 混合计算 → 写入新颜色 → 慢(多了 "读回" 这一步)

优化策略:

// 1. 设置 opaque = YES(告诉 GPU 这个视图完全不透明,跳过混合)
self.view.opaque = YES;
// ⚠️ 这要求视图的背景色 alpha = 1.0,且填满自身 bounds

// 2. 避免不必要的透明背景
label.backgroundColor = [UIColor whiteColor];   // ✅ 显式设置不透明背景
// label.backgroundColor = [UIColor clearColor]; // ❌ 透明背景会触发混合

// 3. 使用 Instruments 检查混合情况
// Simulator → Debug → Color Blended Layers
// 绿色 = 无混合 ✅
// 红色 = 有混合 ⚠️

# 9. 动画与渲染

# 9.1 UIView动画与CALayer动画的分工

// UIView 动画(最常用)
[UIView animateWithDuration:0.3 animations:^{
    self.redView.frame = CGRectMake(100, 100, 200, 200);
}];
// 底层实际是:
//   1. 设置 CALayer 属性的 model 值
//   2. 创建 CABasicAnimation 加到 layer 上
//   3. CATransaction commit → Render Server 看到动画 → 渐进式渲染

// CALayer 隐式动画
self.redView.layer.position = CGPointMake(200, 200);
// UIView 默认关闭了 CALayer 的隐式动画,需要手动开
[CATransaction begin];
[CATransaction setDisableActions:NO];
self.redView.layer.position = CGPointMake(200, 200);
[CATransaction commit];

// CALayer 显式动画——完全自己控制
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"];
anim.fromValue = [NSValue valueWithCGPoint:startPoint];
anim.toValue   = [NSValue valueWithCGPoint:endPoint];
anim.duration  = 0.5;
[self.redView.layer addAnimation:anim forKey:@"move"];

分工:

UIView 动画 CALayer 动画
设计意图 面向业务开发,闭包式 API 面向底层控制,delegate 回调
自动加动画 UIView.animate block 内 仅在 actionForLayer:forKey: 返回非 nil 时(独立 CALayer)
动画结束回调 completion block animationDidStop:finished: delegate
Model 值处理 自动更新 model 值 需手动处理(否则动画结束弹回原位)

# 9.2 隐式动画的触发条件

CALayer 的大部分属性被修改时,在特定条件下会触发隐式动画(0.25 秒的淡入过渡)。但 UIView 的 backing layer 默认关闭了隐式动画——也就是说,直接改 self.view.layer.position 不会动画。

// 没有隐式动画的场景(UIView 的 backing layer):
self.view.layer.position = CGPointMake(200, 200);    // ❌ 直接跳变

// 有隐式动画的场景(独立 CALayer):
CALayer *standaloneLayer = [CALayer layer];
standaloneLayer.position = CGPointMake(200, 200);     // ✅ 0.25 秒动画过渡

// 手动控制 UIView backing layer 是否开隐式动画:
[CATransaction begin];
[CATransaction setDisableActions:NO];                 // 打开隐式动画
self.view.layer.position = CGPointMake(200, 200);     // ✅ 现在有动画了
[CATransaction commit];

# 9.3 presentationLayer与modelLayer

疑惑:动画过程中,真实的 layer 在哪?怎么拿到 "动画中间状态" 的值?

论证:CALayer 有两层数据存储:

  • modelLayer:你的代码看到的 "真实值"(比如 layer.position 直接读取的值)
  • presentationLayer:屏幕当前实际显示的值(动画中间帧)
Model Layer:      position = (200, 300)    ← 最终目标
Presentation Layer: position = (156, 278)   ← 动画播放到一半时屏幕上的真实位置

// 获取屏幕上的真实位置:
CGPoint screenPosition = [self.redView.layer.presentationLayer position];

实战——拿到动画中的实时 frame:

- (CGRect)realTimeFrame {
    CALayer *presentation = self.layer.presentationLayer;
    return presentation ? presentation.frame : self.layer.frame;
}

这就是为什么在手势拖拽中可以 "拿到动画中控件的实时位置"——读的是 presentationLayer,不是 modelLayer。

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章的 Label 不刷新问题,七个疑问现在逐条作答:

疑问 答案
① UIView 和 CALayer 的关系? 第 2.1:UIView 持有 CALayer,前者管事件,后者管显示和合成
② layoutSubviews 什么时候调? 第 3.1 + 4.2:addSubview / frame改变 / 旋转 / UIScrollView滚动 / setNeedsLayout 后
③ setNeedsDisplay 不是立刻画,"立刻"指什么? 第 5.1:下一个 RunLoop 周期 CoreAnimation 提交事务时
④ CoreAnimation 事务何时提交? 第 6.2:RunLoop 的 BeforeWaiting 阶段,由内置 Observer 触发
⑤ Render Server 是什么? 第 6.3:独立进程 backboardd,负责所有 App 的图层合成和 GPU 调度
⑥ 离屏渲染为何影响性能?如何避免? 第 7 章:多一次纹理复制 + GPU 状态切换;圆角用 CPU 预裁剪图片代替 cornerRadius
⑦ drawRect 是 CPU 画还是 GPU 画? 第 5.2:CPU(CoreGraphics 软件光栅化),结果写入 layer.contents

四种修复方案(按实时性需求递进):

方案 A:接受默认行为(不修改)

刷新有几百毫秒延迟,但在绝大多数场景下不可感知。如果你的 Label 不需要严格跟视频帧同步——不要动。

方案 B:用 CATransaction flush 强制立刻提交

dispatch_source_set_event_handler(timer, ^{
    self.currentTime += 0.1;
    self.timeLabel.text = [NSString stringWithFormat:@"%.1f s", self.currentTime];
    [CATransaction flush];   // ← 立刻提交渲染
});

代价:破坏 CoreAnimation 的合批优化,频繁调用会增加 CPU 开销。

方案 C:用 displayLayer: 绕过 setNeedsDisplay 延迟

// 重写 CALayer delegate 方法,手动设置 contents
- (void)displayLayer:(CALayer *)layer {
    UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
                                          initWithSize:layer.bounds.size];
    UIImage *img = [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
        NSString *text = [NSString stringWithFormat:@"%.1f s", self.currentTime];
        [text drawAtPoint:CGPointZero withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:14]}];
    }];
    layer.contents = (__bridge id)img.CGImage;   // 不走 drawRect,不依赖事务提交时机
}

代价:需要手动管理文字渲染(没有 UILabel 的排版能力)。

方案 D:打上 CATransaction 时间戳 + 选择性 flush

@property (nonatomic, assign) CFTimeInterval lastFlushTime;

dispatch_source_set_event_handler(timer, ^{
    self.currentTime += 0.1;
    self.timeLabel.text = [NSString stringWithFormat:@"%.1f s", self.currentTime];
    
    CFTimeInterval now = CACurrentMediaTime();
    if (now - self.lastFlushTime > 1.0 / 30.0) {   // 限制 flush 频率不超过 30 FPS
        [CATransaction flush];
        self.lastFlushTime = now;
    }
});

代价:略复杂,但平衡了实时性和 CPU 开销。推荐方案。

# 10.2 一个像素的一生

把 self.timeLabel.text = @"1.0 s" 到像素亮起来的全过程串成知识树:

self.timeLabel.text = @"1.0 s";
        │
        ├─ UILabel 内部:
        │   ├── 更新 _text 属性
        │   ├── 计算 intrinsicContentSize(如有约束依赖)
        │   └── [self setNeedsDisplay]  ← 标记 CALayer 脏
        │
        ├─ 等待 RunLoop 一个周期结束
        │   └── RunLoop BeforeWaiting
        │
        ├─ CoreAnimation Observer 触发 ────────────── 第 6.2 节
        │   └── CATransaction::commit()
        │       │
        │       ├── 收集脏 Layer → 构建 render tree
        │       │
        │       ├── Layout 阶段 ──────────────────── 第 4 章
        │       │   └── 如有 setNeedsLayout → layoutSubviews
        │       │
        │       ├── Display 阶段 ─────────────────── 第 5 章
        │       │   ├── CALayer.display
        │       │   ├── UIView.displayLayer:(作为 delegate)
        │       │   ├── drawRect: → CoreGraphics CPU 绘制
        │       │   └── 生成 CGImageRef → layer.contents
        │       │
        │       └── 打包 render tree → IPC → Render Server
        │
        ├─ Render Server(backboardd 进程)─────── 第 6.3 节
        │   ├── 收到 render tree
        │   ├── 按 Z-order 排列所有图层(含系统 UI)
        │   ├── 离屏渲染检测 ──────────────────── 第 7 章
        │   │   ├── 有 cornerRadius + masksToBounds?
        │   │   ├── 有 shadow?
        │   │   └── 有 mask?
        │   │
        │   ├── 将图层树翻译为 OpenGL / Metal 指令
        │   └── 发送指令到 GPU
        │
        └─ GPU ────────────────────────────────────────
            ├── Vertex Shader:处理顶点坐标
            ├── Rasterization:把矢量转为像素
            ├── Fragment Shader:计算每个像素的最终颜色
            │   ├── 纹理采样(layer.contents 位图)
            │   ├── alpha 混合(如果有透明图层)
            │   └── 离屏渲染(如果需要)
            │
            ├── 写入 Framebuffer
            └── 显示控制器扫描 Framebuffer → 屏幕亮起 🖥

每一步都对应专栏的一篇——理解一个像素的一生,就是理解 iOS 渲染的全部。

# 10.3 设计哲学回扣

整理本篇的四条跨篇适用的设计哲学:

哲学 1:分层抽象——View 管交互,Layer 管显示

UIView 和 CALayer 的双重身份不是 "重复设计",而是 把 "交互关注" 和 "渲染关注" 彻底解耦。这种分离让 iOS 的图形栈可以在不引入 UI 依赖的前提下运行(比如 ARKit 的纯 Layer 渲染管线)。分层的本质是让每层做自己最擅长的事。

哲学 2:按需标记、统合提交——setNeedsDisplay 的 "懒惰" 是智能

setNeedsDisplay 只在 CALayer 上设置一个脏标记,不立刻画——因为下一个 RunLoop 周期内可能还有 10 次 setNeedsDisplay 要被合并。苹果选择 "等到事务提交时再一口气画"——在用户感知不到延迟的前提下,最大化 CPU/GPU 的合批效率。这是典型的 "延迟物化" 思想。

哲学 3:进程外合成——用进程边界保护渲染安全

Render Server 用独立进程 (backboardd) 做图层合成——这不是为了 "微内核",而是为了把渲染失败隔离在渲染进程内部。一个 App 崩溃了,它的位图还在 Render Server 缓存里,显示不受影响。iOS 进程模型的 "多进程安全" 在图形栈上体现得淋漓尽致。

哲学 4:GPU 贵在 "一次过"——离屏渲染是默认关闭的道路

GPU 的批量渲染是它的核心优势——一次状态设置,画一万个三角形。离屏渲染(切换渲染目标 → 画 → 切换回 → 贴纹理)打破了批量优势,相当于 "在高速公路上强行掉头"。苹果默认让 99% 的渲染走 on-screen——只有显式要求(cornerRadius+masksToBounds / shadow / mask)才触发支路。这是 "默认快速、例外可慢" 的设计。

# 10.4 渲染速查表

概念 核心要点 调试工具
UIView 生命周期 init → addSubview → willMove/didMove → layoutSubviews → drawRect → dealloc lldb 断点
CALayer 背板 UIView 持有 CALayer,Layer 负责显示和合成 view.layer
frame vs bounds frame=在父视图中的位置,bounds=自身坐标系 po view.frame
setNeedsDisplay 标记脏,不立刻画——RunLoop BeforeWaiting 才提交绘制 —
drawRect CPU 绘制,CoreGraphics API,生成 layer.contents 位图 Time Profiler
CoreAnimation 管线 Commit → Layout → Display → Render,事务在 RunLoop BeforeWaiting 提交 Core Animation instrument
Render Server backboardd 进程,App 的图层树通过 IPC 发给它 ps aux \| grep backboardd
离屏渲染 GPU 先画到临时缓冲区再贴回 → 多一次纹理复制 Color Off-screen Rendered (黄色)
图层混合 透明图层触发 alpha 混合 → GPU 需读回 Framebuffer Color Blended Layers (红色)
shouldRasterize 快照缓存,适合静态复杂图层 Core Animation instrument
presentationLayer 动画中间帧的屏幕实际显示值 layer.presentationLayer

60 秒诊断命令:

// 1. 看是否有离屏渲染(模拟器)
// Simulator → Debug → Color Off-screen Rendered

// 2. 看图层混合
// Simulator → Debug → Color Blended Layers

// 3. 看图层是否发生了光栅化
// Simulator → Debug → Color Hits Green and Misses Red

// 4. 打印视图层级
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]

// 5. 看某个 layer 的 contents 大小
(lldb) po [NSValue valueWithCGSize:self.view.layer.frame.size]
// CGSize * 屏幕 scale * 4 字节 = contents 占用内存

// 6. 抓帧分析(Instruments → Core Animation → 录制)

drawRect 要不要用的判断:

需要动态绘制的图形(图表、签名、手写轨迹)?
  ├─ 是 → drawRect: ✅
  └─ 否 → 组合 UIView / CALayer + 图片 ✅(避免 CPU 绘制)
            └─ 还需要高性能?
                  ├─ 是 → 用 CALayer 直接操作(不创建 UIView)
                  └─ 否 → 用 UIImageView(硬件解码,无需 CPU 参与)

下一篇:你已经知道 UIView 怎么布局和绘制、像素怎么上屏,下一章进入 05.事件响应与手势识别 (opens new window)——把 "手指点下去的一瞬间,hitTest 怎么遍历视图树、手势识别器怎么跟 RunLoop 抢主线程" 彻底剖开。

上次更新: 2026/06/28, 17:55:19
RunLoop运行循环揭秘
事件响应与手势识别

← RunLoop运行循环揭秘 事件响应与手势识别→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式