I recently migrated my game to ARC. First, I noticed my app crashed after playing for a while. So I began debugging it and noticed that, on receiving a memory warning, the deallocation of some resources was being corrupted.
Background (Don't read if you understand the code below)
In my game, OpenGL textures are encapsulated in an Objective-C class (Texture
). This class keeps track of the 'use count', i.e. how many objects are referencing the texture at a given time (similar to Obj-C retainCount
) with a property adeptly named useCount
. On deallocation, the OpenGL texture is destroyed.
Sprites are created using TextureAtlas
objects. Each texture atlas is associated with a Texture
object and a database of named subregions inside the texture image. On creation of the atlas, it increases the associated Texture
's use count by 1. Also, each texture atlas keeps track of how many Sprite
instances are referencing its texture (i.e., how many sprites have been created from the atlas and are still around).
Needless to say, on deallocation each atlas decreases the associated texture's use count by 1.
In addition, when a new Sprite
is created (from a TextureAtlas
or otherwise), the useCount
of the corresponding texture is also increased, once by each sprite. And decreased again, on sprite deallocation.
So, as long as some Sprite
is referencing a TextureAtlas
's texture, the atlas can't be purged. And as long as some Sprite
or TextureAtlas
is referencing a Texture
, the texture can't be purged either.
Texture
objects and TextureAtlas
objects, in turn, are managed by the singletons TextureManager
and TextureAtlasManager
respectively. These two managers are responsible for creating resources as necessary and purging unused resources on low-memory situations.
I chose this design (decoupling Texture
use count by sprites and atlases, and TextureAtlas
use count by sprites) because some times I might need a texture for something else than a sprite (say, a 3D object).
Still here?
So, when I receive a memory warning, first I call the -purge
method in the TextureAtlasMananger
:
- (void) purge
{
// Called on Low Memory Situations.
// purges unused atlases.
// _atlasRank is an array of atlases in MRU order
// _atlasDatabase is a dictionary of atlases keyed by their name
NSUInteger count = [_atlasRank count];
NSMutableArray* atlasesToRemove = [[NSMutableArray alloc] init];
for (NSUInteger i=0; i < count; i++) {
TextureAtlas* atlas = [atlasRank objectAtIndex:i];
if ([atlas canDelete]) {
// Means there are no sprites alive that where created
// from this atlas
[atlasesToRemove addObject:atlas];
[_atlasDatabase removeObjectForKey:[atlas name]];
NSLog(@"TextureAtlasManager: Successfully purged atlas [%@]", [atlas name]);
}
else{
// Means some sprite remains that was
// created from this atlas
NSLog(@"TextureAtlasManager: Failed to purge atlas [%@]", [atlas name]);
}
}
[_atlasRank removeObjectsInArray:atlasesToRemove];
// At this point, atlasesToRemove should be deallocated
// by ARC, and every atlas in atlasesToRemove
// should be deallocated as well.
// This FAILS to delete unused textures:
[[TextureManager sharedManager] purgeUnusedTextures];
// (:Removed atlases are not yet deallocated and 'retain'
// their texture)
// ...But This SUCCEEDS:
[[TextureManager sharedManager] performSelector:@selector(purgeUnusedTextures)
withObject:nil
afterDelay:0.5];
// (i.e., -[TextureAtlas dealloc] gets called before -purgeUnusedTextures)
}
It seems that the temporary array I created to hold the atlases scheduled for deletion (I don't like removing objects from the array being iterated) is being 'autoreleased' later.
I've checked this similar question: Inconsistent object deallocation with ARC? , but I fail to see how it applies to my case. The array in question is a local variable of the method, created with alloc/init. How can I ensure it is not autoreleased? (if that is the case).
EDIT (Solved?)
I can confirm that the delayed deallocation disappears (i.e., code works as intended) if I replace this:
[atlasRank removeObjectsInArray:atlasesToRemove];
with this:
[atlasRank removeObjectsInArray:atlasesToRemove];
atlasesToRemove = nil;