views:

107

answers:

4

I have a problem with an Objective-C object (in an iOS game app) that is being mysteriously deallocated.

The object is a GameCharacter instance which is instantiated like so:

for (int c = 0; c < kNrOfGuards; c++) {
    GameCharacter* guard = [[GameCharacter alloc] initGuard:self sprite:guardSprite];
    [characterArray addObject:guard];
    [guard release];
}

I also have a convenience method for finding a GameCharacter:

- (GameCharacter*)findCharacterWithIndex:(int)index {
    return [characterArray objectAtIndex:index];
}

And the code that generates the error looks like:

for (int c = 0; c < [self characterCount]; c++) {
    GameCharacter* tempCharacter = [self findCharacterWithIndex:c];
    if (tempCharacter.playerId == playerIndex]) {
        ...
    }
}

Running this code for some time (never immediately) generates an error in the Console:

[GameCharacter playerId]: message sent to deallocated instance 0x4e47560

With the NSZombieEnabled trick I've managed to track down the object(s) that is causing the problem, but I still can't understand why this object is being deallocated. Searching my code for "release"/"dealloc" does not produce any clues.

I've tried removing the "release" (and even adding a "retain"!) to the alloc/init loop (see top), it seems to extend the time the app can run but not remove the problem entirely.

Any hints would be much appreciated!

EDIT

Thanks to quixoto, Olie, Eiko, tc., I've figured out that it is my GameCharacter object that is being deallocated, but I still don't understand quite why. Here is the trace log in reverse chronological order:

#0 -[GameCharacter dealloc]
#1 objc_setProperty
#2 -[TiledGroundLayer setSelectedCharacter:]
#3 -[TiledGroundLayer selectNextCharacterForPlayer:searchStep:]
#4 -[GameScene selectNextCharacter:]
#5 -[GameScene endTurn]
#6 -[HUDLayer onClickDone:]

What happens here, is that the user clicks "Done", the selected character on screen is changed and thus the property selectedCharacter on TiledGroundLayer (step #2-4). Since selectedCharacter owns the previous GameCharacter object, it seems it is being deallocated. But why is it not being retained properly by the NSMutableArray ([characterArray addObject:guard];)?

A: 

You release your GameCharacter instance somewhere. Your code looks ok, so it's somewhere in the other places that uses those objects.

Eiko
+1  A: 

Debugging spurious retains/releases in 3 easy steps:

  1. Override -retain, -release, and -autorelease for the class you are interested in. Make them log a message (NSLog(@"%@ %s", self, sel_getName(_cmd))) and super-call.
  2. Breakpoint all these methods (at the super-call, i.e. after the log message so you know which object it is). Edit the breakpoint; add the command "bt" and check the auto-continue box (or just use two commands "bt", "continue").
  3. Clear the log. Run the app. Print out the log. Stick it to a whiteboard. Draw some arrows until you find the spurious release/autorelease.

My first impression was that characterArray was being released too early, but that should result in it complaining about sending a message to a deallocated NSArray. Unless, of course, you're accessing characterArray from multiple threads (don't do that!).

tc.
+1  A: 

There's not quite enough code here to tell what the problem is but, from the error message, I'd guess that the playerId object is what's not being retained. That is, it seems that your tempCharacter is fine, but not the playerId field.

If you have

@property(nonatomic,retain) SomeObject *playerId;

then remember that

playerId = foo;

will NOT hold onto the object for you. You must use the accessor:

self.playerId = foo;

EDIT in response to Tom's question-edit:

I absolutely, positively guarantee you that objects placed in an NSMutableArray are retained by that array until (a) they are removed or (b) the array is released. So you can stop looking there, the problem is somewhere else. :)

One thing you can try is to add the following code to the object that is being released when you think it shouldn't:

#pragma mark -
#pragma mark Memory-use debugging

#define DEBUG_RETAIN_RELEASE    0
#define DEBUG_ALLOC_DEALLOC     0



#if DEBUG_ALLOC_DEALLOC

static int allocCounter = 0;
+(id)alloc
{
    id me = [super alloc];
    NSLog(@"%@ ALLOC (%2d):   %@", [me class], ++allocCounter, me);

    return me;
}

#endif


#if DEBUG_RETAIN_RELEASE
- (id)retain
{
    id result = [super retain];
    NSLog(@"%@ retain      %@, count: %2d", [self class], self, [self retainCount]);
    return result;
}


- (void)release
{
    // we have to log BEFORE the release, in case it's the last one! e
    NSLog(@"%@ RELEASE     %@, count: %2d", [self class], self, ([self retainCount] - 1));
    [super release];
}


- (id)autorelease
{
    id result = [super autorelease];
    NSLog(@"%@ AUTOrelease %@, count: %2d", [self class], self, [self retainCount]);
    return result;
}

#endif

// ...


- (void)dealloc
{
#if DEBUG_ALLOC_DEALLOC
    NSLog(@"%@ dealloc (%2d): %@", [self class], --allocCounter, self);
#endif

    [self releaseMyStuff];
    [super dealloc];
}

Then start with DEBUG_ALLOC_DEALLOC = 1 and put a breakpoint on the dealloc log statement. If that doesn't help, set DEBUG_RETAIN_RELEASE = 1 and break on both retain & release.

You'll be surprised at all the iOS retains you get, but don't worry about it, iOS promises balanced retain-release, if used properly. I'm just warning you because you may be expecting a much lower retain count, and be surprised to see it climb during some operation or another.

Luck!

Olie
+1  A: 

Based on your update:

#0 -[GameCharacter dealloc]
#1 objc_setProperty
#2 -[TiledGroundLayer setSelectedCharacter:]

I would guess that you're releasing your existing reference to an object in your setter, followed by retaining the new copy. However, if the new object happens to be the exact same object as the existing reference, you might be sending the retain message to an already deallocated object.

-(void) setSelectedCharacter: (GameCharacter*) newCharacter
{
  [character release]; // Oops if character == newCharacter
  character = [newCharacter retain];
} 
Paul Alexander
The property is defined as: `@property (nonatomic, retain) GameCharacter* selectedCharacter;` - wouldn't the "retain" part take care of this?
Tom Söderlund
Paul Alexander, you're absolutely right! It was the "retain" part of `@property (nonatomic, **retain**)` that released the object prematurely. By changing it to "assign" instead, the problem is solved. Thanks to all of you!
Tom Söderlund
Just to be clear, changing it to assign (from retain) changes the behaviour of the code. You may have fixed THIS bug, but be careful that you didn't just introduce a dozen others.
Olie
What Paul was trying to describe is this: In his example code, imagine that character and newCharacter are the same object. The first line of the function releases NEWCHARACTER, then the 2nd line attempts to retain a retained object. The correct order to do that function is this: id oldCharacter = character; character = [newCharacter retain]; [oldCharacter release]; See?
Olie