blog.lukhnos.org

Cyclic Retention Pitfall in Objective-C Blocks

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.


  1. 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.