I recently discovered that ARC does not clean up objects automatically when an exception is thrown, which was the cause of some strange test pollution in my specs.
An example will best illustrate how this can happen. Here’s a simple assertion method that raises an exception if an object is not an instance of a given class:
- (void)assertObject:(id)object isKindOfClass:(Class)class
{
if (![object isKindOfClass:class])
{
[NSException raise:NSInternalInconsistencyException
format:@"Expected %@, received %@",
NSStringFromClass(class),
NSStringFromClass([object class])];
}
}
When the exception is thrown, object
will leak. Here’s why: ARC calls retain
on all objects passed to your methods, and pairs them with a release
when your method returns. But it doesn’t call release
if an exception occurs.
This is not a bug. The ARC documentation explicitly states:
By default in Objective C, ARC is not exception-safe for normal releases:
It does not end the lifetime of __strong variables when their scopes are abnormally terminated by an exception.
It does not perform releases which would occur at the end of a full-expression if that full-expression throws an exception.
The reasoning is that exceptions in Objective-C are reserved for very exceptional cases. They are not suitable for control flow or general error handling and typically signal that the app is about to crash.
This assumption falls flat if you’re running automated tests. As the assertObject:isKindOfClass:
method implies, Objective-C testing frameworks typically do use exceptions for control flow. As a result, the default behavior can cause your tests to leak memory and cause test pollution.
Luckily there’s a way to turn on ARC exception handling by providing the -fobjc-arc-exceptions
flag when compiling. This ended up solving my obscure case of test pollution. If one test failed it would leak a global object and in turn cause others run after it to fail in strange ways.