Вопрос

I have a question similar to this one:

CGLayerRef in NSValue - when to call retain() or release()?

I am drawing 24 circles as radial gradients in a view. To speed it up I am drawing the gradient into a layer and then drawing the layer 24 times. This worked really well to speed up the rendering. On subsequent drawRect calls some of the circles may need to be redrawn with a different hue, while others remain the same.

Every time through drawRect I recalculate a new gradient with the new hue and draw it into a new layer. I then loop through the circles, drawing them with the original layer/gradient or new layer/gradient as appropriate. I have a 24 element NSMutableArray that stores a CGLayerRef for each circle.

I think this is the answer provided in the question I linked above, however it is not working for me. The second time through drawRect, any circle that is drawn using the CGLayerRef that was stored in the array causes the program to crash when calling CGContextDrawLayerAtPoint. In the debugger I have verified that the actual hex value of the original CGLayerRef is stored properly into the array, and in the second time through drawRect that the same hex value is passed to CGContextDrawLayerAtPoint.

Further, I find that if I don't CGLayerRelease the layer then the program doesn't crash, it works fine. This tells me that something is going wrong with the memory management of the layer. It's my understanding that storing an object into an NSArray will increment it's reference count, and it won't be deallocated until the array releases it.

Anyway, here is the relevant code from drawRect. Down at the bottom you can see that I commented out CGLayerRelease. In this configuration the app doesn't crash although I think this is a resource leak. If I uncomment that release then the app crashes the second time though drawRect (between the first and second calls one of the circles has it's led_info.selected property cleared, indicating that it should use the saved layer rather than the new layer:

NSLog(@"ledView drawing hue=%4f sat=%4f num=%d size=%d",hue_slider_value,sat_slider_value,self.num_leds,self.led_size);
rgb_color = [UIColor colorWithHue:1.0 saturation:1.0 brightness:1.0 alpha:1.0];
end_color = [UIColor colorWithHue:1.0 saturation:1.0 brightness:1.0 alpha:0.0];
NSArray *colors = [NSArray arrayWithObjects:
                   (id)rgb_color.CGColor, (id)end_color.CGColor, nil];
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace,(__bridge CFArrayRef) colors, NULL);

CGLayerRef layer = CGLayerCreateWithContext(context, (CGSize){self.led_size,self.led_size}, /*auxiliaryInfo*/ NULL);
if (layer) {
    CGContextRef layer_context = CGLayerGetContext(layer);
    CGContextDrawRadialGradient(layer_context, gradient, led_ctr,self.led_size/8,led_ctr, self.led_size/2,kCGGradientDrawsBeforeStartLocation);
} else {
    NSLog(@"didn't get a layer");
}

for (int led=0;led<[self.led_info_array count];led++) {
    led_info=[self.led_info_array objectAtIndex:led];

// the first time through selected=1 and led_info.cg_layer=nil for all circles,
// so this branch is taken.
    if (led_info.selected || led_info.cg_layer==nil) {
        CGPoint startPoint=led_info.rect.origin;
        CGContextDrawLayerAtPoint(context, startPoint, layer);
        CGContextAddRect(context, led_info.rect);
        led_info.cg_layer=layer;

// the second time through drawRect one or more circles have been deselected.
// They take this path through the if/else
    } else {
        CGPoint startPoint=led_info.rect.origin;
// app crashes on this call to CGContextDrawLayerAtPoint
        CGContextDrawLayerAtPoint(context, startPoint, led_info.cg_layer);
    }

}

// with this commented out the app doesn't crash.
//CGLayerRelease(layer);

Here is the declaration of led_info:

@interface ledInfo : NSObject
@property CGFloat hue;
@property CGFloat saturation;
@property CGFloat brightness;
@property int selected;
@property CGRect rect;
@property CGPoint center;
@property unsigned index;
@property CGLayerRef cg_layer;
- (NSString *)description;
@end

led_info_array is the NSMutableArray of ledInfo objects, the array itself is a property of the view:

@interface ledView : UIView
@property float hue_slider_value;
@property float sat_slider_value;
@property unsigned num_leds;
@property unsigned led_size;
@property unsigned init_has_been_done;
@property NSMutableArray *led_info_array;
//@property layerPool *layer_pool;
@end

The array is initialized like this: self.led_info_array = [[NSMutableArray alloc] init];

Edit: since I posted I have found that if I put retain/release around the assignemt into the NSMutableArray then I can also leave in the original CGLayerRelease and the app works. So I guess this is how it is supposed to work, although I'd like to know why the retain/release is necessary. In the objective C book I am reading (and the answer to the question linked above) I thought assigning into NSArray implicitly did retain/release. The new working code looks like this:

    if (led_info.selected || led_info.cg_layer==nil) {
        CGPoint startPoint=led_info.rect.origin;
        CGContextDrawLayerAtPoint(context, startPoint, layer);
        CGContextAddRect(context, led_info.rect);
        if (led_info.cg_layer) CGLayerRelease(led_info.cg_layer);
        led_info.cg_layer=layer;
        CGLayerRetain(layer);
    } else {
        CGPoint startPoint=led_info.rect.origin;
        CGContextDrawLayerAtPoint(context, startPoint, led_info.cg_layer);
    }

You can probably tell that I'm brand new to Objective C and iOS programming, and I realize that I'm not really sticking to convention regarding case and probably other things. I'll clean that up but right now I want to solve this memory management problem.

Rob, thanks for the help. I could use a little further clarification. I think from what you are saying that there are two problems:

1) Reference counting doesn't work with CGLayerRef. OK, but it would be nice to know that while writing code rather than after debugging. What is my indication that when using "things" in Objective C/cocoa that resource counting doesn't work?

2) You say that I'm storing to a property, not an NSArray. True, but the destination of the store is the NSArray via the property, which is a pointer. The value does make it into the array and back out. Does resource counting not work like this? ie instead of CGLayerRef, if I were storing some NSObject into NSArray using the code above would resource counting work? If not, then would getting rid of the intermediate led_info property and accessing the array directly from within the loop work?

Это было полезно?

Решение

You're not storing the layer directly in an NSArray. You're storing it in a property of your ledInfo object.

The problem is that a CGLayer is not really an Objective-C object, so neither ARC nor the compiler-generated (“synthesized”) property setter will take care of retaining and releasing it for you. Suppose you do this:

CGLayerRef layer = CGLayerCreateWithContext(...);
led_info.cg_layer = layer;
CGLayerRelease(layer);

The cg_layer setter method generated by the compiler just stores the pointer in an instance variable and nothing else, because CGLayerRef isn't an Objective-C object reference. So when you then release the layer, its reference count goes to zero and it's deallocated. Now you have a dangling pointer in your cg_layer property, and when you use it later you crash.

The fix is to write the setter manually, like this:

- (void)setCg_layer:(CGLayerRef)layer {
    CGLayerRetain(layer);
    CGLayerRelease(_cg_layer);
    _cg_layer = layer;
}

Note that it's important to retain the new value before releasing the old one. If you release the old one before retaining the new one, and the new one happens to be the same as the old one, you might deallocate the layer right in the middle!

UPDATE

In response to your edits:

  1. Reference counting works with CGLayerRef. Automatic reference counting (ARC) doesn't. ARC only works with things that it thinks are Objective-C objects ARC does not automatically retain and release a CGLayerRef, because ARC doesn't think a CGLayerRef is a reference to an Objective-C object. An Objective-C object is (generally speaking) an instance of a class declared with @interface, or a block.

    The CGLayer Reference says that CGLayer is derived from CFType, the basic type for all Core Foundation objects. (As far as ARC is concerned, a Core Foundation object is not an Objective-C object.) You need to read about “Ownership Policy” and “ Core Foundation Object Lifecycle Management” in the Memory Management Programming Guide for Core Foundation.

  2. The “destination of the store” is an instance variable in your ledInfo object. It's not “the NSArray via the property”. The value doesn't ”make it into the array and back out.” The array gets a pointer to your ledInfo object. The array retains and releases the ledInfo object. The array never sees or does anything with the CGLayerRef. Your ledInfo object is responsible for retaining and releasing any Core Foundation objects it wants to own, like the layer in its cg_layer property.

    As I mentioned, if ledInfo doesn't retain the layer (with CFRetain or CGLayerRetain) in its cg_layer setter, it risks the layer being deallocated, leaving the ledInfo with a dangling pointer. Do you understand what a dangling pointer is?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top