setNeedsDisplay():方法的本质和 iOS 中的重绘机制

作者: IT Sectr 发布日期: 2026-07-20 阅读时间: 6 分钟

setNeedsDisplay() — 是 UIView 的实例方法,它将视图标记为需要重新绘制。与直接调用 draw(_:) 不同,setNeedsDisplay() 仅为视图设置 dirty 标志并立即返回控制权。系统在最近的渲染周期中处理此请求,从而可以将多个调用合并为一次绘制操作。根据 Apple Documentation (2025),通过 setNeedsDisplay() 延迟重绘是更新自定义 UIView 内容的唯一正确方式。

要点

  • setNeedsDisplay() — UIView 延迟重绘请求,不同步调用 draw(_:)
  • dirty 标志 — 视图的内部标志,由 setNeedsDisplay() 设置并在 draw(_:) 后重置
  • setNeedsDisplay(_:) — 带 CGRect 的重载,仅部分重绘指定区域
  • displayIfNeeded() — 强制同步重绘,用于立即更新
  • 渲染周期 — 系统在 run loop 结束时处理 dirty 视图,将多个请求合并为一次遍历

什么是 setNeedsDisplay()

setNeedsDisplay() — 是一种 UIView 方法,向系统发出信号,表示视图内容已过时,需要重新渲染。该方法不接受参数并返回 void。调用后,视图被标记为 dirty,在最近的绘制周期中,系统会为此视图调用 draw(_:)。

关键特性是 异步性。对同一视图的多个连续 setNeedsDisplay() 调用不会导致多次 draw(_:) 调用 — 系统将其合并为一次操作。这对性能至关重要:如果视图属性在一帧中多次更改,draw(_:) 仅调用一次。

延迟重绘如何工作

延迟重绘的机制基于 UIKit 的渲染周期。当调用 setNeedsDisplay() 时,系统将内部标志 layer.needsDisplay 设置为 true。在当前 run loop 结束时,系统遍历所有 dirty 图层并为每个图层调用 draw(_:)。

Run loop 和渲染步骤

UIKit run loop 包含 update 阶段,在此阶段处理 dirty 视图。在此阶段,必要时调用 layoutSubviews(),然后对所有标记的视图调用 draw(_:)。绘制完成后,结果通过 Core Animation 传递到 GPU 进行合成。

swift
class ProgressView: UIView {
    var progress: CGFloat = 0 {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let ctx = UIGraphicsGetCurrentContext() else { return }

        // 背景
        ctx.setFillColor(UIColor.lightGray.cgColor)
        ctx.fill(rect)

        // 进度条
        let fillRect = CGRect(x: rect.minX, y: rect.minY,
                              width: rect.width * progress, height: rect.height)
        ctx.setFillColor(UIColor.systemGreen.cgColor)
        ctx.fill(fillRect)
    }
}

在此示例中,progressView 更新 progress,每个 setter 自动调用 setNeedsDisplay()。系统将等待 run loop 结束并使用当前的 progress 值调用 draw(_:)。如果 progress 在一帧中更改了三次,draw(_:) 仅执行一次 — 使用最后一个值。

setNeedsDisplay() vs setNeedsDisplay(_:)

setNeedsDisplay(_:) 方法接受 rect 参数(CGRect 类型),用于限制重绘区域。这允许仅重绘视图的更改部分,而不是整个视图。在处理大型视图且更改涉及小区域时,性能差异变得显著。

特性setNeedsDisplay()setNeedsDisplay(_:)
参数CGRect — 重绘区域
重绘区域整个视图 bounds仅指定矩形
性能大型视图较低局部更改较高
draw(_:) 中的 rect 参数整个 bounds指定矩形
使用时机整个内容更改点状更改(光标、高亮)

对于复杂图形,建议先在 offscreen 上下文中绘制整个场景,然后仅对更改的区域调用 setNeedsDisplay(_:)。这是 iOS 上绘图应用程序和图形编辑器的标准技术。

displayIfNeeded() 对比 setNeedsDisplay()

displayIfNeeded() — 是一种同步方法,立即启动层次结构中所有 dirty 视图的重绘,无需等待 run loop 结束。与仅设置标志的 setNeedsDisplay() 不同,displayIfNeeded() 强制立即执行 draw(_:)。

Apple 建议仅在极端情况下使用 displayIfNeeded(),当绘制结果需要立即获取时 — 例如,在创建屏幕截图之前或为 UIGraphicsImageRenderer 同步生成图像时。在常规场景中,通过 setNeedsDisplay() 的异步模型更可取,因为它不会阻塞主线程。

swift
    // 异步 — 推荐
someView.setNeedsDisplay()

    // 同步 — 仅在必要时
someView.setNeedsDisplay()
someView.displayIfNeeded() // draw(_:) 立即调用

没有事先调用 setNeedsDisplay() 而直接调用 displayIfNeeded() 不会产生效果 — 该方法仅处理已标记为 dirty 的视图。如果没有视图需要重绘,displayIfNeeded() 将在不调用 draw(_:) 的情况下结束。

优化 setNeedsDisplay() 调用

频繁调用 setNeedsDisplay() 可能会降低性能,尤其是在动画期间。让我们看看主要的优化策略。

通过模型合并更改

不要在每次单独属性更改时调用 setNeedsDisplay(),而是累积更改并在应用所有更新后一次性调用该方法。例如,在更改元素的颜色、大小和位置时 — 在设置所有属性后调用一次 setNeedsDisplay()。

使用 CALayer 处理可动画属性

如果属性可以通过 CALayer 进行动画处理(backgroundColor、opacity、position、transform),请使用 Core Animation 而不是 draw(_:)。CALayer 属性在 GPU 上更新,无需调用 draw(_:),可在不增加 CPU 负载的情况下提供 60 FPS。

通过 CATransaction 批量更新

要将多个更改分组到一次渲染遍历中,请使用 CATransaction。这允许在事务结束时将多个 setNeedsDisplay() 合并为一次 draw(_:) 操作。

swift
CATransaction.begin()
CATransaction.setDisableActions(true)

view1.setNeedsDisplay()
view2.setNeedsDisplay()
view3.setNeedsDisplay()

CATransaction.commit()

不要滥用 displayIfNeeded() — 同步重绘会阻塞主线程,可能导致丢帧。仅在对绘制结果有严格要求且需要 in 当前 run loop 结束前完成的操作中使用。

常见问题

可以直接调用 draw(_:) 而不是 setNeedsDisplay() 吗?

不可以,直接调用 draw(_:) 是被禁止的。setNeedsDisplay() 正确地将视图标记为 dirty,系统会在渲染周期的适当时刻调用 draw(_:)。直接调用 draw(_:) 会忽略缓存机制,可能导致状态不一致。

多次调用 setNeedsDisplay() 会发生什么?

对同一视图的多次 setNeedsDisplay() 调用会被系统合并为一次 draw(_:) 调用。这是因为该方法仅设置 dirty 标志,该标志仅在 run loop 结束时实际执行 draw(_:) 后才重置。

setNeedsDisplay() 和 setNeedsLayout() 有何不同?

setNeedsDisplay() 请求重绘视图内容(调用 draw(_:))。setNeedsLayout() 请求重新布局子视图(调用 layoutSubviews())。layoutSubviews() 可能导致尺寸变化,进而可能触发重绘。

何时使用 setNeedsDisplay(_:)?

setNeedsDisplay(_:) 应在点状更改时使用:光标移动、选定区域颜色更改、图表部分更新。这限制了重绘区域,与完全重绘整个视图相比提高了性能。

更改 frame 会自动调用 setNeedsDisplay() 吗?

默认情况下,更改 frame 或 bounds 不会调用 setNeedsDisplay()。系统仅移动视图。如果需要在尺寸更改时重绘,请将 contentMode 属性设置为 .redraw — 这样 UIKit 会在每次 bounds 更改时自动调用 setNeedsDisplay()。

总结

  • setNeedsDisplay() — 通过设置 dirty 标志请求延迟重绘的 UIView 异步方法
  • setNeedsDisplay(_:) — 带 CGRect 的重载,仅部分重绘指定区域,优化性能
  • displayIfNeeded() — 用于立即重绘 dirty 视图的同步方法,仅在极必要时使用
  • Run loop — 系统在当前周期结束时处理 dirty 视图,将多个调用合并为一次 draw(_:) 遍历
  • CALayer — 对于可动画属性(position、opacity),使用 Core Animation 而不是 draw(_:) — 效率高得多
  • CATransaction — 将多个更改分组到一个渲染周期中,减少 draw(_:) 调用次数
  • contentMode = .redraw — 在视图 bounds 更改时启用自动调用 setNeedsDisplay()

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读