daveBlog

Sitting in relative obscurity since 2007…

View My GitHub Profile

Follow me on Twitter

Come work with me!

Site feed

Objective-C Reference Counting From the Bottom Up

I've been interviewing a lot of Objective-C developers, lately. In my mind, one of the most fundamental parts of working with Objective-C is understanding how reference counting works, and I'm consistently surprised by how many people are unable to clearly describe it. In this post, I'll outline the absolute minimum that anyone working with Objective-C ought to know about reference counting. If you find yourself in an interview with me, and you can describe everything in this blog post, I'll know where you got your information you'll be doing better than most.

The Basics

Objective-C objects are reference counted. This means that each object comes with a little counter, and anyone can modify the value of this counter by calling retain (to increase the value) or release (to decrease the value). When the value of the counter reaches 0, the dealloc method is called on the object, and any memory associated with the object is reclaimed by the system.

A good developer will make use of this system by calling retain on objects whose pointers are being stored in other objects and calling release on those objects when the pointer no longer needs to be stored. This is, in fact, the reason that we call it "reference counting": the counter is intended to indicate how many objects (or, in some cases, global variables) are holding references to the object.

Deadly Embraces and Retain Cycles

When two objects each call retain on the other, we call it a "deadly embrace": if no other action is taken, the objects will never be deallocated. This is widely regarded as a "bad thing".

When this sort of pattern occurs with more than two objects (for example, A retains B, B retains C, and C retains A), it is called a "retain cycle", and these can be even nastier to deal with.

Deadly embraces and retain cycles are dealt with by either preventing one object in the cycle from retaining its "parent" or by having explicit cleanup logic that is executed when it is certain that the objects are no longer needed. Really elite developers sometimes create proxy objects to stand in for one of the objects, although a full discussion of this technique is beyond the scope of this post.

Autorelease

The autorelease method is perhaps the most widely misunderstood concept in Objective-C memory management, although in practice, it's actually relatively simple.

At the beginning of each iteration through your application's run loop, an NSAutoreleasePool is created. At the end of each iteration it is destroyed. You can also create and destroy NSAutoreleasePools anytime you wish to do so (for example, you generally need to manually create and destroy them in background threads; more on this, later).

When you call autorelease on an object, the object searches for the most recently created NSAutoreleasePool in its thread. Assuming that it finds a pool, it adds itself to the list of objects that need to be released. When the NSAutoreleasePool is destroyed, it calls release on each object that is in its list--if an object is listed multiple times, release is called multiple times. If the object fails to find an NSAutoreleasePool, a message is logged to the console and the object is leaked (this is why you need to create an NSAutoreleasePool when doing work on a background thread).

Ownership and Object Creation

While the notion of "ownership" is not baked into the semantics of Objective-C, one object can be thought to "own" another if it has called retain on the object or has created an instance of the object via the alloc or copy methods. An object that owns another is responsible for calling release on that object when it is no longer needed.

This brings us to an interesting question: what should be done for a method that creates an object but isn't called alloc or copy? In order for the object to be active, its retain count must be at least 1, but the calling method cannot be relied upon to call release.

The answer, of course, is to use the autorelease method. This allows the retain count to be a non-zero value when the method returns, and if nothing is done to keep the object alive, it will be reclaimed when the active NSAutoreleasePool is destroyed.

Property Getters and Setters

Care must be taken when writing a method that sets a property value. The naive implementation of such a method looks like this:

- (void)setProperty:(id)value
{
	[propertyField release];
	propertyField = [value retain];
}

This is bad: if value is the same as propertyField, and the retain count of this object is 1 when we enter the method, the first line of the method will cause the object to be deallocated, and the second line will cause bad things to happen (although bad stuff may not happen immediately, which is worse).

This problem can be worked around by retaining value before releasing propertyField, by ensuring that the values are not equal to each other before performing the operation, or even by autoreleasing propertyField instead of releasing it (although this adds overhead).

Another interesting problem in this space involves methods that get property values. Again, the naive implementation of such a method looks like this:

- (id)property
{
  return propertyField;
}

99 times out of a hundred, this will work fine. This of course means that when you run into that one time it doesn't work, it's going to be a huge pain to debug. Here's the case in which you can get into trouble:

id foo = [obj property];
[obj setProperty:bar];
[foo doSomething];

If the retain count of propertyField is 1 at the beginning of this block, then the second line will cause it to drop down to zero and be reclaimed. This means that the third line will be performed on an object that has been deallocated, which is, to say the least, less than ideal. The solution to this is to modify the property getter so that it calls retain and autorelease on propertyField before returning it. This has no net effect on the retain count of the object, but it does ensure that the object will stay alive at least until the active NSAutoreleasePool goes out of scope.

comments powered by Disqus