Think in G

Never stop ThinkinG…

2 Stories about Memory Leak in Objective-C

with one comment

As we know, Objective-C uses reference counting as it’s memory management mechanism. It’s simple, reliable and efficient. But even the smart and powerful ARC, we still have to take care of each piece of code, or there might  be leaking somewhere, silently.

Here, I will talk about two cases of memory leak. They are all from my recent project. Although a veteran might think these as nothing. But I still want to share the stories.

Case 1: When Core Animation meets Delegate

To create an awesome UI/UX, using animations is inevitable. Sometimes, I have to use CAAnimation directly instead of the convenient methods provided by the UIView class. And I usually use delegate objects to make these animation objects more controllable.

@implementation GHKLeakingController
// These codes cause memory leak due to a retain cycle.
- (void)showMask {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    animation.fromValue = [self.maskView.layer.presentationLayer valueForKeyPath:@"opacity"];
    animation.toValue = [NSNumber numberWithFloat:1.0f];
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeBoth;
    animation.duration = 1.0f;
    animation.delegate = self;

    [self.maskView.layer addAnimation:animation forKey:@"fadeIn"];
}

//The delegate method for the animation
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag) {
        NSLog(@"%@", @"The animation is finished. Do something here.");
    }
}
@end

When these code runs, everything looks OK. But, when you drop this instance of the controller, everything leaks! WHY?
Let’s take a look at the object graph.

Pay attention to where marked (1) and (2).

  1. Because I want the layer to remain it’s look after the animation stops. I use a common approach of overriding the default behavior of animation.
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeBoth;
    

    By doing this, the layer will retain the animation until you remove it manually.

  2. I also make the controller’s instance itself as the delegate object of the animation. As you can see in the object graph, since then, the controller is retained by the animation. Don’t be surprised! Apple has already documented this behavior. Because this is a rare case of the delegate pattern, there even has a warning says:

    Important The delegate object is retained by the receiver. This is a rare exception to the memory management rules described in Advanced Memory Management Programming Guide.
    An instance of CAAnimation should not be set as a delegate of itself. Doing so (outside of a garbage-collected environment) will cause retain cycles.

Well, everything is clear now. I did both (1) and (2) at the same time, and didn’t realized that those objects had been connected as a big cycle. By manually removing the animation from the layer object in the delegate method, the cycle is broken.

//The delegate method for the animation
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag) {
        NSLog(@"%@", @"The animation is finished. Do something here.");
    }
    [self.mask.layer removeAnimationForKey:@"fadeIn"];
}

Case 2: Gesture Recognizer & Blocks

In order to intercept the touch events sent to an instance of MKMapView. I googled and finally found a solution. I created a customized gesture recognizer, in the -viewDidLoad method, I initialized a new instance of the recognizer and added it to the map view.

// WSTouchInterceptor.h

typedef void (^TouchesEventBlock)(UIGestureRecognizer * recognizer, NSSet * touches, UIEvent * event);

@interface WSTouchInterceptor : UIGestureRecognizer
@property (copy, nonatomic) TouchesEventBlock beginHandler;
@property (copy, nonatomic) TouchesEventBlock endHandler;
- (id)initWithTouchBeginHandler:(TouchesEventBlock)beginHandler touchEndHandler:(TouchesEventBlock)endHandler;
@end

 

// WSTouchInterceptor.m

@implementation WSTouchInterceptor
@synthesize beginHandler = _beginHandler;
@synthesize endHandler = _endHandler;

- (id)initWithTouchBeginHandler:(TouchesEventBlock)beginHandler touchEndHandler:(TouchesEventBlock)endHandler {
    self = [super init];
    if (self) {
        self.beginHandler = beginHandler;
        self.endHandler = endHandler;
    }

    return self;
}
//...
@end

 

@implementation GHKLeakingController
- (void)viewDidLoad
{
    [super viewDidLoad];

    //Initialize the event interceptor.
    UIGestureRecognizer *interceptor = nil;
    interceptor = [[WSTouchInterceptor alloc] initWithTouchBeginHandler:^(UIGestureRecognizer *recognizer, NSSet *touches, UIEvent *event) {
        [self handleTouchBegin];
    } touchEndHandler:^(UIGestureRecognizer *recognizer, NSSet *touches, UIEvent *event) {
        [self handleTouchEnd];
    }];
    [self.map addGestureRecognizer:interceptor];
}
@end

In the codes listed above. The event interceptor is initialized with two blocks. Each block references the “self” object. Of cause, the controller itself will be retained by the blocks. You already know what’s gonna happen, right? No?
Again, let’s check the object graph. It is slightly simple than the previous one.

Yes! The blocks cause the problem. According to the Apple’s official Transitioning to ARC Release Notes, the better solution is to break the cycle by passing a weak reference into the block.

@implementation GHKLeakingController
- (void)viewDidLoad
{
    [super viewDidLoad];

    //Initialize the event interceptor.
    UIGestureRecognizer *interceptor = nil;

    GHKLeakingController * __weak handler = self;
    interceptor = [[WSTouchInterceptor alloc] initWithTouchBeginHandler:^(UIGestureRecognizer *recognizer, NSSet *touches, UIEvent *event) {
        [handler handleTouchBegin];
    } touchEndHandler:^(UIGestureRecognizer *recognizer, NSSet *touches, UIEvent *event) {
        [handler handleTouchEnd];
    }];
    [self.map addGestureRecognizer:interceptor];
}
@end

Conclusion: What I’ve learned?

In the above examples, there won’t be any problem if the animation is not added to the layer or the gesture recognizer is not added to the map view. I mean when writing codes, I usually take care of that the current object is owning what, but there has been less consideration about it is(or will be) owned by whom. When I add those objects to some parent objects, there’s more likely chance to make a retain cycle.
When considering the ownership and the object graph, We have to look back as well as forward.

BTW: I found it’s really hard to find there is a retain cycle. If you have any suggestion about detecting the retain cycle, please tell me.

When I was writing this article, I found another article on this topic: Block Retain Loop. I think this is also worth reading.

Written by ghawk.gu

May 10th, 2012 at 12:45 am