Your problem stems from the auto release pool, a somewhat anachronistic feature in the days of ARC.
When an object is created with an alloc
/init
combination the resultant object is owned by the caller. The same is true for the standard new
method, it is defined as alloc
followed by init
.
For each init...
method a class by have a matching <type>...
method, this is defined as alloc
/init
/autorelease
and returns an unowned object to the caller. For example your code uses stringWithFormat:
which is defined as alloc
/initWithFormat
/autorelease
.
The unowned returned object is in the auto release pool and unless ownership is taken it will be release automatically the next time that pool is emptied, which for the main autorelease pool is once per iteration of the main event loop. In many programs iterations of the event loop are frequent enough to reclaim objects from the auto release pool quick enough that memory usage does not climb. However if objects are created and then discarded a lot with a single iteration of the event loop, as in your example of a large for
loop, the auto release pool can end up with a lot of needed objects before it is emptied.
A Common Solution
A common solution to this problem is to use a local auto release pool, which is emptied as soon as it is exited. By judicious placement of such local auto release pools memory use can be minimised. A common place for them is wrapping the body of loops which generate a lot of garbage, in your code this would be:
void process()
{
for (int i=0; i<targetCount; i++)
{ @autoreleasepool
{
calledFunction(i);
}
}
}
here all auto released and discarded objects created by calledFunction()
will be reclaimed on every iteration of the loop.
A disadvantage of this approach is determining the best placement of the @autoreleasepool
constructs. Surely in these days of automatic referencing count (ARC) this process can be simplified? Of course...
Another Solution: Avoid The Auto Release Pool
The problem you face is objects ending up in the auto release pool for too long, a simple solution to this is to never put the objects in the pool in the first place.
Objective-C has a third object creation pattern new...
, it is similar to the <type>...
but without the autorelease
. Originating from the days of manual memory management and heavy auto release pool use most classes only implement new
- which is just alloc
/init
- and no other members of the family, but you can easily add them with a category. Here is newWithFormat
:
@interface NSString (ARC)
+ (instancetype)newWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
@end
@implementation NSString (ARC)
+ (instancetype)newWithFormat:(NSString *)format, ...
{
va_list args;
va_start(args, format);
id result = [[self alloc] initWithFormat:format arguments:args];
va_end(args);
return result;
}
@end
(Due to the variable arguments this is more involved than most new
family methods would be.)
Add the above to your application and then replace calls to stringWithFormat
with newWithFormat
and the returned strings will be owned by the caller, ARC will manage them, they won't fill up the auto release pool - they will never enter it, and you won't need to figure out where to place @autoreleasepool
constructs. Win, win, win :-)
HTH