draw(_:) 和 drawRect — 什么是它,调用和重写

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

draw(_:) / drawRect — 是UIView类的方法(在Swift中)及其在Objective-C中的类似方法(drawRect:),负责使用Core Graphics绘制视图的内容。系统在视图首次出现在屏幕上时以及调用setNeedsDisplay()后自动调用此方法。根据Apple Documentation(2025),draw(_:) 是开发者可以访问当前屏幕图形上下文进行自定义绘制的唯一方法。重写draw(_:)可以完全控制组件的外观——从简单的几何图形到复杂的动画图形。

要点

  • draw(_:) — 通过Core Graphics进行自定义绘制的UIView方法,不由开发者直接调用
  • drawRect: — Objective-C中draw(_:)的等效方法,语法不同,功能相同
  • CGContext — draw(_:)内部可用于所有绘制操作的图形上下文
  • setNeedsDisplay() — 开发者请求再次调用draw(_:)的唯一正确方式
  • UIGraphicsGetCurrentContext() — 在draw(_:)内部获取当前上下文的函数,绘制前的必需步骤

什么是 draw(_:) / drawRect

draw(_:) — 是 UIKit 调用的 UIView 实例方法,用于渲染视图的内容。在此方法内部,开发者可以访问 CGContext 图形上下文,并使用 Core Graphics API 绘制线条、填充、文本和图像。Objective-C 中的 drawRect: 执行相同的功能,但语法不同:唯一的参数是指定重绘区域的 CGRect。

根据 Apple Engineering(2024),draw(_:) 通过 CPU 渲染位图图形上下文工作,这提供了最大的灵活性,但与 CALayer 相比需要更多资源。是否使用 draw(_:) 的决定基于图形的复杂性和性能要求。

Swift 和 Objective-C 中的方法签名

在 Swift 中,该方法声明为 override func draw(_ rect: CGRect),其中 rect 是需要重绘的矩形。在 Objective-C 中,签名是 - (void)drawRect:(CGRect)rect。在通过 setNeedsDisplay(_:) 进行部分重绘时,rect 参数可能小于视图的 bounds。

objective-c
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextFillRect(context, rect);
}

系统何时调用 draw(_:)

系统在严格定义的场景下自动调用 draw(_:)。理解这些触发器有助于避免不必要的重绘并优化视图性能。下面列出了该方法的所有自动调用情况。

  • 首次渲染 — 当视图首次添加到层次结构并在屏幕上可见时
  • setNeedsDisplay() — 调用此方法后,系统在最近的绘制周期中
  • setNeedsDisplay(_:) — 相同,但指定了要重绘的具体矩形
  • contentMode — 当 bounds 改变,且 contentMode 需要重绘时(例如 .redraw)
  • setNeedsLayout() — 在某些情况下,重新排列子视图后可能需要重绘

Apple 文档警告:切勿直接调用 draw(_:)。系统自行决定何时执行绘制,直接调用会破坏内部缓存机制。要请求重绘,始终使用 setNeedsDisplay()setNeedsDisplay(_:)

如何在 Swift 中重写 draw(_:)

在 Swift 中重写 draw(_:) 从获取图形上下文和随后的 Core Graphics 调用开始。建议为逻辑绘制块创建独立的方法——这可以提高代码的可读性和可测试性。

swift
override func draw(_ rect: CGRect) {
    super.draw(rect)

    guard let context = UIGraphicsGetCurrentContext() else { return }

    // 线条参数
    context.setStrokeColor(UIColor.darkGray.cgColor)
    context.setLineWidth(2.0)

    // 绘制三角形
    context.move(to: CGPoint(x: rect.midX, y: rect.minY + 10))
    context.addLine(to: CGPoint(x: rect.maxX - 10, y: rect.maxY - 10))
    context.addLine(to: CGPoint(x: rect.minX + 10, y: rect.maxY - 10))
    context.closePath()
    context.strokePath()
}

在此示例中,draw(_:) 绘制一个三角形,带有 2 像素厚的深灰色轮廓。Apple 建议在方法开头调用 super.draw(rect) 以保留父级的绘制逻辑,尽管 UIView 中 draw(_:) 的默认实现是空的。

draw(_:) 的幂等性规则

每次调用 draw(_:) 在相同输入数据下应产生相同的结果。这允许系统缓存结果,并在视图内容未更改时不再调用 draw(_:)。不要在 draw(_:) 内部使用随机值、系统时间或网络请求。

Objective-C 中的 drawRect: 与 draw(_:) 的区别

drawRect: — 历史上是该方法的第一个版本,随 Objective-C 一起在 iOS 2.0 中出现。在 Swift 中,该方法被重命名为 draw(_:),使用外部参数 _。功能上这些方法是相同的:两者都接收重绘区域的 CGRect,并使用 UIGraphicsGetCurrentContext() 访问图形上下文。

特性draw(_:) (Swift)drawRect: (Objective-C)
签名override func draw(_ rect: CGRect)- (void)drawRect:(CGRect)rect
调用 supersuper.draw(rect)[super drawRect:rect]
上下文UIGraphicsGetCurrentContext()UIGraphicsGetCurrentContext()
参数 rectrect: CGRectCGRect rect
性能相同相同

在将项目从 Objective-C 迁移到 Swift 时,重命名方法是首要任务之一。Xcode 提供自动转换器,但 drawRect: 需要手动更新为 draw(_:)。根据 Apple(2024),Swift 版本的 draw(_:) 更适合新项目。

优化 draw(_:) 中的绘制

draw(_:) 在 CPU 上执行,非优化的实现可能导致帧率下降和性能低下。Apple 推荐几种经过验证的方法来加速绘制。

最小化绘制操作的数量

每个 Core Graphics 操作(move(to:)、addLine(to:)、strokePath)都有开销。将操作分组,对复杂形状使用 CGPath — 路径创建一次,在每次 draw(_:) 调用时重复使用。

使用 UIBezierPath 处理矢量对象

UIBezierPath — 是围绕 CGPath 的 Objective-C 封装,提供用于创建形状的简单 API。提前创建 UIBezierPath(例如在初始化器中),然后在 draw(_:) 内部调用 fill()stroke()

swift
private let starPath: UIBezierPath = {
    let path = UIBezierPath()
    // 构建星形
    path.move(to: CGPoint(x: 50, y: 0))
    for i in 1...5 {
        let angle = CGFloat(i) * 4 * CGFloat.pi / 5
        path.addLine(to: CGPoint(x: 50 + 40 * cos(angle),
                               y: 50 + 40 * sin(angle)))
    }
    path.close()
    return path
}()

override func draw(_ rect: CGRect) {
    UIColor.systemYellow.setFill()
    starPath.fill()
}

使用 draw(_:) 时的常见错误

开发人员在重写 draw(_:) 时经常犯同样的错误。了解这些模式有助于避免错误和性能下降。让我们看看最常见的问题及其解决方案。

  • 直接调用 draw(_:) — 切勿直接调用 draw(_:)。使用 setNeedsDisplay() 请求重绘。直接调用会破坏缓存,并可能导致显示不正确。
  • draw(_:) 内部的重计算 — draw(_:) 应尽可能轻量。在方法外部创建 UIBezierPath、图像和其他重量级对象,一次性初始化。
  • 在 draw(_:) 内部创建对象 — 在 draw(_:) 内部构造 UIColor、UIFont 和 UIGraphicsImageRenderer 会创建额外的内存负载。将对象创建移到类属性中。
  • 忽略 rect 参数 — rect 指示需要重绘的区域。在 rect 之外绘制会被系统拒绝,但会消耗资源。在绘制之前检查与 rect 的交集。
  • 缺少 super.draw(rect) — 尽管 UIView 的实现是空的,Apple 建议调用 super.draw(rect) 以兼容未来的 UIKit 更改。

遵循这些规则,您将确保在任何 iOS 项目中稳定快速地绘制自定义视图。通过 Instruments(Core Animation)分析 draw(_:) 以查看实际执行时间和瓶颈。

常见问题

可以直接调用 draw(_:) 吗?

不可以,直接调用 draw(_:) 被 Apple 文档禁止。系统自行管理绘制周期。要请求重绘,请使用 setNeedsDisplay(),它会在最近的渲染周期中正确标记视图需要更新。

drawRect: 与 draw(_:) 有何不同?

功能上这些方法是相同的。drawRect: 用于 Objective-C,draw(_:) 用于 Swift。两者都接收重绘区域的 CGRect,并通过 UIGraphicsGetCurrentContext() 使用相同的 Core Graphics 上下文。

为什么空的 UIView 不会调用 draw(_:)?

Apple 优化了渲染:如果 UIView 没有重写 draw(_:),系统不会为其创建位图上下文。这可以节省内存。如果存在重写但方法未被调用——请检查视图的 frame 是否为零以及视图是否可见。

系统多久调用一次 draw(_:)?

仅在需要时:首次渲染、setNeedsDisplay() 之后、bounds 改变且 contentMode = .redraw 时。在静态状态下,draw(_:) 不会被再次调用,从而节省 CPU 和电池资源。

在 Swift 中需要调用 super.draw(rect) 吗?

Apple 建议在重写方法开头调用 super.draw(rect)。尽管 UIView 的当前实现是空的,但调用 super 可以确保与未来 UIKit 版本的兼容性,并且是一种良好的实践。

总结

  • draw(_:) / drawRect — Swift 和 Objective-C 中 UIView 的主要绘制方法,由系统自动调用
  • CGContext — 通过 UIGraphicsGetCurrentContext() 在 draw(_:) 内部可用的 Core Graphics 图形上下文
  • setNeedsDisplay() — 请求重绘的正确方法;禁止直接调用 draw(_:)
  • 幂等性 — 为了正确缓存,draw(_:) 在相同输入下应产生相同的结果
  • UIBezierPath — 在 draw(_:) 外部创建路径,以最小化每次调用的 CPU 负载
  • 参数 rect — 包含重绘区域;使用交集进行优化——不要在其边界之外绘制
  • 分析 — 通过 Instruments Core Animation 检查 draw(_:) 的性能以检测慢操作

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

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

讨论项目

另请阅读