Search code examples
objective-cxcodecore-animationautomatic-ref-countingexc-bad-access

Core Animation bad access on device


I'm trying to do a frame by frame animation with CAlayers. I'm doing this with this tutorial http://mysterycoconut.com/blog/2011/01/cag1/ but everything works with disable ARC, when I'm try to rewrite code with ARC, it's works on simulator perfectly but on device I got a bad access memory.

Layer Class interface

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface MCSpriteLayer : CALayer {
    unsigned int sampleIndex;
}

// SampleIndex needs to be > 0
@property (readwrite, nonatomic) unsigned int sampleIndex; 

// For use with sample rects set by the delegate
+ (id)layerWithImage:(CGImageRef)img;
- (id)initWithImage:(CGImageRef)img;

// If all samples are the same size 
+ (id)layerWithImage:(CGImageRef)img sampleSize:(CGSize)size;
- (id)initWithImage:(CGImageRef)img sampleSize:(CGSize)size;

// Use this method instead of sprite.sampleIndex to obtain the index currently displayed on screen
- (unsigned int)currentSampleIndex; 


@end

Layer Class implementation

@implementation MCSpriteLayer

@synthesize sampleIndex;

- (id)initWithImage:(CGImageRef)img;
{
    self = [super init];
    if (self != nil)
    {
        self.contents = (__bridge id)img;
        sampleIndex = 1;
    }

    return self;
}

+ (id)layerWithImage:(CGImageRef)img;
{
    MCSpriteLayer *layer = [(MCSpriteLayer*)[self alloc] initWithImage:img];
    return layer ;
}


- (id)initWithImage:(CGImageRef)img sampleSize:(CGSize)size;
{
    self = [self initWithImage:img];  // IN THIS LINE IS BAD ACCESS
    if (self != nil)
    {
        CGSize sampleSizeNormalized = CGSizeMake(size.width/CGImageGetWidth(img), size.height/CGImageGetHeight(img));
        self.bounds = CGRectMake( 0, 0, size.width, size.height );
        self.contentsRect = CGRectMake( 0, 0, sampleSizeNormalized.width, sampleSizeNormalized.height );
    }

    return self;
}


+ (id)layerWithImage:(CGImageRef)img sampleSize:(CGSize)size;
{
    MCSpriteLayer *layer = [[self alloc] initWithImage:img sampleSize:size];
    return layer;
}


+ (BOOL)needsDisplayForKey:(NSString *)key;
{
    return [key isEqualToString:@"sampleIndex"];
}

// contentsRect or bounds changes are not animated
+ (id < CAAction >)defaultActionForKey:(NSString *)aKey;
{
    if ([aKey isEqualToString:@"contentsRect"] || [aKey isEqualToString:@"bounds"])
        return (id < CAAction >)[NSNull null];

    return [super defaultActionForKey:aKey];
}


- (unsigned int)currentSampleIndex;
{
    return ((MCSpriteLayer*)[self presentationLayer]).sampleIndex;
}


// Implement displayLayer: on the delegate to override how sample rectangles are calculated; remember to use currentSampleIndex, ignore sampleIndex == 0, and set the layer's bounds
- (void)display;
{
    if ([self.delegate respondsToSelector:@selector(displayLayer:)])
    {
        [self.delegate displayLayer:self];
        return;
    }

    unsigned int currentSampleIndex = [self currentSampleIndex];
    if (!currentSampleIndex)
        return;

    CGSize sampleSize = self.contentsRect.size;
    self.contentsRect = CGRectMake(
        ((currentSampleIndex - 1) % (int)(1/sampleSize.width)) * sampleSize.width, 
        ((currentSampleIndex - 1) / (int)(1/sampleSize.width)) * sampleSize.height, 
        sampleSize.width, sampleSize.height
    );
}


@end

I create the layer on viewDidAppear and start animate by clicking on button, but after init I got a bad access error

    -(void)viewDidAppear:(BOOL)animated
{

    [super viewDidAppear:animated];
    NSString *path = [[NSBundle mainBundle] pathForResource:@"mama_default.png" ofType:nil];
    CGImageRef richterImg = [UIImage imageWithContentsOfFile:path].CGImage;
    CGSize fixedSize = animacja.frame.size;
    NSLog(@"wid: %f, heigh: %f", animacja.frame.size.width, animacja.frame.size.height);
    NSLog(@"%f", animacja.frame.size.width);

    richter = [MCSpriteLayer layerWithImage:richterImg sampleSize:fixedSize];
    animacja.hidden = 1;
    richter.position = animacja.center;

    [self.view.layer addSublayer:richter];
}

-(IBAction)animacja:(id)sender
{
    if ([richter animationForKey:@"sampleIndex"])
    {NSLog(@"jest");
    }
    if (! [richter animationForKey:@"sampleIndex"])
    {
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"sampleIndex"];
    anim.fromValue = [NSNumber numberWithInt:0];
    anim.toValue = [NSNumber numberWithInt:22];
    anim.duration = 4;
    anim.repeatCount = 1;

    [richter addAnimation:anim forKey:@"sampleIndex"];
    }

}

Have you got any idea how to fix it? Thanks a lot.


Solution

  • TL;DR

    Your crash comes from a dangling CGImageRef.

    You can fix it by e.g. changing the line

    richter = [MCSpriteLayer layerWithImage:richterImg sampleSize:fixedSize];
    

    to

    richter = [MCSpriteLayer layerWithImage:[[UIImage imageNamed:@"mama_default"] CGImage] sampleSize:fixedSize];
    

    or a number of equivalent options.

    Explanation

    To explain what’s going on, I’ll slightly modify, and annotate your implementation of viewWillAppear::

    - (void)viewDidAppear:(BOOL)animated
    {
    
        [super viewDidAppear:animated];
    
        // 1
        UIImage *richterImage = [UIImage imageNamed:@"mama_default"];
        // 2
        CGImageRef backingImage = richterImage.CGImage;
        CGSize fixedSize = animacja.frame.size;
    
        // 3
        richter = [MCSpriteLayer layerWithImage:backingImage sampleSize:fixedSize];
        animacja.hidden = 1;
        richter.position = animacja.center;
    
        [self.view.layer addSublayer:richter];
    }
    

    Let‘s go through the comments:

    1. You ask the UIImage class for an image, and it answers by returning a non-owning reference.
    2. You ask that image for its primitive backing-store, (the CGImage) and it answers by returning a non-owning reference to the memory of that store. Note, that this is not an Objective-C object!
    3. You use the non-owning reference to that backing store as the argument for creating your sprite layer.

    Under manual reference counting, the object from step 1 lives in the nearest enclosing autorelease pool. Because you don’t see one anywhere near your code, that object is implicitly guaranteed to outlive the scope of your implementation of viewDidAppear:.

    Under ARC, that is not the case:
    Because the object returned in step 1 is never referenced after step 2, that object may go away any line after step two, which is the last reference to it. And because the backing-store is not an Objective-C object, referencing it any time later without having explicitly claimed interest (through CGImageRetain, e.g.) in it, it becomes invalid as soon as the UIImage becomes invalid.

    Because under ARC the compiler inserts calls to special functions that are functionally equivalent to the methods retain, release, and autorelease but constant, it can optimize some of the redundant retain/(auto-)release pairs away, thus minimizing messaging overhead, and the lifetime of certain objects.

    How aggressively it optimizes those, however, is an implementation detail, and depends on a number of parameters. So when you call layerWithImage:sampleSize:, (which lays claim to the CGImageRef in self.contents = (__bridge id)img;) the first parameter you pass in may or may not be a pointer to invalid memory, based on how aggressively the compiler optimized your code.