setNeedsDisplay() — 是 UIView 的实例方法,它将视图标记为需要重新绘制。与直接调用 draw(_:) 不同,setNeedsDisplay() 仅为视图设置 dirty 标志并立即返回控制权。系统在最近的渲染周期中处理此请求,从而可以将多个调用合并为一次绘制操作。根据 Apple Documentation (2025),通过 setNeedsDisplay() 延迟重绘是更新自定义 UIView 内容的唯一正确方式。
要点
setNeedsDisplay() — 是一种 UIView 方法,向系统发出信号,表示视图内容已过时,需要重新渲染。该方法不接受参数并返回 void。调用后,视图被标记为 dirty,在最近的绘制周期中,系统会为此视图调用 draw(_:)。
关键特性是 异步性。对同一视图的多个连续 setNeedsDisplay() 调用不会导致多次 draw(_:) 调用 — 系统将其合并为一次操作。这对性能至关重要:如果视图属性在一帧中多次更改,draw(_:) 仅调用一次。
延迟重绘的机制基于 UIKit 的渲染周期。当调用 setNeedsDisplay() 时,系统将内部标志 layer.needsDisplay 设置为 true。在当前 run loop 结束时,系统遍历所有 dirty 图层并为每个图层调用 draw(_:)。
UIKit run loop 包含 update 阶段,在此阶段处理 dirty 视图。在此阶段,必要时调用 layoutSubviews(),然后对所有标记的视图调用 draw(_:)。绘制完成后,结果通过 Core Animation 传递到 GPU 进行合成。
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(_:) 方法接受 rect 参数(CGRect 类型),用于限制重绘区域。这允许仅重绘视图的更改部分,而不是整个视图。在处理大型视图且更改涉及小区域时,性能差异变得显著。
| 特性 | setNeedsDisplay() | setNeedsDisplay(_:) |
|---|---|---|
| 参数 | 无 | CGRect — 重绘区域 |
| 重绘区域 | 整个视图 bounds | 仅指定矩形 |
| 性能 | 大型视图较低 | 局部更改较高 |
| draw(_:) 中的 rect 参数 | 整个 bounds | 指定矩形 |
| 使用时机 | 整个内容更改 | 点状更改(光标、高亮) |
对于复杂图形,建议先在 offscreen 上下文中绘制整个场景,然后仅对更改的区域调用 setNeedsDisplay(_:)。这是 iOS 上绘图应用程序和图形编辑器的标准技术。
displayIfNeeded() — 是一种同步方法,立即启动层次结构中所有 dirty 视图的重绘,无需等待 run loop 结束。与仅设置标志的 setNeedsDisplay() 不同,displayIfNeeded() 强制立即执行 draw(_:)。
Apple 建议仅在极端情况下使用 displayIfNeeded(),当绘制结果需要立即获取时 — 例如,在创建屏幕截图之前或为 UIGraphicsImageRenderer 同步生成图像时。在常规场景中,通过 setNeedsDisplay() 的异步模型更可取,因为它不会阻塞主线程。
// 异步 — 推荐
someView.setNeedsDisplay()
// 同步 — 仅在必要时
someView.setNeedsDisplay()
someView.displayIfNeeded() // draw(_:) 立即调用
没有事先调用 setNeedsDisplay() 而直接调用 displayIfNeeded() 不会产生效果 — 该方法仅处理已标记为 dirty 的视图。如果没有视图需要重绘,displayIfNeeded() 将在不调用 draw(_:) 的情况下结束。
频繁调用 setNeedsDisplay() 可能会降低性能,尤其是在动画期间。让我们看看主要的优化策略。
不要在每次单独属性更改时调用 setNeedsDisplay(),而是累积更改并在应用所有更新后一次性调用该方法。例如,在更改元素的颜色、大小和位置时 — 在设置所有属性后调用一次 setNeedsDisplay()。
如果属性可以通过 CALayer 进行动画处理(backgroundColor、opacity、position、transform),请使用 Core Animation 而不是 draw(_:)。CALayer 属性在 GPU 上更新,无需调用 draw(_:),可在不增加 CPU 负载的情况下提供 60 FPS。
要将多个更改分组到一次渲染遍历中,请使用 CATransaction。这允许在事务结束时将多个 setNeedsDisplay() 合并为一次 draw(_:) 操作。
CATransaction.begin()
CATransaction.setDisableActions(true)
view1.setNeedsDisplay()
view2.setNeedsDisplay()
view3.setNeedsDisplay()
CATransaction.commit()
不要滥用 displayIfNeeded() — 同步重绘会阻塞主线程,可能导致丢帧。仅在对绘制结果有严格要求且需要 in 当前 run loop 结束前完成的操作中使用。
常见问题
不可以,直接调用 draw(_:) 是被禁止的。setNeedsDisplay() 正确地将视图标记为 dirty,系统会在渲染周期的适当时刻调用 draw(_:)。直接调用 draw(_:) 会忽略缓存机制,可能导致状态不一致。
对同一视图的多次 setNeedsDisplay() 调用会被系统合并为一次 draw(_:) 调用。这是因为该方法仅设置 dirty 标志,该标志仅在 run loop 结束时实际执行 draw(_:) 后才重置。
setNeedsDisplay() 请求重绘视图内容(调用 draw(_:))。setNeedsLayout() 请求重新布局子视图(调用 layoutSubviews())。layoutSubviews() 可能导致尺寸变化,进而可能触发重绘。
setNeedsDisplay(_:) 应在点状更改时使用:光标移动、选定区域颜色更改、图表部分更新。这限制了重绘区域,与完全重绘整个视图相比提高了性能。
默认情况下,更改 frame 或 bounds 不会调用 setNeedsDisplay()。系统仅移动视图。如果需要在尺寸更改时重绘,请将 contentMode 属性设置为 .redraw — 这样 UIKit 会在每次 bounds 更改时自动调用 setNeedsDisplay()。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。