Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@beccadax
Created August 26, 2012 02:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beccadax/3473115 to your computer and use it in GitHub Desktop.
Save beccadax/3473115 to your computer and use it in GitHub Desktop.
Suggested code to get rid of objects with no references except for their presence in a specific set
// self.resources is an NSMutableSet of ANResource objects.
// I want to remove the objects that have no other references in the app.
// (This is intended to be called in response to an iOS memory warning.)
// Will this do the trick?
- (void)discardUnusedResources {
// We pass the objects through a weak reference; if they're not
// referenced anywhere else in the app, they won't survive the journey.
NSMutableSet * newResources = [NSMutableSet new];
NSUInteger oldResourceCount = self.resources.count;
__weak ANResource * weakResource;
while((weakResource = [self.resources anyObject])) {
[self.resources removeObject:weakResource];
__strong ANResource * strongResource = weakResource;
if(strongResource) {
[newResources addObject:strongResource];
}
}
self.resources = newResources;
NSLog(@"-[ANSession discardUnusedResources]: started with %u resources, ended with %u", oldResourceCount, self.resources.count);
}
@bjhomer
Copy link

bjhomer commented Aug 26, 2012

Probably won't work. The call to anyObject likely autoreleases its return value, which means the weakResource pointer will not go to nil. You could probably throw in an @autoreleasepool to get around this. However, it still may not detect other objects that are no longer being used but still are held in an outer autorelease pool.

@aufflick
Copy link

@bjhomer's queries might be simply worked around by a delayed call for the second part, but the question is why you need to do this? If you only optionally want to cache stuff, just always store them with a weak reference, and access them via a method that will lazily recreate them if the reference is nil.

@bjhomer
Copy link

bjhomer commented Aug 26, 2012

In fact, if you just want an NSSet with weak pointers, look at NSHashTable. It has a -weakObjectsHashTable method available in 10.8, but it looks like it's just a convenience method for something you could do with the more verbose init method.

@beccadax
Copy link
Author

@aufflick @bjhomer In the general case, I want to hold references to these objects in the long term, but under memory pressure (specifically, an iOS memory warning), I want to release them. I suppose I'm not married to this pattern of behavior, though.

I wrote a minimal test program to look at some of the behavior here. This code:

    NSMutableSet * set = [[NSMutableSet alloc] initWithObjects:[NSObject new], nil];

    __weak NSObject * weakObject = [set anyObject];
    [set removeObject:weakObject];

    NSLog(@"weakObject = %@", weakObject);

Does print an object. But if I set a breakpoint on -[NSObject autorelease], I see that the autorelease is not happening on the -anyObject line. That's not actually surprising—collection accessors like -anyObject or -objectAtIndex: don't usually autorelease their return values; I've been bitten by that a few times. The autorelease is happening on the -removeObject: line, and if the Debug Navigator is to be believed, it's happening in my test code, not in -removeObject. And sure enough, if I use an autorelease pool:

    NSMutableSet * set = [[NSMutableSet alloc] initWithObjects:[NSObject new], nil];

    __weak NSObject * weakObject = [set anyObject];
    @autoreleasepool {
        [set removeObject:weakObject];
    }

    NSLog(@"weakObject = %@", weakObject);

The last line prints a nil.

Still, that's some rather worryingly unpredictable behavior, and I don't like that the LLVM team could change their minds about it at any time.

@beccadax
Copy link
Author

@aufflick @bjhomer In the general case, I want to hold references to these objects in the long term, but under memory pressure (specifically, an iOS memory warning), I want to release them. I suppose I'm not married to this pattern of behavior, though.

I wrote a minimal test program to look at some of the behavior here. This code:

    NSMutableSet * set = [[NSMutableSet alloc] initWithObjects:[NSObject new], nil];

    __weak NSObject * weakObject = [set anyObject];
    [set removeObject:weakObject];

    NSLog(@"weakObject = %@", weakObject);

Does print an object. But if I set a breakpoint on -[NSObject autorelease], I see that the autorelease is not happening on the -anyObject line. That's not actually surprising—collection accessors like -anyObject or -objectAtIndex: don't usually autorelease their return values; I've been bitten by that a few times. The autorelease is happening on the -removeObject: line, and if the Debug Navigator is to be believed, it's happening in my test code, not in -removeObject. And sure enough, if I use an autorelease pool:

    NSMutableSet * set = [[NSMutableSet alloc] initWithObjects:[NSObject new], nil];

    __weak NSObject * weakObject = [set anyObject];
    @autoreleasepool {
        [set removeObject:weakObject];
    }

    NSLog(@"weakObject = %@", weakObject);

The last line prints a nil.

Still, that's some rather worryingly unpredictable behavior, and I don't like that the LLVM team could change their minds about it at any time.

@aufflick
Copy link

ok how about this - use both a weak NSHashTable and a strong NSSet. When you set objects into your cache, add them to both sets. When you get memory pressure, call removeAllObjects on the NSSet. When you read from your cache, always read from the hash table. You never really need to remove objects from the NSHashTable since whether the key doesn't exist or it has been release, nil will be the result, and you don't care about exactly when weak objects will be reaped. (If you're worried about your hash table getting massive, you could just prune any nil values either periodically or on every X calls to your cache api).

@belkadan
Copy link

No one here's heard of NSCache?

@beccadax
Copy link
Author

@belkadan, NSCache drops the entire cache on the floor under memory pressure. That's not what I want—it would lose references to objects that are still active. The main purpose of this set is not caching, but uniquing—ensuring that each of the external resources is represented by exactly one ANResource object. (Think of the way a Core Data managed object context always has exactly one object for each record—that's what I'm doing.) If I used NSCache, then a cache drop could cause a new object to be created for a resource which already has an old object, and the old object would slowly become staler and staler.

I'm using an NSHashTable for now, but I'm not really happy with that solution, because I want this code to run on iOS 5 as well as 6...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment