WIJL: IOS Memory Management

My buddy Adam and I just emerged from a 3 evening/late-night binge of what I can only describe as hell. We were trying to fix memory problems with TumbleOn.

We learned a bit on the way, and the most painful part of it all was that learning each bullet point below took more than average amounts of searching and reading and re-reading Apple docs.

#1 – Double check that you use “self.” in assignments

When you seem to be having dealloc release problems, particularly with a bizarre error of [CALayer release] sent to deallocated instance, your problem may be that you set a property without using self., as in, you did this:

  1.  myprop = [[[thing alloc] init] autorelease]

When you should have done this:

  1.  //note the "self." here..
  2.  self.myprop = [[[thing alloc] init] autorelease]

When you don’t use the “self.”, you’re not going through the setThing() method, and thus, you’re not getting a retain added to your object. Later when you do properly use self to set to nil in your dealloc, you’re releasing one too many times, and it all goes boom.

It’s obvious, amateur hour stuff, but easy to forget. In our case, this CALayer getting the extra release was a child of a UILabel, so that may help you in your hunt.

#2 – ALWAYS create & work with UI* on the main thread

In our case we were creating UIImageView objects on a background thread (NSOperationQueue..), throwing them to the main thread, adding them as children to our main view on the main thread. We had read in Apple docs that you should never do this, and to expect explosions, and yet, there were none, so we did it anyway.

For us, this problem manifested in really odd ways. If you’ve ever actually added a UIView subview from a background thread, you’re familiar with how it will take seconds for the child to actually appear while IOS (hopefully) comes up to speed with your careless neglect for the ALWAYS-ON-MAIN-THREAD rule. We saw that, thus the throwing the UIImageView to the main thread for the last “add subview” operation, and all seemed well.

Until all was not well. For a long while we have seen TumbleOn eat entire chunks of memory in the 10MB+ per sec range, and hold it until the app is idle. For months we theorized the main run loop’s auto release just doesnt drain when the device is under heavy cpu load (such as, say, fetching dozens of images, resizing them and showing them on screen every few seconds..). We thought the autorelease was just not going at all. Then we’d stop for a while, let the device be idle, and suddenly our correctly-freed memory would be freed up again.

It turns out that just like background thread addSubView hacks, background thread UIView creation & destruction takes a bit for the main thread to catch up on. Autorelease is going all the time, it just can’t ordinarily deal with your junky code not following the rules of always creating, modifying, and doing anything with UIView objects on the main thread.

Creating our UIImageView on the main thread, and ensuring all subsequent operations on that view were on the main thread cleared up our memory problem immediately.

So, even if your app isn’t crashing, pay heed to the guideline to never mess with UIView derivatives on background threads.

#3 AutoRelease vs Release

In our bewildering hunt for why memory was so messed up, we learned quite a bit about autorelease pools. The docs on NSAutoreleasePool are ambiguous at best, but a fair bit of blog and stackoverflow reading has led us to the following theories:

Think of the autoreleasepool as a list keeper. He keeps a list of objects you’ve added to him by calling “autorelease” on those objects while that pool is the topmost pool of the pool stack. When you call drain on him, he is going to iterate through his list of objects and call release on them.

When you create your own pool, say, in a tight loop, or in a method that creates a ton of objects, you can save memory. However, there is a little work involved in that pool going through his list and calling release on all of those objects, so consider instead properly managing your own retain release cycles, without autorelease where possible.

A few easy patterns for this are:

The simple basic pattern..

  1.  Object * obj = [[[Object] alloc] init]; //retain count 1
  2.  obj.doSomething();
  3.  // done with obj now, release it..
  4.  // DON'T do this before you're done with the obj!!
  5.  [obj release];

When you need to hang on to the object, set it on a property..

  1.  self.obj = [[[Object] alloc] init];
  2.  // retain count is now 2,
  3.  //   one for the init,
  4.  //   one for the setter on the property calling retain
  5.  [self.obj release];
  6.  // retain count is now 1,
  7.  // make sure to do self.obj = nil in your dealloc..

When you need to give someone a chance to use your object without resorting to autorelease, use a delegate:

  1.  Object * obj = [[[Object] alloc] init]; //retain count 1
  3.  [someone performSelector:@selector(somemethod:) withObject:obj];
  4.  // if 'someone' wanted to hang onto the object,
  5.  // they could have..
  7.  // done with obj now, release it..
  8.  [obj release];

When returning objects from something wrapped in a local autorelease pool, you can do this:

  1.  NSAutoReleasePool * pool = [[NSAutoReleasePool alloc] init];
  2.  Object * obj = [[[Object] alloc] init];
  3.  // do stuff
  4.  [pool drain];
  5.  [obj autorelease];
  6.  // now obj is in the "parent" autoreleasepool's list
  7.  // of objects to take care of, and not our local pool that we
  8.  // just threw away..
  9.  return obj;

#4 – Rumor and other hearsay

More tidbits worth sharing.. random things we read in random places and all purely speculative..

  1. UIImage is background thread safe.
  2. IOS 5.x halves the memory available to iPad 1 apps, down to about 30MB max. Turning off notification center junk and iCloud makes this better, as does doing the “reset all” which supposedly does not actually reset any app settings, just OS innards. This may all be fixed in 5.0.1, doesn’t seem to be on our iPad 1.
  3. IOS “doesnt like throwing away UIViews”, things loaded from nibs are cached, and otherwise. Consider using a pattern like UITableView does with UITableViewCells to re-use a small number of UIView subclasses if you’re throwing lots of them around.
  4. IOS may kill your app if it sees a ton of messages queued up quickly and not much throughput. For example in our case, setting the UIImage into a right-sized UIImageView causes a invisible resize operation, which can slow down the main thread. In our case, we did a right-size resize of the UIImageView on a background thread before throwing it over the wall to the main thread. But an alternative method would be to batch up similar events with some sort of queue management mechanism, to avoid sending 1000 messages for 1000 items of interest.
  5. If you’re doing internet activity, such as calling a webservice using NSURLConnection or a library such as ASI HTTP that uses that, you may want to look into clearing out NSURLCache once in a while. There’s talk that NSURLConnection and NSURLRequest’s default behaviour is to cache each and every response it gets, which is no bueno if you’re fetching a lot of stuff that you intend to not eat memory forever. Be careful on clearing it, there’s a clear for request method that’s safer than clear all. Clear all could affect ongoing requests, such as UIWebViews loading content.

I can’t take credit for any of this except for the bit where I’m the messenger relaying the research results. Most answers and theories here were found on various stackoverflow threads, and a few answers come from a couple of great coworkers of ours who are much smarter than we are 🙂

Good luck fixing your IOS memory bug, you’ll probably need it :/