主线程Runloop与帧渲染的关系

10 阅读6分钟

如何理解iOS里面runloop和帧渲染的关系,如果页面没有更新,runloop需要进行一次循环,才能渲染出一帧新的画面吗

要理解 iOS 中Runloop帧渲染的核心关系,核心结论先明确:页面无更新时,Runloop 不会触发帧渲染;只有当存在 UI 更新需求时,Runloop 的特定阶段才会驱动 Core Animation 完成一帧的渲染,且一次 Runloop 循环仅对应一帧渲染

简单来说:Runloop 是帧渲染的 “调度器” 和 “执行载体” ,帧渲染是 Runloop 在有 UI 任务时的核心执行动作之一,二者绑定在 iOS 的 UI 刷新机制中,且严格遵循屏幕刷新率(如 60Hz 下 16.67ms / 帧)。

一、先理清基础:Runloop 的核心作用与 UI 相关的运行模式

Runloop 的本质是一个无限循环的事件处理机制,核心作用是:有任务时唤醒执行,无任务时休眠,避免线程空转消耗资源。对 iOS UI 线程(主线程)而言,其 Runloop 是默认开启且永不退出的,且运行在 **NSDefaultRunLoopMode (默认模式,处理 UI 事件、刷新、定时器等),这是帧渲染的唯一载体 **(UI 渲染必须在主线程执行)。

主线程 Runloop 的简化循环逻辑:

while (1) {
    1. 休眠,等待事件唤醒(如UI触摸、UI更新标记、定时器、网络回调等);
    2. 唤醒后,处理所有待执行的事件/任务(如触摸事件、代理回调);
    3. 进入「UI刷新/渲染阶段」,检查是否有UI更新需求;
    4. 若有则执行帧渲染,若无则直接进入下一次循环;
    5. 处理收尾工作,再次休眠;
}

可见:帧渲染只是 Runloop 循环中的一个 “可选阶段” ,仅当有 UI 更新需求时才会执行。

二、核心机制:Runloop 如何驱动帧渲染(60Hz 为例)

iOS 的帧渲染由Core Animation框架主导,且与 Runloop 的特定阶段强绑定,整个流程受CADisplayLink(屏幕刷新定时器)和CATransaction(UI 事务)共同控制,完整链路如下:

步骤 1:标记 UI 更新,触发 CATransaction

当你执行UIKit 的更新操作(如view.frame = CGRect(...)label.text = @"xxx"[UIView animateWithDuration:...])时,UIKit 并不会立即渲染,而是会:

  1. 将本次 UI 修改封装到CATransaction(核心动画事务)中;
  2. 标记 **“有 UI 更新待处理” ,并将 CATransaction 加入主线程的事务队列 **;
  3. 若未手动开启事务,UIKit 会自动创建隐式事务,并在当前 Runloop 循环结束前提交(默认异步提交)。

步骤 2:CADisplayLink 唤醒 Runloop,触发事务提交

屏幕的CADisplayLink是一个与屏幕刷新率同步的定时器(60Hz 下 16.67ms 触发一次),它会定时唤醒主线程 Runloop,并触发两个关键动作:

  1. 提交所有待处理的CATransaction(隐式 / 显式),将 UI 修改的属性同步到CALayer(UIView 的底层渲染载体,UIKit 基于 Core Animation 封装,所有 UI 最终都由 CALayer 渲染);
  2. 让 Runloop 进入 「beforeWaiting(即将休眠)」阶段 —— 这是Runloop 驱动帧渲染的核心阶段

步骤 3:Runloop 的 beforeWaiting 阶段执行 “一帧渲染”

主线程 Runloop 的CFRunLoopRun源码中,beforeWaiting阶段是专门为UI 刷新 / 帧渲染设计的,此时 Core Animation 会完成一帧的完整渲染流程(即 iOS 的 “离屏渲染 / 屏幕渲染” 全流程):

  1. 布局(Layout) :计算所有需要更新的 CALayer 的位置、大小(如layoutSubviews);
  2. 绘制(Display) :绘制 CALayer 的内容(如drawRect:、图片解码);
  3. 合成(Commit) :将所有 CALayer 的渲染结果提交到GPU,由 GPU 完成图层合成;
  4. 显示(Present) :GPU 将合成后的帧数据发送到屏幕缓冲区,完成一帧的显示。

步骤 4:一次 Runloop 循环 = 一帧渲染,无更新则跳过

整个帧渲染流程在 Runloop 的一次beforeWaiting阶段内完成,且一次 Runloop 循环仅会执行一次帧渲染。如果本次 Runloop 唤醒后无任何 UI 更新标记(即 CATransaction 队列为空),Runloop 会直接跳过beforeWaiting阶段的渲染动作,进入休眠,不会产生任何帧数据。

三、回答核心问题:页面没有更新时,Runloop 会怎样?

页面无更新时,Runloop 依然会进行循环,但不会触发帧渲染,分两种场景:

  1. 无任何任务:主线程 Runloop 会休眠(占用资源极低),直到被外部事件唤醒(如用户触摸屏幕、网络回调、定时器触发),唤醒后处理完事件,因无 UI 更新,直接休眠,全程无渲染;
  2. 有非 UI 任务:如主线程执行网络数据解析、普通代码逻辑,Runloop 唤醒后处理完这些任务,检查无 UI 更新标记,同样跳过渲染阶段,进入下一次循环。

关键误区纠正:不是 “Runloop 循环一次才能渲染一帧”,而是 “有渲染需求时,一次 Runloop 循环会触发一帧渲染”—— 因果关系不能颠倒:UI 更新需求是因,Runloop 触发渲染是果,Runloop 循环只是执行载体

四、延伸:为什么主线程卡顿时,UI 会掉帧 / 卡死?

帧渲染的核心要求是:在 16.67ms 内完成一帧的所有渲染工作(60Hz)。而主线程 Runloop 是串行执行的 ——同一时间只能处理一个任务

如果主线程 Runloop 在处理非渲染任务时(如复杂的 for 循环、同步网络请求、大量数据解析)耗时超过 16.67ms,会导致:

  1. Runloop 无法及时进入beforeWaiting的渲染阶段;
  2. CADisplayLink 的屏幕刷新信号被阻塞,后续的帧渲染被推迟;
  3. 当阻塞时间过长时,会出现掉帧(跳过多帧)甚至卡死(Runloop 无法执行渲染,UI 完全无响应)。

这也是 iOS 开发中禁止主线程执行耗时操作的核心原因 —— 会阻塞 Runloop,进而阻断帧渲染。

五、关键总结:Runloop 与帧渲染的核心关系

  1. 载体唯一:UI 帧渲染只能在主线程 Runloop中执行,无主线程 Runloop 则无 UI 渲染;
  2. 触发有条件:Runloop 仅在 ** 存在 UI 更新标记(CATransaction)** 时,才会在beforeWaiting阶段驱动帧渲染,无更新则跳过;
  3. 一一对应一次 Runloop 循环仅对应一帧渲染,严格遵循屏幕刷新率,不会出现一次循环多帧渲染的情况;
  4. 调度与执行:Runloop 是帧渲染的调度器(由 CADisplayLink 定时唤醒),帧渲染是 Runloop 的可选执行动作(有 UI 任务时才执行);
  5. 串行依赖:Runloop 的串行执行特性决定了 —— 主线程的任何耗时任务都会阻塞帧渲染,导致掉帧 / 卡死。

六、补充:手动触发 UI 刷新的底层逻辑([UIView setNeedsDisplay]/[UIView setNeedsLayout]

我们常用的手动标记 UI 刷新的方法,本质是向 Runloop 发送 “UI 更新需求” 的信号

  • setNeedsLayout:仅标记布局需要更新,Runloop 渲染阶段会执行layoutSubviews

  • setNeedsDisplay:标记绘制需要更新,Runloop 渲染阶段会执行drawRect:/displayLayer:

  • 二者均不会立即触发渲染,而是等待下一次 Runloop 的渲染阶段统一执行,这是 iOS 的批量刷新优化(避免多次 UI 修改触发多次渲染,提升性能)。

    如果需要立即渲染(跳过批量优化),可手动提交 CATransaction:

[CATransaction begin]; [CATransaction setDisableActions:YES]; // 关闭动画
view.frame = CGRectMake(10, 10, 100, 100);
[CATransaction commit]; // 立即提交,触发当前Runloop的渲染阶段