iOS:使用 UIView 的 'drawRect:' 与其层的委托 'drawLayer:inContext:'

Posted

技术标签:

【中文标题】iOS:使用 UIView 的 \'drawRect:\' 与其层的委托 \'drawLayer:inContext:\'【英文标题】:iOS: Using UIView's 'drawRect:' vs. its layer's delegate 'drawLayer:inContext:'iOS:使用 UIView 的 'drawRect:' 与其层的委托 'drawLayer:inContext:' 【发布时间】:2011-02-12 16:39:56 【问题描述】:

我有一个类是UIView 的子类。我可以通过实现drawRect 方法或通过实现drawLayer:inContext:CALayer 的委托方法)在视图中绘制内容。

我有两个问题:

    如何决定使用哪种方法?每个都有一个用例吗?

    如果我实现drawLayer:inContext:,它就会被调用(而drawRect 不是,至少就放置断点而言),即使我没有将我的视图分配为CALayer 委托使用:

    [[self layer] setDelegate:self];

    如果我的实例未定义为层的委托,怎么会调用委托方法?如果drawLayer:inContext: 被调用,什么机制可以防止drawRect 被调用?

【问题讨论】:

【参考方案1】:

如何决定使用哪种方法?每个都有用例吗?

始终使用drawRect:,并且永远不要使用UIView 作为任何CALayer 的绘图委托。

如果我的实例未定义为层的委托,怎么会调用委托方法?如果调用drawLayer:inContext:,什么机制可以防止调用drawRect?

每个UIView 实例都是其支持CALayer 的绘图委托。这就是为什么[[self layer] setDelegate:self]; 似乎什么也没做。这是多余的。 drawRect: 方法实际上是视图层的绘图委托方法。在内部,UIView 实现了drawLayer:inContext:,它自己做一些事情,然后调用drawRect:。你可以在调试器中看到:

这就是为什么在您实现drawLayer:inContext: 时从未调用过drawRect:。这也是为什么您永远不应该在自定义UIView 子类中实现任何CALayer 绘图委托方法。您也不应该将任何视图作为另一层的绘图委托。这会导致各种古怪。

如果您因为需要访问CGContextRef 而实现drawLayer:inContext:,则可以通过调用UIGraphicsGetCurrentContext()drawRect: 内部获取。

【讨论】:

我将它作为遵循 Apple 的代码示例 AccelerometerGraph 的一部分来实现。我猜想在那个代码示例中,他们实现了drawLayer:inContext:,因为除了视图的根层之外,还有一些层添加到层层次结构中,而且实际上委托不是UIView 实例。 developer.apple.com/library/ios/#DOCUMENTATION/WindowsViews/… " 还有其他方法可以提供视图的内容,例如直接设置底层的内容,但覆盖 drawRect: 方法是最常用的技术。" 嗨,Nathan,我对 iOS 中的自定义绘图非常陌生。我有一个子类 UIView 类,我需要在 3 个不同的图层上相互绘制。我想做第一层,在上面画点东西,然后放第二层,然后画第三层。如何从我拥有的 UIView 子类中完成它? 这不是正确的答案。只要有可能,我们应该使用 CAlayer 而不是覆盖 drawRect 以避免潜在的性能问题。请参阅 Apple 的 WWDC 2012 视频 - iOS 应用程序性能:图形和动画(链接到站点 developer.apple.com/videos/wwdc/2012)。从 11.40 查看,Apple 工程师专门解决了这种情况。 Nathan,我评论了您的声明“始终使用 drawRect:”。这是不正确的。 Apple 建议使用 CALayer 或 drawLayer:inContext ,除非绝对需要覆盖 drawRect。详细信息在 WWDC 2012 视频中。您是正确的,通过覆盖 drawRect (但不是唯一的解决方案)可以轻松实现某些情况,例如矢量绘图。然而,在大多数情况下,CALayer 提供了优于 drawRect 的性能优势。因此,答案应该是“逐案”。【参考方案2】:

drawRect 只应在绝对需要时实施。 drawRect 的默认实现包括许多智能优化,例如智能缓存视图的渲染。覆盖它会绕过所有这些优化。那很糟。有效地使用图层绘制方法几乎总是优于自定义drawRect。 Apple 经常使用UIView 作为CALayer 的代表——事实上,每个UIView is the delegate of it's layer。您可以在几个 Apple 示例中了解如何在 UIView 中自定义图层绘图,包括(目前)ZoomingPDFViewer。

虽然drawRect 的使用很常见,但至少从 IIRC 的 2002/2003 年起,这种做法就已不鼓励使用。走这条路的理由不多了。

Advanced Performance Optimization on iPhone OS(幻灯片 15)

Core Animation Essentials

Understanding UIKit Rendering

Technical Q&A QA1708: Improving Image Drawing Performance on iOS

View Programming Guide: Optimizing View Drawing

【讨论】:

这个答案与@NathanEror 接受的答案相矛盾。在我看来,要避免 drawRect 的原因是每次刷新视图时都会调用它,即使没有真正改变;如果没有drawRect,那么Core Animation 使用层的现有后备存储,这可能是之前绘制的。但我非常困惑,需要一个官方的 Apple 序列图......有任何指示吗? 参见 WWDC 2012 会议“iOS 应用性能:图形和动画”。这可能与您正在寻找的东西一样接近。 developer.apple.com/videos/wwdc/2012 天哪,这是一个很棒的演示文稿!我刚升级...我能感觉到我的生命值和法力值在增加!【参考方案3】:

这里是来自 Apple 的示例 ZoomingPDFViewer 的代码:

-(void)drawRect:(CGRect)r


    // UIView uses the existence of -drawRect: to determine if it should allow its CALayer
    // to be invalidated, which would then lead to the layer creating a backing store and
    // -drawLayer:inContext: being called.
    // By implementing an empty -drawRect: method, we allow UIKit to continue to implement
    // this logic, while doing our real drawing work inside of -drawLayer:inContext:



-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context

    ...

【讨论】:

如果有人无法真正弄清楚,注释的意思是它可能会编译出整个 drawRect 方法,如果它定义但没有任何内容,所以它与不实现 drawRect 相同所有,他们只是插入了它,所以他们可以解释为什么你不使用它。对我来说有点困惑,但我注释掉了整个空的 drawRect 方法,并且程序仍然可以正常工作(ZoomingPDFViewer) No... 注释意味着 IF -drawRect: 存在,UIKit 的行为不同(即 UIKit 会查看视图是否响应到选择器 drawRect:,并且基于此,它的 CALayer 可能会或可能不会失效) .【参考方案4】:

您是否使用drawLayer(_:inContext:)drawRect(_:)(或两者)用于自定义绘图代码,取决于您是否需要在动画制作时访问图层属性的当前值。

我今天在实现my own Label class 时遇到了与这两个函数相关的各种渲染问题。在查看文档、反复试验、反编译 UIKit 并检查 Apple 的 Custom Animatable Properties example 之后,我对它的工作原理有了很好的了解。

drawRect(_:)

如果您不需要在动画期间访问图层/视图属性的当前值,您可以简单地使用drawRect(_:) 来执行您的自定义绘图。一切都会好起来的。

override func drawRect(rect: CGRect) 
    // your custom drawing code

drawLayer(_:inContext:)

假设您想在自定义绘图代码中使用backgroundColor

override func drawRect(rect: CGRect) 
    let colorForCustomDrawing = self.layer.backgroundColor
    // your custom drawing code

当您测试您的代码时,您会注意到backgroundColor 在动画运行时不会返回正确的(即当前)值。相反,它返回最终值(即动画完成时的值)。

为了在动画期间获取当前值,您必须访问传递给drawLayer(_:inContext:)layer 参数backgroundColor。而且您还必须绘制到context 参数

知道视图的self.layer 和传递给drawLayer(_:inContext:)layer 参数并不总是同一层非常重要!后者可能是前者的副本,其中部分动画已应用于其属性。 这样您就可以访问飞行中动画的正确属性值。

现在绘图按预期工作:

override func drawLayer(layer: CALayer, inContext context: CGContext) 
    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code

但是有两个新问题:setNeedsDisplay()backgroundColoropaque 等几个属性不再适用于您的视图。 UIView 不再将调用和更改转发到它自己的层。

setNeedsDisplay() 只有在你的视图实现了drawRect(_:) 时才会做某事。函数是否真的做某事并不重要,但 UIKit 使用它来确定您是否进行自定义绘图。

这些属性可能不再起作用,因为不再调用 UIView 自己的 drawLayer(_:inContext:) 实现。

所以解决方案非常简单。只需调用drawLayer(_:inContext:) 的超类实现并实现一个空的drawRect(_:)

override func drawLayer(layer: CALayer, inContext context: CGContext) 
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code



override func drawRect(rect: CGRect) 
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.


总结

使用drawRect(_:),只要您不存在动画期间属性返回错误值的问题:

override func drawRect(rect: CGRect) 
    // your custom drawing code

如果您需要在动画制作时访问视图/图层属性的当前值,请使用drawLayer(_:inContext:) drawRect(_:)

override func drawLayer(layer: CALayer, inContext context: CGContext) 
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code



override func drawRect(rect: CGRect) 
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.

【讨论】:

【参考方案5】:

在 iOS 上,视图与其层之间的重叠非常大。默认情况下,视图是其层的委托,并实现层的drawLayer:inContext: 方法。据我了解,drawRect:drawLayer:inContext: 在这种情况下或多或少是等效的。可能drawLayer:inContext: 的默认实现会调用drawRect:,或者drawRect: 仅在您的子类未实现drawLayer:inContext: 时才会调用。

如何决定使用哪种方法?每个都有用例吗?

这并不重要。为了遵守约定,我通常会使用drawRect:,而当我实际上必须绘制不属于视图的自定义子层时,我会保留使用drawLayer:inContext:

【讨论】:

你在几秒钟内就击败了我。 :-P @Nathan:但你的回答比我的好。 嗨 Ole,我对 iOS 中的自定义绘图非常陌生。我有一个子类 UIView 类,我需要在 3 个不同的图层上相互绘制。我想做第一层,在上面画点东西,然后放第二层,然后画第三层。如何从我拥有的 UIView 子类中完成它? 这两个函数有不同的用途。在下面查看我的答案:***.com/a/36050120/1183577【参考方案6】:

Apple Documentation 有这样的说法:“还有其他方法可以提供视图的内容,例如直接设置底层的内容,但覆盖 drawRect: 方法是最常用的技术。”

但它没有涉及任何细节,所以这应该是一个线索:除非你真的想弄脏你的手,否则不要这样做。

UIView 层的委托指向 UIView。但是,根据是否实现 drawRect:,UIView 的行为会有所不同。例如,如果您直接在图层上设置属性(例如其背景颜色或角半径),如果您有一个 drawRect: 方法,这些值将被覆盖 - 即使它完全为空(即甚至不调用 super)。

【讨论】:

以上是关于iOS:使用 UIView 的 'drawRect:' 与其层的委托 'drawLayer:inContext:'的主要内容,如果未能解决你的问题,请参考以下文章

iOS重绘机制drawRect

iOS UIView drawRect 调整大小动画

iOS 简单的使用drawRect 绘制

iOS8 问题 - UIView drawRect 未被调用

iOS drawRect的作用和调用机制

drawRect 绘制的内容是不是能够使用 UIView 动画制作动画?