0

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;
4

2 回答 2

3

You can check if any of your objects is inadvertently retained by an autorelease pool: Just wrap your purge method's contents in an @autorelease block. That would remove any recently autoreleased objects when control flow leaves the scope of the pool.

Edit, to answer comment:

ARC does not give a precise promise about when objects are added to an autorelease pool. The resulting code actually differs when compiling with optimizations.

You can somehow control behavior for automatic variables (function scope) by adding a objc_precise_lifetime attribute to it's declaration.

于 2012-07-11T13:13:29.007 回答
2

You've already found the answer, but to elaborate: because you created a local reference to each object in your cache, the lifetime of each of those objects was extended at least to the end of the loop (and possibly further at ARC's discretion.) If you mark a local variable as __weak, ARC will skip that step.

于 2012-07-11T13:49:24.337 回答