Recently I wrote a flashcard app for a client that uses a lot of images. On my old iPhone 3G the app often gave memory warnings and in certain situations was even terminated because it could not free up enough memory. That is something you want to avoid! Newer iPhone models have more memory to spare but low memory conditions can still occur. Your app should be prepared to handle those situations.
View controllers will unload their views
If memory gets tight and you have one or more view controllers that are not currently displaying their views – for example they are hidden behind a modal view – then these controllers will unload their views. When such a view controller becomes active again, it will reload its NIB and the views contained in it.
You need to help the view controller unload its view by implementing the -viewDidUnload method. Here you release all references you keep to retained UI elements, such as buttons, labels and text views. Basically you need to release all your retained IBOutlets and set their pointers to nil. Since you’re probably using properties for these IBOutlets, it’s as simple as nilling them:
- (void)viewDidUnload
{
[super viewDidUnload];
self.myButton = nil;
self.someLabel = nil;
} |
When the view controller reloads its view, it loads its NIB again and makes new instances of all the views in it. If any of the old views are still around, then they are no longer used for anything; their pointers will be overwritten to point to the new views. So if you don’t release your retained UI elements in -viewDidUnload then they will stay in memory and you’ll have a memory leak when the view reloads.
Note that -viewDidUnload is not called when the view controller is deallocated. It is only used in low memory conditions. So you’ll have to do the same work of releasing any retained objects in -dealloc. I usually create a -releaseObjects method that I call in both.
- (void)releaseObjects
{
[myButton release], myButton = nil;
[someLabel release], someLabel = nil;
}
- (void)viewDidUnload
{
[super viewDidUnload];
[self releaseObjects];
// free up anything else here that you can get rid of
}
- (void)dealloc
{
[self releaseObjects];
// free any other objects that may live beyond the view
[super dealloc];
} |
Note that in -releaseObjects I don’t use the properties. Instead I specifically release the object and then set its pointer to nil. This is because this method will also be called from -dealloc. Many developers consider it not a good idea to call accessor methods from -dealloc (which is what happens when you use a property).
An example
It is important to realize that -viewDidUnload and -dealloc are not entirely the same. They both release view-related stuff, but typically -dealloc does more. When the view controller’s view is unloaded for memory reasons, the controller itself stays around. So you may not want to release all of your ivars in -viewDidUnload.
Let’s say we have a MainViewController that owns an NSMutableArray named “favorites”. It presents a modal table view controller, FavoritesViewController, that lets the user delete favorites from the list. Whenever the user deletes a favorite, FavoritesViewController notifies the MainViewController that the list has changed through a delegate method.
If MainViewController‘s view gets unloaded while the FavoritesViewController is showing, then we don’t want to release its “favorites” array in -viewDidUnload. We will still need to make changes to that array while the user is interacting with the FavoritesViewController. But we do want to release “favorites” in MainViewController‘s -dealloc, because then we’re truly done with it.
In code that would look something like this:
@interface MainWindowController : UIViewController
@property (nonatomic, retain) NSMutableArray* favorites;
@property (nonatomic, retain) UIButton* someButton;
@property (nonatomic, retain) NSDictionary* lookupTable;
@end
@implementation MainViewController
@synthesize favorites, someButton, lookupTable;
- (id)init
{
if ((self = [super init]))
{
self.favorites = [NSMutableArray arrayWithCapacity:10];
}
return self;
}
- (void)viewDidLoad
{
self.someButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
// This is some object that we only need while the view is showing.
self.lookupTable = [NSDictionary dictionaryWithObjectsAndKeys:.....];
}
- (void)releaseObjects
{
// We don't need these objects anymore if we don't have a view.
[someButton release], someButton = nil;
[lookupTable release], lookupTable = nil;
}
- (void)viewDidUnload
{
[super viewDidUnload];
[self releaseObjects];
// Note: we don't want to release favorites here!
}
- (void)dealloc
{
[self releaseObjects];
[favorites release];
[super dealloc];
}
// This is the delegate method.
// It may be called when our view is not loaded.
- (void)favoritesViewControllerDidDeleteFavorite:(id)favorite
{
[favorites removeObject:favorite];
if ([self isViewLoaded])
{
// Update the view...
}
}
@end |
Alternatively, you could initialize the favorites list in -viewDidLoad like this:
- (void)viewDidLoad
{
...
if (self.favorites == nil)
self.favorites = [NSMutableArray arrayWithCapacity:10];
} |
You have to check to see if it already exists because, as should be obvious by now, -viewDidLoad may be called more than once.
Another way to do this is to make the property’s getter perform lazy loading:
Now you don’t have to initialize it anywhere! Note that this requires that you refer to favorites as “self.favorites” where you want to use it, not just as the ivar “favorites”.
The rule of thumb is that -viewDidUnload should free as much data as possible. You should only keep enough state information in order to restore the view to its full state when the view controller becomes active again. In the above example, we didn’t need to keep “lookupTable” alive beyond the lifetime of the view but we did need to keep “favorites” around.
But you can do more!
Your view controllers are also notified of low memory situations with the -didReceiveMemoryWarning method. You should override this method to release any data that you do not need at that specific moment. Your app delegate also has a similar method, -applicationDidReceiveMemoryWarning, and other classes can subscribe to the UIApplicationDidReceiveMemoryWarningNotification
notification message.
The trick is to set things up so that as many objects as possible can be released when memory gets tight. But you don’t want to release them all the time, only when necessary. If you always release an object that the app might need again a second later, then this leads to a lot of extra processing and will only drain the battery faster.
For example, the flashcard app uses a CardView class. This is a UIView subclass that serves as a container view for two subviews: the back of the card and the front of the card. Initially, the back is shown. When the user taps it, the card flips over with an animation.
Should the iPhone give a low memory warning when the front of the card is showing, the app will unload the back image. We don’t need it at that point because it is not visible. Only when the next card is shown, the back image is automatically loaded into memory again.
What makes this work is “lazy loading”. Lazy loading is a good principle to use in as many places as possible. It requires a little extra logic and planning on your part, but that’s still better than an app that spontaneously crashes.
Properties and lazy loading go well together. You write the getter method yourself and make it do lazy loading. Then you always access the object through the property. In that case, if the object got unloaded out from under you, it will be reconstructed on-the-fly.
For example, here is an excerpt from the CardView class:
@interface CardView : UIView
{
BOOL showingBackView;
}
@property (nonatomic, retain) UIImageView* backView;
@end
@implementation CardView
@synthesize backView;
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
if (!showingBackView)
{
[backView removeFromSuperview];
[backView release], backView = nil;
}
}
- (UIView*)backView
{
if (backView == nil)
{
self.backView = [[[UIImageView alloc] initWithFrame:...] autorelease];
backView.image = [UIImage imageNamed:@"Some Image"];
[self.view addSubview:backView];
}
return backView;
}
- (void)showBack
{
self.backView.hidden = NO;
}
@end |
Now whenever I need to use the back view, I access it with “self.backView”. If it’s not yet loaded, it will be constructed right then and there. So we can safely release it in -didReceiveMemoryWarning if we’re not using it, and it will come back to life when we do want to use it again.
UIImage imageNamed
You probably load your images using the -[UIImage imageNamed] method. It caches the images behind the scenes. There once was a time where -imageNamed did not properly release the cached images on a low memory warning but apparently it has been behaving well for quite some time now. Unless you’re still targeting 2.x, I wouldn’t worry about it.
But if you don’t trust -imageNamed‘s caching mechanism or want more control over it, you can create a simple image cache of your own. Something like this:
@implementation MyImageCache
+ sharedInstance
{
static MyImageCache* instance = nil;
if (instance == nil)
instance = [[self alloc] init];
return instance;
}
- (NSMutableDictionary*)dictionary
{
if (dictionary == nil)
dictionary = [[NSMutableDictionary dictionaryWithCapacity:10] retain];
return dictionary;
}
- (void)dealloc
{
[dictionary release];
[super dealloc];
}
- (UIImage*)imageNamed:(NSString*)filename
{
UIImage* image = [self.dictionary objectForKey:filename];
if (image != nil)
return image;
image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:filename ofType:nil]];
if (image == nil)
return nil;
[self.dictionary setObject:image forKey:filename];
return image;
}
- (void)flushMemory
{
[dictionary release], dictionary = nil;
}
@end |
This will keep images in memory until you tell the cache to flush itself. You’d typically do that in your app delegate’s -applicationDidReceiveMemoryWarning method. Note that it loads the dictionary lazily, so no memory is wasted if you don’t use the cache.
A cache like this is useful for buttons or other controls whose state can change. A “Play” button might change into a “Stop” button while the sound is playing. If memory gets low, you can release the “Play” image. The “Stop” image will also be flushed from the cache, but the UIButton will still hold onto it as it is currently displaying this image. When you switch back to the “Play” state, the old Stop image gets freed and a new Play image is loaded into the cache.
Test in the simulator
The iOS Simulator has a menu command that lets you fake low memory conditions: Hardware > Simulate Memory Warning. This is mandatory for testing whether your code actually works! Use this and the Allocations instrument from Instruments to make sure your app can survive when it runs out of memory.
I haven’t found a way yet to trigger low memory conditions on the actual device, other than allocating big chunks of memory with malloc().