Cocoa memory management is pretty straightforward; you only have to learn
a few simple rules.
Chances are, though, that you're also going to deal with exceptions in your code. In Cocoa exceptions can complicate memory management if you're not careful. In this post I'll give you a guided tour of some of the pitfalls and show you how to avoid them.
Modern exception handling is a great mechanism for dealing with errors: It
decouples the
detection and
handling of exceptional conditions, and
automates the
propagation from the point of detection to the point of handling. This results in code that's much cleaner when written, much easier to write correctly, and much easier to maintain correctly over time. If you've ever had to deal with a lot of procedural code that returns error codes that need to be checked and propagated manually, you'll know just how much more complex they make your code.
If they're so great, then, why do I say exceptions can complicate memory management? First of all, they make it easy to leak memory. Say you have a simple method that, for efficiency, avoids using autorelease for a temporary object:
- (void)leakOnException {
NSMutableArray *array = nil;
array = [[NSMutableArray alloc] initWithCapacity:0];
[self doSomething:array];
[array release];
}
There's a pretty obvious memory leak, if
-doSomething: throws an exception. The solution is also obvious: Put the
-release in a
@finally block:
- (void)noLeakOnException {
NSMutableArray *array = nil;
array = [[NSMutableArray alloc] initWithCapacity:0];
@try {
[self doSomething:array];
}
@finally {
[array release];
}
}
Of course, your code may not be quite so simple, but the pattern will be obvious — and it applies to all resource management, not just memory. So be careful not to leak file descriptors, or forget to unlock any locks you've acquired!
Another issue you might run into is over-releasing an exception object. Exceptions are effectively out-of-band return values; in Cocoa, this means the exception object itself is autoreleased. When would this result in over-releasing the exception? When you're using an internal autorelease pool:
- (void)overReleaseException {
NSAutoreleasePool *pool = nil;
pool = [[NSAutoreleasePool alloc] init];
[self doSomething];
[pool release];
}
At first glance, this looks fine. After all, if
-doSomething throws an exception, the autorelease pool will still be cleaned up when the next autorelease pool "out" in scope is released. However, this could actually happen
before the exception is delivered — by the release of an outer autorelease pool in a
@finally block — meaning the exception would be a zombie on delivery!
How do you fix it? One way is to make sure you don't release any pools in
@finally blocks; so long as the exception is caught before any pools are released, you're safe. Another way is catch and re-throw any thrown exception in order to "promote" it to the next pool out any time you or any of your callers releases a pool in a
@finally block:
- (void)promoteException {
NSAutoreleasePool *pool = nil;
id savedException = nil;
pool = [[NSAutoreleasePool alloc] init];
@try {
[self doSomething];
}
@catch (id anything) {
savedException = [anything retain];
@throw;
}
@finally {
[pool release];
[savedException autorelease];
}
}
There are several things to note about the above. The first is the use of
id in both the
@catch block and in the declaration of the variable we're using to save any thrown exception. You need to do this to avoid limiting your exception handling to just exceptions of class
NSException. Unlike in Java, Objective-C exceptions are not required to be subclasses of any specific class or to conform to any specific protocol.
Also note that there is no argument in the
@throw statement; this re-throws the caught exception exactly as caught; this is important, because Objective-C
does support matching
@catch blocks based on the type of the exception.
Finally —
ahem — the order of operations in the
@finally block is important. The whole point of the above is to retain any thrown exception
across the release of the interior autorelease pool, which is the pool the exception was placed in on its way out of
-doSomething, and ensure that it's autoreleased in the next pool "out" in scope. To do this, we need to make sure that the interior pool is released before autoreleasing the exception. Once the
@finally block executes, exception propagation will continue and the exception will be located in the proper autorelease pool.
One other comment about this pattern: You'll need to follow it
any time you are returning an object created within the scope of an interior autorelease pool. For example, if
-doSomething could return an
NSError you would need to ensure that it's promoted to the outer pool before being returned, like so:
- (BOOL)promoteError:(NSError **)error {
BOOL success;
NSAutoreleasePool *pool = nil;
pool = [[NSAutoreleasePool alloc] init];
success = [self doSomething:error];
if ((success == NO) && (error != NULL)) {
[*error retain];
}
[pool release];
if ((success == NO) && (error != NULL)) {
[*error autorelease];
}
return success;
} The above is the same pattern as before, modulo any exception handling. The checks around the manipulation of
*error are there because it's valid to pass
NULL for output
NSError parameters to indicate that no error should be generated, and even if
NULL isn't passed, you're only allowed to touch the parameter if an error is actually generated.
One more bit about exceptions in Cocoa before I go: In general, the Cocoa frameworks themselves will not generate exceptions for anything but "programming errors." In other words,
-[NSArray objectAtIndex:] may generate an exception if you ask for an object outside the range of the array, but this is the kind of exception that should never occur when an end user is using an application. The Cocoa frameworks will generally not use exceptions to do things like indicate a file doesn't exist, or a server couldn't be connected to. Other frameworks, however, may not follow the same pattern — and application code may not either. So it pays to be robust.
Update: Thanks to
Michael Tsai for pointing out that I wasn't explicit enough in explaining how you might over-release an exception. I've clarified that section a bit.