CADisplayLink与帧渲染

16 阅读7分钟

为什么CADisplaylink的回调会影响ui刷新呢?回调里有UI渲染相关代码?

CADisplayLink 的回调本身并没有直接的 UI 渲染代码,但它是iOS 帧渲染的 “触发开关” 和 “时间锚点” ,Core Animation 会紧跟在 CADisplayLink 回调执行后,自动启动一帧的渲染流程,二者是强时序绑定的关系,这也是回调能驱动 UI 刷新的关键。

简单说结论:CADisplayLink 的回调是系统给开发者的 “刷新时机通知” ,回调内可做 UI 更新的最后准备工作(如修改布局参数、更新动画进度),回调执行完毕后,Runloop 会立即进入 Core Animation 的渲染阶段,自动完成本次的 UI 刷新;如果跳过这个回调,帧渲染的触发开关就不会被打开,本次 VSync 周期的 UI 刷新就会被跳过。

下面从底层机制执行流程两层讲清楚,为什么这个 “无渲染代码的回调” 能决定 UI 刷新:

一、先明确:CADisplayLink 的设计定位 ——VSync 信号的 “上层通知器”

CADisplayLink 是 Core Animation 对硬件 VSync 信号封装层,它的核心设计目的不是 “执行渲染”,而是:

  1. 精准对齐屏幕刷新率:让开发者的代码能在每一次屏幕刷新前的瞬间执行(60Hz 下 16.67ms 一次),保证 UI 更新和屏幕刷新同步,避免画面撕裂 / 跳帧
  2. 触发系统的渲染流程:将硬件级的 VSync 信号转化为Runloop 可识别的事件,并通过 “回调执行→渲染启动” 的时序,让 Core Animation 的渲染工作紧跟 VSync 信号执行
  3. 给开发者预留刷新时机:回调内是本次屏幕刷新前修改 UI 的最后窗口期,开发者可在此时做轻量的 UI 更新操作(如动画进度更新progress = 0.5、视图位置微调),这些修改会被立刻纳入本次的渲染流程。

核心设计逻辑硬件 VSync 信号 → 触发 CADisplayLink 回调 → 回调执行完毕 → 系统自动启动渲染,这个链路是 Core Animation 硬编码的,开发者无法修改。

二、核心机制:CADisplayLink 回调与帧渲染的强时序绑定流程

结合主线程 Runloop 的执行阶段,一次 VSync 周期内,CADisplayLink 回调 + 帧渲染的完整执行链路如下(60Hz 为例,这整个过程都在同一个 Runloop 循环内完成):

【硬件层】屏幕产生VSync信号 → 【系统层】Core Animation捕获信号,投递CADisplayLink事件到主线程Runloop
↓
【Runloop阶段1】唤醒主线程,取出CADisplayLink事件,**执行回调block**(开发者编写的代码,无系统渲染逻辑)
- 开发者可在回调内做:修改UI属性、更新动画进度、标记`setNeedsLayout/setNeedsDisplay`等
- 注意:回调内的UI修改依然是**标记性的**,不会立即渲染,只是更新了CALayer的待刷新状态
↓
【Runloop阶段2】CADisplayLink回调**执行完毕后,Runloop自动进入「Core Animation渲染阶段」**(硬编码的时序)
- 布局(Layout):计算所有待刷新CALayer的frame/bounds/constraints
- 绘制(Display):执行`drawRect:`/`displayLayer:`,生成图层内容
- 提交(Commit):将图层树提交到GPU,完成图层合成
- 显示(Present):GPU将合成后的帧数据写入屏幕缓冲区,完成一帧显示
↓
【Runloop阶段3】渲染完成,主线程无新任务则进入休眠,等待下一次VSync信号

关键节点:

  1. 回调是渲染的 “前置条件” :只有 CADisplayLink 回调成功执行完毕,Runloop 才会触发后续的渲染阶段;如果回调被阻塞 / 丢弃,渲染阶段也会被同步阻塞 / 丢弃;
  2. 回调内的 UI 修改会被 “实时纳入” 渲染:因为回调和渲染在同一个 Runloop 循环内,回调内修改的 UI 属性(如view.alpha = 0.8)会被 Core Animation 立即检测到,无需等待下一次循环,这保证了 UI 更新的时效性
  3. 渲染是系统自动执行的:开发者无需在回调内调用任何渲染接口(如[UIView render]),Core Animation 会在回调后自动扫描所有待刷新的 CALayer,完成渲染全流程。

三、再解答:如果回调里什么都不写,会触发 UI 刷新吗?

会的!只要 CADisplayLink 的回调被执行,无论回调内是否有开发者代码,系统都会在回调后执行渲染阶段

  • 如果回调内无 UI 修改:Core Animation 扫描后发现没有待刷新的 CALayer,会跳过实际的渲染操作(布局 / 绘制 / 合成),直接进入休眠,这一步几乎无性能消耗;
  • 如果回调内有 UI 修改:Core Animation 会基于修改后的 CALayer 状态,完成一帧的完整渲染。

这也印证了:回调本身不决定 “是否渲染”,只决定 “何时触发渲染检查” ,而 “是否真的刷新 UI”,取决于是否有待处理的 CATransaction/UI 更新标记

四、反证:如果 CADisplayLink 回调被阻塞 / 丢弃,会发生什么?

如果主线程阻塞导致 CADisplayLink 回调无法执行(或被系统丢弃),那么Runloop 的渲染阶段就不会被触发,哪怕此时有大量的 UI 更新标记(如setNeedsDisplay),也无法完成渲染 —— 因为渲染阶段的 “启动开关” 就是 CADisplayLink 的回调执行完毕

举个极端例子:你在主线程执行一个死循环,此时屏幕依然产生 VSync 信号,但 CADisplayLink 回调永远无法执行,Runloop 的渲染阶段也永远不会启动,哪怕你之前调用了view.text = @"新内容",这个 UI 更新也永远不会显示在屏幕上,直到主线程阻塞解除。

五、补充:开发者在 CADisplayLink 回调里该做 / 不该做什么?

正因为回调是渲染前的最后窗口期,且和渲染共享同一个 16.67ms 的 VSync 周期,所以回调内的代码执行效率直接影响帧渲染:

✅ 该做的(轻量操作,耗时 < 1ms):

  • 更新动画进度(如CGFloat progress = link.timestamp / totalTime);
  • 微调 UI 属性(如视图的位置、透明度、缩放);
  • 标记轻量的setNeedsLayout/setNeedsDisplay
  • 做动画的状态判断(如是否完成、是否需要暂停)。

❌ 不该做的(耗时操作,绝对禁止):

  • 复杂的计算(如大量循环、数据解析);
  • 同步网络请求 / 文件读写;
  • 重绘大量视图(如调用[view drawRect:]、加载高清大图);
  • 新增 / 删除大量视图(如[self.view addSubview:xxx]循环)。

原因:CADisplayLink 回调 + 帧渲染的总耗时必须 < 16.67ms(60Hz),如果回调内的代码耗时过长,会直接挤压渲染阶段的时间,导致渲染超时,最终引发掉帧 / 卡顿

六、最终核心逻辑梳理

  1. CADisplayLink 回调无系统级的 UI 渲染代码,但它是 Core Animation 帧渲染的唯一触发开关
  2. 回调与渲染是同一 Runloop 循环内的强时序绑定回调执行完毕→渲染阶段启动,这是 Core Animation 的硬编码逻辑;
  3. 回调的设计价值是对齐 VSync 信号+预留刷新窗口期,让开发者的 UI 修改能精准同步到屏幕刷新;
  4. 回调的执行状态(正常 / 阻塞 / 丢弃)直接决定渲染阶段的启动状态,这是它能 “影响 UI 刷新” 的底层原因。

简单类比:把屏幕刷新比作火车发车,VSync 信号是发车时间表,CADisplayLink 回调是发车前的检票环节,帧渲染是火车开出。检票环节本身不负责开车,但只有检票完成,火车才会按时间表发车;如果检票被阻塞,火车就会晚点 / 取消发车,哪怕乘客(UI 更新标记)已经到站,也无法出发。