macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Knowing When to Let Go: Better Living Through Memory Management
Pages: 1, 2

Retain/Release/Autorelease

Cocoa has a third method up its memory managing sleeve: -autorelease. Autoreleasing an object is similar to releasing it, except that it's got an intrinsic delay; it says "wait a bit before releasing it." But how long is the delay?



Program flow control in Cocoa is generally handled by means of NSRunLoop instances, whether you're aware of them or not. If you're writing a Foundation Tool, or spawning a new thread for use with NSConnection, you'll be creating them yourself, but if you're just writing a "simple" application, the magic is in your main function. Apple's default main function implementation calls a function, NSApplicationMain, with the arguments that the program was provided with on launch. This function does a lot of work, including loading your .nib files, setting up the global NSApplication instance NSApp, and starting the run loops that manage application events like mouse-clicks.

In addition to managing input from the user and other sources like ports, NSRunLoop also works with another class, NSAutoreleasePool, to implement the delay. Here's a quick summary of what's happening:

  • The current run loop receives input of some sort.

  • It then runs the appropriate code: if the user clicked a button, then the button's target is sent the action message specified in the .nib file, and the right method is run.

  • That method branches off into several objects and they interact. Eventually, an object is autoreleased.

  • The autoreleased object's retain count stays the same for the moment, but it is added to the list of objects handled by the current autorelease pool.

  • Eventually, the action method concludes its work, and control is returned to the run loop.

  • At this time--and notably prior to looping back to continue execution on input--the autorelease pool is "purged" by being released; that is, all the objects it contains are sent the -release message at this time, which will as normal call -dealloc if it has to.

After that, the return loop will operate on the next piece of input with a fresh autorelease pool ready to be filled.

All of this raises a question: Why and when is this useful? To cover the why, think about the -description method implemented by NSObject and (hopefully) overridden to return a useful description of the receiver in all your classes. This method returns an object, but it's probably not an instance variable and therefore is not retained by your object. So what is its state? Should it be up to the calling code to -release it? Autoreleasing removes the confusion.

With this in mind, here's a guideline for the use of -autorelease:

  • If you need to defer ownership of an object, autorelease it.

To simplify that, if you want to create an object and give it to something else and then completely forget about it, make sure that it's autoreleased. Another good example is if you want to create an array without having to release every object you add to it after you're done; autorelease, and it's all done for you.

If you look at the Cocoa classes, there are lots and lots of examples of methods that autorelease. All of the convenience creator methods--class methods other than +alloc which return an object--autorelease as a, you guessed it, convenience.

This has two important ramifications. The first is writing convenience creators for our own classes and is dead simple. Here's an example that's almost a template:

+(id)funkyObjectWithFriend:(id)aFriend
{
	return [[[self alloc] initWithFriend:aFriend] autorelease];
}

Like I said, dead simple.

Next, let's think about the example of -description. Should we call -autorelease or is it already called? To answer this, we'll look at another example:

-(id)description
{
	return [NSString stringWithFormat:...];
}

In this example, we're calling one of NSString's convenience creators. So we can be certain that the returned object is already autoreleased, and that there is no need for us to autorelease it ourselves. But for a more complex example, what if, for some reason, there is no convenience creator for exactly what we need? In this case, we'll be calling +alloc instead of a convenience creator, and so we will have to autorelease it ourselves. Remember, if you created it yourself with +alloc, -copy, or -mutableCopy, then it's up to you to release or autorelease it! Going the other way, it's safe to assume that if you're getting an object that something else created, you do not have to autorelease.

Because of the way -autorelease operates, it is not safe to assume that just because the object you receive exists now, it will exist until you're done with it. This makes it absolutely vital to remember that if you need an object that you did not create to stay around until you're done, you need to retain it. Otherwise, it could easily be deleted out from under you, causing no end of frustration. Always remember: if you need it but did not create it, retain it.

One thing that can help you with this is to write -set... methods for all your ivars, even if you don't make these methods public, and use them instead of setting the ivars directly. A simple example that's a good template for single-threaded use (multithreading-safe accessors are beyond the scope of this document) is as follows:

-setFriend:(id)aFriend
{
	id old = friend; // friend is an ivar
	friend = [aFriend retain];
	[old release];
}

This is basically a retaining swap; out with the old, in with the new.

You might be asking yourself why we don't just use -autorelease all the time since it makes things so much easier. Let's take a look.

As you'll recall from our discussion of autoreleasing, the autorelease pool has to keep a list around of all the autoreleased objects and -release them all when it's purged at the end of the run loop. Because of all this "bookkeeping" it has to do, autoreleasing is inefficient. Many experienced Cocoa programmers will in fact recommend that you autorelease only when you have to in order to keep complex programs responsive.

So is there no way to have your cake and eat it too? Is it always a choice between efficiency and ease-of-coding? Let's look at a few of the general issues facing programmers working on memory management systems.

Fragmentation, Efficiency, Concurrency

Three of the biggest problems memory management has to overcome are fragmentation, efficiency, and concurrency. Fragmentation will be familiar to you if you've ever had to "defrag" a hard drive. In short, it's the tendency for storage, be it memory or a drive, to go from an ordered state to an unordered one.

Why is it bad? Fragmentation means that for purposes of memory management, you have to keep large, slow lists of memory blocks around, just like the autorelease pool does. In a perfect world, objects would arrange themselves nicely so you could just get rid of a whole group of them at once by clearing a single, large, specific section of memory. Unfortunately, our world is not so perfect, and repeated allocations and deallocations compounded by the realities of virtual memory can lead us right into the next point: a lack of efficiency.

Memory operations, whether they're allocations or deallocations, are among the most commonly run operations in all but the simplest programs. Even simple matters like adding an object to an array ends up involving several allocations; the object and array both have to be allocated, and then the array has to add the space required for the object you add. And because modern operating systems feature memory protection to keep them from crashing as well as virtual memory, there's overhead inherent to all of these operations.

In complex programs, it can be beneficial to write code to grab a single large section of memory from the system all at once, and then do smaller allocations from it yourself; this effectively means you're doing it all yourself, however, and as such is not for the faint of heart. And doing it yourself leads us heavily into our third issue: concurrency.

Along with memory protection and virtual memory, modern operating systems allow and even encourage the use of multitasking and multithreading. The first is the capacity for sharing the system's resources between multiple programs such that they're run alongside one another, concurrently. This isn't much of a problem for memory management unless you happen to be a systems programmer, but that is quite out of the scope of this article.

The second is the capacity for multiple parts of a single program to be run at the same time, and this is a much more pertinent problem to the average Cocoa developer. Multithreading is a complex issue, but the problems it presents for memory management are much like the problems it presents for anything else, except possibly worse: if two sections of code can be run at once, they can be trying to access the same memory at once, and can calmly and quietly run amok all over each other, causing your program to fail quite spectacularly. So the programmer responsible for the memory management code has to think about what operations should be made thread-safe and at what cost, because all thread safety measures cause a reduction in direct efficiency.

To answer the question, no, you can't have your cake and eat it too. There are many more balances to be found between efficiency and ease than the one provided by Cocoa's retain/release/autorelease system, but they are perhaps better the subject for a future article. If you're interested, however, I recommend that you look at MemoryManagement.org and browse the glossary for terms such as "conservative garbage collection" and "reference counting."

It's clear that memory management is a complex issue, but hopefully this article has provided you both with a handle on using it in Cocoa and an idea of what's at work (or play) behind the scenes. I hope you've enjoyed this as much as I have!

Rob Rix is a renaissance man masquerading as a specialist, and is Canadian to boot.


Return to Mac DevCenter.