Search code examples
ioscocoa-touchinstrumentsobjective-c-blocks

Instruments is reporting I'm leaking NSMallocBlocks (aka block object) but am I really?


I'm working on an app where different view controllers that get pushed and popped to/from a navigation controller can register CoreAnimation sequences and these sequences are then triggered at different times throughout the pushed controller's life time.

Instruments is reporting that every time I push a controller, I'm leaking block objects (32-byte leaks each time I push, for each animation block). However, I don't see where am I leaking. This is the relevant code:

There's a singleton AnimationFactory which has, among others, this method:

- (void)registerAnimationBlock:(int(^)(NSArray*, NSDictionary*))animationBlock forKey:(NSString*)key
{
  [self.animationBlocks setObject:[animationBlock copy] forKey:key];
  [animationBlock release];
}

And then, the different view controllers, when pushed to the navigation controller's stack, register their different animation sequences with code like this, for instance:

- (void)setupCommonAnimations
{
  int(^animationBlock)(NSArray*,NSDictionary*);

  /************************************************************************************************************************/
  //  Move stuff up
  /************************************************************************************************************************/
  animationBlock =
  ^(NSArray* layers, NSDictionary* parameters)
  {
    CGFloat timeOffset = [[parameters objectForKey:@"timeOffset"] floatValue];
    CABasicAnimation* a;

    CABasicAnimation* a2 = [CABasicAnimation animationWithKeyPath:@"opacity"];
    a2.fromValue = [NSNumber numberWithFloat:0.];
    a2.toValue = [NSNumber numberWithFloat:1.];

    CAAnimationGroup* g = [CAAnimationGroup animation];
    g.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    g.fillMode = kCAFillModeBoth;
    g.removedOnCompletion = NO;
    g.duration = .4;

    [CATransaction begin];
    [CATransaction setCompletionBlock:
     ^{
       for(CALayer *layer in layers)
         layer.opacity = 1;
     }];

    for(CALayer *layer in layers)
    {
      a = [CABasicAnimation animationWithKeyPath:@"position.y"];
      a.fromValue = [NSNumber numberWithFloat:1024 + layer.frame.size.height / 2];
      a.toValue = [NSNumber numberWithFloat:layer.frame.origin.y + layer.frame.size.height / 2];

      g.animations = [NSArray arrayWithObjects:a,a2, nil];
      g.beginTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil] + timeOffset;

      [layer addAnimation:g forKey:nil];

      timeOffset += .2;
    }

    [CATransaction commit];

    return 0;
  };

  [[AnimationFactory sharedFactory] registerAnimationBlock:animationBlock forKey:@"StuffUpAnimation"];

/************************************************************************************************************************/
  //  Sequential Fade-in
  /************************************************************************************************************************/
  animationBlock =
  ^(NSArray* layers, NSDictionary* parameters)
  {
    CGFloat timeOffset = [[parameters objectForKey:@"timeOffset"] floatValue];
    CABasicAnimation* a2 = [CABasicAnimation animationWithKeyPath:@"opacity"];
    a2.fromValue = [NSNumber numberWithFloat:0.];
    a2.toValue = [NSNumber numberWithFloat:1.];
    a2.duration = .4;
    a2.fillMode = kCAFillModeBoth;
    a2.removedOnCompletion = NO;

    [CATransaction begin];
    [CATransaction setCompletionBlock:
     ^{
       for(CALayer *layer in layers)
         layer.opacity = 1;
     }];

    for(CALayer *layer in layers)
    {
      a2.beginTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil] + timeOffset;
      [layer addAnimation:a2 forKey:nil];
      timeOffset += .4;
    }

    [CATransaction commit];

    return 0;
  };

  [[AnimationFactory sharedFactory] registerAnimationBlock:animationBlock forKey:@"SeqFadeInAnimation"];

The above method would be called on the view controller's init or viewWillAppear for instance. And I'm reusing the animationBlock variable for registering different animations.

Finally, when a controller gets popped, it calls the following as part of its dealloc sequence:

- (void)cleanupAnimations
{
  [[AnimationFactory sharedFactory] removeAnimationBlockForKey:@"SeqFadeInAnimation"];
  [[AnimationFactory sharedFactory] removeAnimationBlockForKey:@"StuffUpAnimation"];
}

According to Instruments, I leak every time I do:

[[AnimationFactory sharedFactory] registerAnimationBlock:animationBlock forKey:@"StuffUpAnimation"];

which, from the first code snippet, translates to:

[self.animationBlocks setObject:[animationBlock copy] forKey:key];
[animationBlock release];

As far as I understand:

  1. I'm creating a stack block in a view controller
  2. then passing it to the singleton, which creates a heap copy of it, stores it in a mutable dictionary and calls release to balance the increased retain count on the block.
  3. The original stack block should cease to exist once the view controller's method in which it was declared goes out of scope.
  4. I'm not even referencing anything from the block's enclosing scope, so it shouldn't be retaining self or any variable / object. it gets its stuff to work with from parameters at the time of execution.

So I don't' see where the leak is. Further, I'm removing the blocks from the animationBlocks array when the view controller is popped, but I shouldn't even need this (other than to reclaim the memory as soon as I'm done with it). Because next time the user causes the same view controller to get pushed, it'll re-register the animation block with the same key, and calling setObject:withKey with an existing key will cause a release being sent to the object hashed to that key and then set the new object to that location.

What am I overlooking?


Solution

  • You should be releasing the copy that you've made, not the original block. You need to do:

    animationBlock = [animationBlock copy];
    [self.animationBlocks setObject:animationBlock forKey:key];
    [animationBlock release];