Memory management in the iPhone is a hot topic. And since tonight I’m talking about it on tonight’s monthly meetup of the French-speaking Swiss iPhone Developers group, I might as well share some tips here from my own experience.
I won’t go dive through the basics; I think that Scott Stevenson did a great job in his
“Learn Objective-C” tutorial at CocoaDevCentral, from where the image below comes. I’m just going to highlight some iPhone-specific issues here and there, and provide some hints on how to solve them.
To begin with, some important background information:
- The iPhone 3G has 128 MB of RAM, but at least half of it might be used by the OS; this might leave as little as 40 MB to your application… but remember: you will get memory warnings even if you only use 3 MB;
- The iPhone does not use garbage collection, even if it uses Objective-C 2.0 (which can use garbage collection on Leopard, nevertheless);
- The basic memory management rule is: for every [ alloc | retain | copy ] you have to have a [ release ] somewhere;
- The Objective-C runtime does not allow objects to be instantiated on the stack, but only on the heap; this means that you don’t have “automatic objects”, nor things like auto_ptr objects to help you manage memory;
- You can use autorelease objects; but watch out! Since they are not released until their pool is released, they can become de facto memory leaks for you…;
- The iPhone does not have a swap file, so forget about virtual memory. When there is no more memory, there is no more memory.
Having said this, here’s my list of tips:
- Respond to Memory Warnings
- Avoid Using Autoreleased Objects
- Use Lazy Loading and Reuse
- Avoid UIImage’s imageNamed:
- Build Custom Table Cells and Reuse Them Properly
- Override Setters Properly
- Beware of Delegation
- Use Instruments
- Use a Static Analysis Tool
- Use NSZombieEnabled
Respond to Memory Warnings
Whatever you do in your code, please do not forget to respond to memory warnings! I can’t stress this much. I have seen application crashes just because the handler methods were not present on the controllers, which means that, even if you do not have anything to clear in your controller, at least do this:
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
And you might as well respond to them on your application delegate, as follows:
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
[[ImageCache sharedImageCache] removeAllImagesInMemory];
}
For the description of the ImageCache class, continue reading
Or finally, as an NSNotification:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(whatever:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
Avoid Using Autoreleased Objects
Autoreleasing objects is easy and useful, but on the iPhone you should be careful with it. By default there is an NSAutoreleasePool instance created for you at the beginning of the main() function,
but this pool is not cleared up until your application quits! This means that during runtime, your autoreleased objects are de facto memory leaks, since they are retained until the application quits. (please see the comments below; I have experienced better performance when avoiding autoreleased objects, but my understanding of pools is misleading
I started getting a better performance from my iPhone apps when I stopped using some methods creating autoreleased objects, for example:
// Instead of
NSString *string = [NSString stringWithFormat:@"value = %d", intVariable];
// use
NSString *string = [[NSString alloc] initWithFormat:@"value = %d", intVariable];
...
[string release];
In version 2.0 of the iPhone OS there was also the problem that some “convenience methods” did not work at all; I’m sure you’ve experienced your application crashing when using NSDictionary’s dictionaryWithObjects:forKeys: and then finding out that a replacing that with initWithObjects:forKeys: made your application run just fine. The NDA did not help at the time!
This does not mean that you can’t use autoreleased objects; you should use them, for example, when you have factory methods returning objects not owned neither by the factory nor by the client calling it. You can also use autorelease pools in loops, when you need to allocate lots of small objects, but remember to release the pool right afterwards:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (id item in array)
{
id anotherItem = [item createSomeAutoreleasedObject];
[anotherItem doSomethingWithIt];
}
[pool release];
Remember to always release the pool in the same context where it was created. CocoaDev has an interesting discussion about
using NSAutoreleasePools in loops.
Oh, and please, never release an autoreleased object on the iPhone: your application will crash almost instantly.
Use Lazy Loading and Reuse
If your application consists of several different controllers embedded into each other, defer their instantiation until the last possible moment; this means in practical terms that your init method is minimalistic, and that you do more stuff when you need it.
Avoid UIImage’s imageNamed:
Alex Curylo has written
an absolutely great article about the problems with UIImage’s imageNamed: static method. It seems (and in my tests this appears to be true) that the iPhone OS (versions 2.0 and 2.1 at least) uses an internal cache for images loaded from disk using imageNamed:, and that in cases of low memory this cache is not cleared up completely (this seems to be corrected with version 2.2, though, but I cannot confirm).
Build Custom Table Cells and Reuse Them Properly
Remember to always use static NSString identifiers for your cells, which helps the UITableView class to reuse them and reduce memory consumption:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
Item *item = [items objectAtIndex:indexPath.row];
static NSString *identifier = @"ItemCell";
ItemCell *cell = (ItemCell *)[tableView
dequeueReusableCellWithIdentifier:identifier];
if (cell == nil)
{
cell = [[[ItemCell alloc] initWithIdentifier:identifier]
autorelease];
}
cell.item = item;
return cell;
}
I also avoid using NIBs when working with table cells, for performance reasons. I prefer to draw the cells through my own subclasses of UITableViewCell, themselves using overridden setters for their properties, which takes me to the next point.
Override Setters Properly
As I said above, I tend to create my own subclasses of UITableViewCell, providing a simple property through which I change the model class holding the data that the cell is supposed to show. This has the effect of changing all the values of the fields and labels to the values corresponding to those of the model instance.
Beware of Delegation
If your code is delegate of some other object which you are about to release, remember to set its delegate property to nil before releasing it; otherwise, the object might “think” that its delegate is still there, and will send a message to an invalid pointer.
Use Instruments
The “Leaks” instrument is your friend, and you should use it after you write the first line of code. Typically, I launch it every time before doing a checkin of some new code. In Xcode, select “Run / Start with Performance Tool / Leaks” and you’re done. You can use it in the simulator or on your device.
Use a Static Analysis Tool
Use the
LLVM/Clang Static Analyzer tool. This amazing tool will catch naming errors (regarding the Objective-C naming conventions) and some hidden memory leaks, which are particularly nasty when using CoreFoundation libraries (Address Book, sound, CoreGraphics, etc). You can add it to your daily build script, it’s very easy to use.
But you must use it. Enough said.
Use NSZombieEnabled
Lou Franco has posted an
excellent article about how to use NSZombieEnabled in your development cycle. The idea is to be able to find which messages are being sent to invalid pointers, referencing objects which have been released somewhere in your code. Always remember who’s the owner of your objects, and check for existence elsewhere!