Cast Irony

"It is a very sad thing that nowadays there is so little useless information"

1 note &

Avoiding retain cycles with blocks, a right way

A few months ago, I received a ticket from our QA tester with a subject line guaranteed to strike terror into the heart of any developer: “iPhone app crashing, occasionally.”

Most of the time, the application would work perfectly, but maybe one time in a hundred, without warning, it would crash. It seemed to happen more often when the user would quickly switch back and forth between the different parts of the app. I was able to reproduce the crash with the debugger running, and discovered it was an EXC_BAD_ACCESS, most likely caused by sending a message to an already-deallocated object.

I went through my recent commits looking for something that could possibly have caused this sort of misbehavior. I’d just finished an audit of every block in the application, looking for potential memory leaks. When a block is copied to the heap, it automatically retains any Objective-C objects it has a reference to. If one of those objects happens to have ownership of the block, a retain cycle is formed, which will prevent the block, the object, and anything else owned by either from being deallocated.

The common solution to this problem is to disable the block’s automatic retaining behavior by redeclaring the object with the __block qualifier. Suspecting this to be part of the problem, I commented out the ‘__block’, made a new build, and the crash disappeared. Slowly, and with the help of a few NSZombies, I came up with a likely hypothesis of what was happening:

  1. I had an object, that we’ll call ‘A’. Object A had ownership of a block (in this case, it was stored in a copy property), and the block’s code contained a reference to A, creating a retain cycle.
  2. To break this retain cycle, I’d redeclared A as __block typeof(A) blockA, and exclusively used blockA in the block’s code. The block now did not have ownership of A or blockA.
  3. At some point, A needed to actually invoke the block. Since there was no reason to run the block synchronously, I passed it to dispatch_async().
  4. dispatch_async() copies the block, and GCD keeps ownership of it until after it is finished executing. Note: at this point, the block is co-owned by both object A and GCD.
  5. Because the user is going crazy switching between views, A is released, and because the block didn’t retain A, it is deallocated.
  6. GCD finally gets around to executing the block. The block sends a message to a now-deallocated object A. Boom.

The more I tested it, the more sure I became that this sequence of events matched the behavior I was seeing. It certainly explained why the crashes were so rare: most of the time, the block would execute and be released by GCD before A was released, and nothing bad would happen.

Unfortunately, though I felt I understood the problem, I didn’t have a solution. I decided that leaking a little bit of memory was the lesser of two evils, removed my __block declarations, and sent the app off. It bugged me, though, that there should be such a glaring hole in the memory management rules.

In the traditional pattern for breaking retain cycles, the object with the strong reference is responsible for notifying the other object when it’s deallocating, so the other object can change its weak pointer to nil. This would neatly avoid the problem, because messages sent to nil don’t cause exceptions. Sadly, there’s no way to tell a block to nil-out a pointer in its code.

It was around this point that I remembered Mike Ash’s magical MAZeroingWeakRef class. MAZeroingWeakRef has a target property that is guaranteed to always either point to a valid object, or nil. Instead of writing:

__block typeof(foo) blockFoo = foo;
foo.myBlock = ^{ [blockFoo doSomething]; };

you can write:

MAZeroingWeakRef* fooRef = [MAZeroingWeakRef refWithTarget:foo];
foo.myBlock = ^{ [fooRef.target doSomething]; };

That’s it! A simple fix, but as far as I know it completely solves the problem, with no significant side effects.

Conclusions:

  1. Never use __block to break a retain cycle.

  2. Use MAZeroingWeakRef instead.

Mike Ash, if you’re ever in Portland, I owe you several beers.