在 iOS 上,setNeedsDisplay 真的不会导致 drawRect 被调用...除非 CALayer 的 display 或 drawInContext 最终调用 drawRect?

Posted

技术标签:

【中文标题】在 iOS 上,setNeedsDisplay 真的不会导致 drawRect 被调用...除非 CALayer 的 display 或 drawInContext 最终调用 drawRect?【英文标题】:On iOS, setNeedsDisplay really doesn't cause drawRect to be called... unless CALayer's display or drawInContext finally calls drawRect? 【发布时间】:2012-05-28 22:26:14 【问题描述】:

我不太明白 CALayer 的 displaydrawInContext 与视图中的 drawRect 有何关系。

如果我有一个每 1 秒设置一次 [self.view setNeedsDisplay] 的 NSTimer,则每 1 秒调用一次 drawRect,如 drawRect 内的 NSLog 语句所示。

但是,如果我将 CALayer 子类化并将其用于视图,如果我将 display 方法设为空,那么现在 drawRect 将永远不会被调用。 更新:display 每 1 秒调用一次,如 NSLog 语句所示。

如果我删除那个空的display 方法并添加一个空的drawInContext 方法,那么drawRect 将永远不会被调用。 更新:drawInContext 每 1 秒调用一次,如 NSLog 语句所示。

究竟发生了什么?似乎display 可以选择性地调用drawInContextdrawInContext 可以选择性地调用drawRect(如何?),但这里的真实情况是什么?


更新:答案有更多线索:

我将CoolLayer.m 代码更改为以下内容:

-(void) display 
    NSLog(@"In CoolLayer's display method");
    [super display];


-(void) drawInContext:(CGContextRef)ctx 
    NSLog(@"In CoolLayer's drawInContext method");
    [super drawInContext:ctx];

所以,假设在 View 的位置 (100,100) 有一个月亮(由 Core Graphics 绘制的圆圈),现在我将其更改为位置 (200,200),自然会调用 [self.view setNeedsDisplay] ,现在,CALayer 将完全没有缓存新视图图像,因为我的drawRect 指示现在应该如何显示月亮。

即便如此,入口点是CALayer的display,然后是CALayer的drawInContext:如果我在drawRect设置断点,调用堆栈显示:

所以我们可以看到先进入CoolLayer的display,然后进入CALayer的display,然后是CoolLayer的drawInContext,然后是CALayer的drawInContext,即使在这种情况下,没有这样的缓存存在新图像。

最后,CALayer 的drawInContext 调用代理的drawLayer:InContext。委托是视图(FooView 或 UIView)...drawLayer:InContext 是 UIView 中的默认实现(因为我没有覆盖它)。终于drawLayer:InContext调用drawRect了。

所以我猜测两点:为什么即使没有图像缓存,它也会进入CALayer?因为通过这种机制,图像是在上下文中绘制的,最后返回display,CGImage就是从这个上下文中创建出来的,然后现在设置为缓存的新图像。这就是 CALayer 缓存图像的方式。

我不太确定的另一件事是:如果[self.view setNeedsDisplay] 总是触发drawRect 被调用,那么什么时候可以使用CALayer 中的缓存图像?可能是......在Mac OS X上,当另一个窗口覆盖一个窗口时,现在顶部窗口被移开。现在我们不需要调用drawRect 来重绘所有内容,而是可以使用CALayer 中的缓存图像。或者在 ios 上,如果我们停止应用程序,执行其他操作,然后返回应用程序,则可以使用缓存的图像,而不是调用 drawRect。但是如何区分这两种“脏”呢?一个是“未知脏”——月亮需要按照drawRect 逻辑的要求重新绘制(它也可以使用随机数作为坐标)。其他类型的脏是被掩盖或消失,现在需要重新显示。

【问题讨论】:

【参考方案1】:

当需要显示图层并且没有有效的后备存储时(可能是因为图层收到了setNeedsDisplay 消息),系统会向图层发送display 消息。

-[CALayer display] 方法大致如下:

- (void)display 
    if ([self.delegate respondsToSelector:@selector(displayLayer:)]) 
        [[self.delegate retain] displayLayer:self];
        [self.delegate release];
        return;
    

    CABackingStoreRef backing = _backingStore;
    if (!backing) 
        backing = _backingStore = ... code here to create and configure
            the CABackingStore properly, given the layer size, isOpaque,
            contentScale, etc.
    

    CGContextRef gc = ... code here to create a CGContext that draws into backing,
        with the proper clip region
    ... also code to set up a bitmap in memory shared with the WindowServer process

    [self drawInContext:gc];
    self.contents = backing;

因此,如果您覆盖 display,除非您调用 [super display],否则不会发生任何事情。如果你在FooView 中实现displayLayer:,你必须以某种方式创建自己的CGImage 并将其存储在层的contents 属性中。

-[CALayer drawInContext:] 方法大致如下:

- (void)drawInContext:(CGContextRef)gc 
    if ([self.delegate respondsToSelector:@selector(drawLayer:inContext:)]) 
        [[self.delegate retain] drawLayer:self inContext:gc];
        [self.delegate release];
        return;
     else 
        CAAction *action = [self actionForKey:@"onDraw"];
        if (action) 
            NSDictionary *args = [NSDictionary dictionaryWithObject:gc forKey:@"context"];
            [action runActionForKey:@"onDraw" object:self arguments:args];
        
    

据我所知,onDraw 操作没有记录。

-[UIView drawLayer:inContext:] 方法大致如下:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)gc 
    set gc's stroke and fill color spaces to device RGB;
    UIGraphicsPushContext(gc);
    fill gc with the view's background color;
    if ([self respondsToSelector:@selector(drawRect:)]) 
        [self drawRect:CGContextGetClipBoundingBox(gc)];
    
    UIGraphicsPopContext(gc);

【讨论】:

对了,你怎么知道的?您是阅读源代码还是只是猜测? 我用Hopper反汇编IOS模拟器库。【参考方案2】:

UIView 的更新过程是基于dirty 状态的,这意味着如果它的外观没有变化,则视图不太可能被重绘。

这是开发者参考中提到的内部实现。

【讨论】:

CALayer 是如何发挥作用的? 在文档中明确提到,setNeedsDisplay 仅适用于 UIKit 和 Core Graphics 绘图,我相信脏视图更新对它们都有效。请注意,正如文档所述,这对 CAEAGLLayer 无效。 问题是如果我们使用setNeedsDisplay,但是重写了CALayer的方法,那么drawRect就没有被调用,为什么? 调用 setNeedsDisplay 使得 drawRect 不是 always(!) 被调用——即使你没有覆盖它。 NSView 绘图就是这样优化的 我想如果我们调用setNeedsDisplay,那么通常会调用drawRect——否则,如果你正在编写游戏,并且对象移动了(比如说它是一个圆圈,而不是一个子视图),并且您要求重绘视图,您调用setNeedsDisplay。如果drawRect没有被调用,那游戏怎么玩?【参考方案3】:

实现 drawInContext 或 display 或 drawRect 告诉操作系统当视图脏时你想要调用哪个(需要显示)。选择一个你想要调用脏视图的代码并实现它,不要将任何你依赖的代码放在其他代码中。

【讨论】:

以上是关于在 iOS 上,setNeedsDisplay 真的不会导致 drawRect 被调用...除非 CALayer 的 display 或 drawInContext 最终调用 drawRect?的主要内容,如果未能解决你的问题,请参考以下文章

iOS UIView常用的一些方法setNeedsDisplay和setNeedsLayout 区别

iOS开发:setNeedsLayOut和setNeedsDisplay区别

UICollectionView 在 UICollectionViewCells 上调用 setNeedsDisplay

添加子视图后 setNeedsDisplay 不起作用

cellForRowAtIndexPath:setNeedsDisplay 不适用于在后台下载的图像,即使在主线程上调用它也是如此

在 for 循环中调用 setNeedsDisplay