I’m interested in learning more about how best to handle memory management under tight loops with ARC. In particular, I’ve got an app I’m writing which has a while loop which rus for a really long time, and I’ve noticed that despite having implemented (what I believe to be) the best practices in ARC, the heap keeps growing boundlessly.
To illustrate the problem I’m having, I first set up the following test to fail on purpose:
while (true) {
NSMutableArray *array = [NSMutableArray arrayWithObject:@"Foo"];
[array addObject:@"bar"]; // do something with it to prevent compiler optimisations from skipping over it entirely
}
Running this code and profiling with the Allocations tool shows that the memory usage just endlessly increases. However, wrapping this in an @autoreleasepool as follows, immediately resolves the issue and keeps the memory usage nice and low:
while (true) {
@autoreleasepool {
NSMutableArray *array = [NSMutableArray arrayWithObject:@"Foo"];
[array addObject:@"bar"];
}
}
Perfect! This all seems to work fine — and it even works fine (as would be expected) for non-autoreleased instances created using [[... alloc] init]. Everything works fine until I start involving any UIKit classes.
For example, let’s create a UIButton and see what happens:
while (true) {
@autoreleasepool {
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectZero;
}
}
Now, the memory usage increases ad infinitum — effectively, it appears that the @autoreleasepool is having no effect.
So the question is why does @autoreleasepool work fine for the NSMutableArray and keep the memory in-check, but when applied to a UIButton the heap continues to grow?
Most importantly, how can I keep the heap from expanding endlessly when using UIKit classes in an endless loop like this, and what does this tell us about the best practices for ARC in while(true) or while(keepRunningForALongTime) style loops?
My gut feeling on this is (and I could be totally wrong) is that it’s perhaps something about how the while (true) keeps the runloop from cycling, which is keeping the UIKit instances in memory rather than releasing them… But clearly I’m missing something in my understanding of ARC!
(And to eliminate an obvious cause, NSZombiedEnabled is not enabled.)
Right, I have done some more thinking about this, together with the great contributions from bbum and Wade Tegaskis regarding blocking the runloop, and realised that the way to mitigate this sort of issue is by letting the runloop cycle, by using the
performSelector:withObject:afterDelay:which lets the runloop continue, whilst scheduling the loop to continue itself in the future.For example, to return to my original example with the
UIButton, this should now be rewritten as a method like this:-This way, the method ends immediately and
buttonis correctly released when it goes out of scope, but in the final line,spawnButtoninstructs the runloop to runspawnButtonagain in 0 seconds (i.e. as soon as possible), which in turn instructs the runloop to run… etc etc, you get the idea.All you then need to do is call
[self spawnButton]somewhere else in the code to get the cycle going.This can also be solved similarly using GCD, with the following code which essentially does the same thing:
The only difference here is that the method call is dispatched (asynchronously) onto the main queue (main runloop) using GCD.
Profiling it in Instruments I can now see that although the overall allocation memory is going up, the live memory remains low and static, showing that the runloop is cycling and the old
UIButtons are being deallocated.By thinking about runloops like this, and using
performSelector:withObject:afterDelay:or GCD, there are actually a number of other instances (not just with UIKit) where this sort of approach can prevent unintentional “memory leaks” caused by runloop lockups (I use that in quotations because in this case I was being a UIKit rogue.. but there are other cases where this technique is useful.)