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.
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.
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:
UIImage
class for an image, and it answers by returning a non-owning reference.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!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.