4

I don't really understand how CALayer's display and drawInContext relate to drawRect in the view.

If I have an NSTimer that sets the [self.view setNeedsDisplay] every 1 second, then drawRect is called every 1 second, as shown by an NSLog statement inside of drawRect.

But if I subclass a CALayer and use that for the view, if I make the display method empty, then now drawRect is never called. Update: But display is called every 1 second, as shown by an NSLog statement.

If I remove that empty display method and add an empty drawInContext method, again, drawRect is never called. Update: But drawInContext is called every 1 second, as shown by an NSLog statement.

What is exactly happening? It seems that display can selectively call drawInContext and drawInContext can selectively somehow call drawRect (how?), but what is the real situation here?


Update: there is more clue to the answer:

I changed the CoolLayer.m code to the following:

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

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

So, let's say, if there is a moon (as a circle drawn by Core Graphics) at location (100,100) in the View, and now I change it to location (200,200), naturally, I will call [self.view setNeedsDisplay], and now, CALayer will have no cache at all for the new view image, as my drawRect dictates how the moon should now be displayed.

Even so, the entry point is CALayer's display, and then CALayer's drawInContext: If I set a break point at drawRect, the call stack shows:

enter image description here

So we can see that CoolLayer's display is entered first, and it goes to CALayer's display, and then CoolLayer's drawInContext, and then CALayer's drawInContext, even though in this situation, no such cache exist for the new image.

Then finally, CALayer's drawInContext calls the delegate's drawLayer:InContext. The delegate is the view (FooView or UIView)... and drawLayer:InContext is the default implementation in UIView (as I did not override it). It is finally that drawLayer:InContext calls drawRect.

So I am guessing two points: why does it enter CALayer even though there is no cache for the image? Because through this mechanism, the image is drawn in the context, and finally returns to display, and the CGImage is created from this context, and then it is now set as the new image cached. This is how CALayer caches images.

Another thing I am not quite sure is: if [self.view setNeedsDisplay] always trigger drawRect to be called, then when can a cached image in CALayer be used? Could it be... on Mac OS X, when another window covers up a window, and now the top window is moved away. Now we don't need to call drawRect to redraw everything, but can use the cached image in the CALayer. Or on iOS, if we stop the app, do something else, and come back to the app, then the cached image can be used, instead of calling drawRect. But how to distinguish these two types of "dirty"? One is a "unknown dirty" -- that the moon needs to be redrawn as dictated by the drawRect logic (it can use a random number there for the coordinate too). The other types of dirty is that it was covered up or made to disappear, and now needs to be re-shown.

nonopolarity
  • 146,324
  • 131
  • 460
  • 740
Jeremy L
  • 3,770
  • 6
  • 41
  • 62

3 Answers3

13

When a layer needs to be displayed and has no valid backing store (perhaps because the layer received a setNeedsDisplay message), the system sends the display message to the layer.

The -[CALayer display] method looks roughly like this:

- (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;
}

So, if you override display, none of that happens unless you call [super display]. And if you implement displayLayer: in FooView, you have to create your own CGImage somehow and store it in the layer's contents property.

The -[CALayer drawInContext:] method looks roughly like this:

- (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];
        }
    }
}

The onDraw action is not documented as far as I know.

The -[UIView drawLayer:inContext:] method looks roughly like this:

- (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);
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • by the way, how do you know? do you read the source code or just making a guess? – nonopolarity Sep 01 '12 at 09:18
  • 5
    I used Hopper to disassemble the IOS simulator libraries. – rob mayoff Sep 01 '12 at 13:43
  • Actually, don't need to disassemble. All of above now can read from apple document: [CALayer display](https://developer.apple.com/documentation/quartzcore/calayer/1410926-display?language=objc). But still appreciate this question and answer ! – Bernard Jun 14 '22 at 03:34
1

The update procedure of UIView is based on the dirty state meaning the view is not likely to be redrawn if there's no change at it's appearance.

That is internal implementation mentioned at the developer reference.

A-Live
  • 8,904
  • 2
  • 39
  • 74
  • how does CALayer come into play? – Jeremy L May 28 '12 at 22:32
  • It's explicitly mentioned at the documentation, that `setNeedsDisplay` works only for UIKit and Core Graphics drawings, i believe the dirty-views update is valid for both of them. Be aware of that is not valid for `CAEAGLLayer` as the documentation says. – A-Live May 28 '12 at 22:37
  • the question is if we use `setNeedsDisplay`, but override CALayer's method, then `drawRect` is not called, and why? – Jeremy L May 28 '12 at 22:44
  • calling `setNeedsDisplay` makes the `drawRect ` not `always`(!) been called - even if you don't override it. That's how the NSView drawing is optimized – A-Live May 28 '12 at 22:46
  • I thought if we call `setNeedsDisplay`, then `drawRect` will usually be called -- or else, if you are writing a game, and the objects moved (say it is a circle, not a subview), and you ask the view to be redrawn, you call `setNeedsDisplay`. If `drawRect` is not called, then how can the game work? – Jeremy L May 28 '12 at 22:50
  • That's correct, we call `setNeedsDisplay` when we expect the view to be redrawn and it does (not immediately, but does) if say UIKit finds the view 'dirty'. If the view is not 'dirty' - there's no need to spend expensive mobile resources to draw the same image, that is actually a very powerful optimization. Or do you say your layer appearance is changed but not redrawn ? (btw, you don't need to delete the method from subclass, use something like `- (void)drawInContext:...{NSLog(@"");[super drawInContext:...];}` for a deeper debugging. – A-Live May 28 '12 at 22:59
  • What I am saying is, why if I make `display` or `drawInContext` totally empty (except just an NSLog statement), then `drawRect` is not called? or so, with the default implementation of `display` and `drawInContext`, how is `drawRect` called? – Jeremy L May 28 '12 at 23:31
  • I'm not able to answer your question as it refers to the proprietary part of the iOs, however i can recommend to read the Android dirty regions rendering notes here: http://developer.android.com/guide/topics/graphics/hardware-accel.html (Android Drawing Models). Hope it makes it clear that you can not gain the control over the whole drawing process and you need to stick to the recommended developer reference techniques instead until you want to perform a deeper Cocoa Touch research somehow. – A-Live May 28 '12 at 23:46
  • Android... hey that's a totally different world! – Jeremy L May 28 '12 at 23:54
  • Huh, whatever you say, you have the direction and the reference to read. I could give you the reference to the video processing engine notes: http://wiki.xbmc.org/index.php?title=Dirty_regions - enjoy. – A-Live May 29 '12 at 00:03
  • oh please see the update in the original question. the flow is pretty much clear... if there is no other details hidden – Jeremy L May 29 '12 at 00:12
  • Regarding the layer `display` call (assuming it's cache is outdated) it might be reasonable to redraw the view over the outdated cache, we also have no information about what really happens inside this method and it might carry another functionality in this case despite it's name (it's asked not to be called directly). Regarding two dirty-region types, i'd say there must be more than just two with respect not only to the dirty-kind but also to the source from where is is marked as dirty. Saying this, i don't have the exact answer to your questions, only recommendation to follow the references. – A-Live May 29 '12 at 00:42
0

Implementing a drawInContext or display or drawRect tells the OS which one you want called when the view is dirty (needsDisplay). Pick the one you want called for a dirty view and implement that, and don't put any code you depend on getting executed in the others.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153