Update: Please Mark Dalrymple's article for a more elegant solution. In short, use __block instead of the NSValue trick below.
Is it tempting to write the following code:
NSBlockOperation *op = [[[NSBlockOperation alloc] init] autorelease];
[op addExecutionBlock:^(void) {
if (![op isCancelled]) {
// run the block if the operation is not cancelled
}
}];
[someOperationQueue addOperation:op];
If you don't use garbage collection (gc), the snippet above will eat up memory, and the Leaks Instrument will not be able to discover it as a leak. Why?
In Objective-C, blocks behave like objects. In non-gc runtime, they have retain count, can be copied1 and must be released when you want to discard them.
When a block is created, it implicitly retains every Objective-C object referenced in its scope. When the block is deallocated, it sends -release
to all those retained objects. In Objective-C, those extra calls are done for you by the compiler. If you set breakpoints to -retain
and -release
, you'll be able to observe this behavior as blocks are created or deallocated.
Usually you don't need to worry about those things, and that's how you can use blocks as good closures that many other modern languages have.
But here's the problem: Because blocks only send -release
when they are deallocated, not when their scope exits, it is possible for them to retain the objects which retain them. The code snippet above is a case in point. The NSBlockOperation
object op
retains an execution block, but then the block implicitly retains op
. The result of such cyclic retention is that the retain count of op
will never reach zero, and hence op
will eat up a chuck of unreclaimable memory. And because op
is retained by another object, it is not a lone wanderer that you forget to call -release
, therefore the Leaks Instrument will never discover it as a leak.
A solution to this problem would be:
NSBlockOperation *op = [[[NSBlockOperation alloc] init] autorelease];
NSValue *weakOpValue = [NSValue valueWithNonretainedObject:op];
[op addExecutionBlock:^(void) {
if (![[weakOpValue nonretainedObjectValue] isCancelled]) {
// run the block if the operation is not cancelled
}
}];
[someOperationQueue addOperation:op];
The NSValue
object used above is effectively a weak reference to op
. This breaks the retention cycle, so the -dealloc
of op
will now be called, and the block will also be released.
Cyclic retention is not a problem for Objective-C runtime's garbage collector, so the first code snippet will not be a problem if you use gc.
Another incomplete solution is for Apple to change the timing of calling -release
in a block. Releasing retained objects when a block exits seems to be a reasonable expectation. But, given the nature of the use case above (an NSBlockOperation
might never get executed), plus the fact that referenced objects must be retained before a block is entered, it's not an encompassing solution, although it would make blocks behave more to such expectation.
One final note: Because of the potential risk of cyclic retention, you must be very careful if you use blocks within a block. This is one place where gc can liberate us from such a blocking issue (pun intended), but gc is not available on the iOS and has its issues on Mac, too.
But not retained. The reason is that when a block is first created, its scope refers to the values residing in the nearest outer scope on the stack. If you toss it around (i.e. passing it as an object), it must first make a (const) copy of those values onto the heap. If you just retain a block then pass it around, chances are it's still referring to the stack. And you know what will happen when dangling stack references are passed around. ↩