To get a reasonable picture of why memory management is such an important topic, you should first have a reasonable picture of what memory management actually is. In light of that, be prepared, because we're going to cover a fair amount of ground.
By the time we're finished you should have a good understanding of memory management not only as it pertains to Cocoa but also in general, which should be helpful in a variety of circumstances as memory management is fairly ubiquitous.
The basic idea of memory management is, simplifying only a little, to keep objects (whether they be Objective-C objects or the more general "memory objects" or chunks of memory) around exactly as long as they're needed. Think about writing a program and how long each object will need to exist. In a perhaps typical document-based program, it might break down like this:
Program-lifetime objects--global instances of classes like NSApplication and NSWorkspace, as well as any data that you set up at launch time that's going to be around until the program quits. Program-lifetime refers to the fact that the objects in question have (approximately) the same lifetime as the program. These are pretty simple objects to keep around since you never have to delete them; the operating system will free up the memory when your application quits.
Document-lifetime objects--instances of classes like your NSDocument subclass, as well as the data your document class operates upon can be called document-lifetime objects because they'll only need to exist as long as the document is open.
Temporary objects--objects that are around for what is potentially a very short amount of time. These might be variables you declare inside a method, and release at the end, or inside a loop. In effect, these objects have the same lifetime as other variables declared in those methods, whether they be ints or floats or BOOLs or something else again.
These categories don't necessarily fit all kinds programs one might write, but they give a pretty good idea of how lifetimes break down. Recall that memory management means to keep objects around exactly as long as they're needed. So now let's look at a few strategies for doing that or as close to that as we can get.
Perhaps the most basic form of memory management that's still reasonably useful is malloc/free. These are two functions in the standard C library which allocate a chunk of memory and delete it (freeing it for use elsewhere), respectively.
The problem with malloc/free is pretty simple: what do you do when an object needs to be used by two chunks of code that can be run at different times? There is a very real and very dangerous possibility that one chunk of code will free the object before the other is done using it, and that it could be written over with data by a totally unconnected piece of code.
Clearly objects need some sort of indicator to let them know when everybody's done using them. One solution is the retain/release mechanism, something that will be of interest to anyone writing software with Objective-C/Cocoa since it's the mechanism Cocoa uses.
With retain/release, each object has a retain count, an unsigned integer which indicates how many chunks of code need it to exist. Use of this mechanism breaks down as follows:
Creation: methods that create an object, like
+alloc, -copy, and -mutableCopy,
always set the object's retain count to 1.
Retain: the -retain method increments the
retain count by 1. So if you've just -alloc'ed a new object,
and you retain it, its retain count will then be 2 (starts at 1, retain
adds 1).
Release: the -release method decrements the
retain count by 1. If after this operation the retain count is 0, then
-release will call the object's -dealloc
method.
Destruction: in a well-coded object, the
-dealloc method will -release all the object's
instance variables that it created/retained, and then call [super dealloc]
to give its superclass a chance to get rid of the ivars it's responsible
for. This is also a good opportunity to close any files the object has
opened and otherwise make sure that the object is completely done
with. Except in your objects' implementation of -dealloc, you
should probably never call -dealloc directly.
Now that we've got them out in the abstract, let's look at some examples of how these methods are actually used.
-(void)exampleOfRetaining
{
id object = [[SomeClass alloc] init];
NSLog(@"the retain count is %d", [object retainCount]);
[object retain];
NSLog(@"now it's %d", [object retainCount]);
[object release];
NSLog(@"and now it's %d", [object retainCount]);
[object release];
}
|
Related Reading
Cocoa in a Nutshell |
If we compile and run this code, we'll see that the object's retain count is always greater than zero. The first NSLog will tell us it's 1, the second that it's 2, and the third that it's 1 again. Why?
The first operation that touches object's retain count is id
object = [[SomeClass alloc] init]. Looking at our list of
retain/release methods again, we see that -alloc sets the retain count to
its initial state of 1. Since we don't change the retain count after that,
the first NSLog shows a retain count of 1. So far, so good.
Next we see [object retain]. Checking against our list we
see that -retain increases the retain count by 1. This matches nicely with
the second log, showing the retain count to be 2; it's as simple as 1 +
1.
After that, we see [object release]. Once again looking at
our list, we see that half the responsibility of -release is decreasing
the retain count by 1... and as we all know, 2 - 1 = 1, which explains the
last NSLog output.
But what about that last [object release] right at the
end? This line shows off the other responsibility of
-release: after it decreases the retain count by 1, it
notices that it's now 0. And since an object with a retain count of 0
doesn't need to exist any longer, it calls -dealloc, which
releases the object's instance variables and ultimately frees the memory
the object was occupying. Our object no longer exists, which is just as it
should be.
This is perfect for situations where we don't have to return an object,
but what about methods that create objects? We've already seen a few
guidelines: +alloc, -copy, and
-mutableCopy must all create a new object with a retain count
of 1. But what about all those lovely "factory"-type methods
like NSString's +stringWithFormat:?
|
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.
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.
Copyright © 2009 O'Reilly Media, Inc.