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

4
我不太理解CALayer的display和drawInContext如何与视图中的drawRect相关。
如果我有一个NSTimer,每1秒钟设置一次[self.view setNeedsDisplay],那么drawRect将每1秒钟被调用一次,这可以通过在drawRect内部使用NSLog语句来显示。
但是,如果我子类化一个CALayer并将其用于视图,则如果我使display方法为空,则现在drawRect将永远不会被调用。更新:但是display每1秒钟被调用一次,这可以通过NSLog语句来显示。
如果我删除该空的display方法并添加一个空的drawInContext方法,则再次不会调用drawRect。更新:但是,drawInContext每1秒钟被调用一次,这可以通过NSLog语句来显示。
到底发生了什么?看起来display可以有选择地调用drawInContext,而drawInContext可以以某种方式有选择地调用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];
}

假设视图中有一个由Core Graphics绘制的圆形(作为月亮)位于(100,100)位置,现在我将其更改为(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逻辑重新绘制月亮(也可以在那里使用随机数进行坐标)。另一种脏状态是它被覆盖或消失了,现在需要重新显示。
3个回答

13

当需要显示一个图层且该图层没有有效的后备存储(可能是因为该图层收到了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);
}

顺便问一下,你是怎么知道的?你读了源代码还是猜的? - nonopolarity
5
我使用Hopper反汇编IOS模拟器库。 - rob mayoff
其实不需要反汇编。现在所有的内容都可以从苹果文档中阅读:CALayer display。但仍然感谢这个问题和答案! - Bernard

1
UIView的更新过程基于“dirty”状态,这意味着如果外观没有改变,视图不太可能被重新绘制。
这是开发者参考文档中提到的内部实现。

CALayer 如何发挥作用? - Jeremy L
文档明确提到,setNeedsDisplay 仅适用于 UIKit 和 Core Graphics 绘图,我相信脏视图更新对它们两者都有效。请注意,根据文档,它不适用于 CAEAGLLayer - A-Live
问题是,如果我们使用 setNeedsDisplay,但覆盖了 CALayer 的方法,那么为什么 drawRect 不会被调用? - Jeremy L
我认为如果我们调用 setNeedsDisplay,那么通常会调用 drawRect -- 或者,如果你正在编写一个游戏,并且对象移动了(比如说是一个圆形,而不是子视图),并且你要求视图重新绘制,你就调用 setNeedsDisplay。如果 drawRect 没有被调用,那么游戏怎么能正常工作呢? - Jeremy L
没错,当我们期望视图被重新绘制时,我们调用 setNeedsDisplay 方法,如果 UIKit 发现视图“脏了”,它会在稍后(不是立即)进行重绘。如果视图没有“脏掉”——就没有必要浪费昂贵的移动资源来绘制相同的图像,这实际上是一种非常强大的优化。或者你是说你的层外观已经改变但没有重绘?(顺便说一句,你不需要从子类中删除该方法,可以使用类似 - (void)drawInContext:...{NSLog(@"");[super drawInContext:...];} 的方法进行更深入的调试。) - A-Live
显示剩余7条评论

0

实现drawInContext或display或drawRect告诉操作系统在视图需要重绘(needsDisplay)时调用哪个方法。选择你想要在脏视图中调用的方法并实现它,不要将任何依赖于其他方法执行的代码放入其中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接