I've been playing with the iPhone SDK recently, and for whatever reason, I'm having much more success wrapping my head around the NextStep programming model adopted by Apple years ago. Previous attempts were, shall we say, less than thrilling.
My first iPhone app is approaching beta, which means that I think the first version is done and I'm waiting on feedback from someone else before I consider submitting it to the App Store.
This morning I had a wrangle or two with memory management. That's not surprising, because memory management is one of the bĂȘte noires of programming in c or c++, and Objective-c isn't going to be that different.
Or so I thought.
After creating a generally functional version of App#1, I decided to run it through some torture testing. So I added a few buttons to the UI and wired them up to some test routines. Essentially, I simulated thousands of uses of the basic functions. There are two buttons that the user will press and three textfields, so I wired up the test buttons to call each button's target 10,000 times and one to change the value in each textfield 10,000 times and to call the various functions which use those values.
Everything worked, but Instruments told me that there was some serious memory usage going on. Not a lot of leakage (although there was some), but some serious usage. Not good. Especially since this is a simple program, without anything fancy going on, except for rolling about 14 million dice.
So I refined my tests a bit, and discovered some stuff worth recording. Others may find this useful or not, but I know I need to remember this, and writing it down will help do that, even if I never come read it again.
This is a memory leak:
dcsText.text = [[NSString alloc] initWithFormat:@"%d", 15 + (i % 10)];
This is not:
NSString *temp = [[NSString alloc] initWithFormat:@"%d", 15 + (i % 10)];
dcsText.text = temp;
[temp release];
Discussion:
I wasn't actually using the first version, but the second. But I wanted to know if the first version was going generate a memory leak, so I tried it. And confirmed that I knew how the tool was working and that the version I had written was working correctly. The first version is a memory leak because the allocated string is created and has a retainCount of 1, but it's never released to reduce the retainCount to 0, so dealloc never gets called. It might be nice if it were an AutoPtr so that it got released when it goes out of scope, but that is a big whopping can of worms that I'd just as soon not open, so we'll let it go at that. I will, however, write me a few helper routines of the second general form.
NSArray
Once I had established the validity of the above form, I went on to see if I could figure out why the app was: 1) slowing down over time; and 2) using so frickin' much RAM. I'd been pretty careful, and I was reasonably certain that everything I was allocating was being deallocated, so where was the leak? Instruments pointed me right at it. Every leaked block was associated with an Array class of some sort. I was only using two arrays: one of NSMutableArray and one of NSArray (basically I built the array using the NSMutableArray and then created an NSArray from that so that client code couldn't alter the array). So what if I just use the NSMutableArray, eliminating the creation of the NSArray? No help. What's the retainCount on the NSArray immediately after I create it? 2? 2? 2!
That's not right. It should be one. So try releasing it immediately after creation. Big time crash later in the program. Obviously something is holding onto it for some reason. Look at the documentation. Whenever you use a factory method, the allocated object is added to the autorelease pool. But the top-level autorelease pool isn't drained until the end of the program, which is why every last one of the 1 million NSMutableArray and NSArray instances are still hanging around. Obviously I need to add a nearer-scope autorelease pool. But it's recommended in iPhone programming that you limit your use of autorelease pools.
Clearly that advice is not meant for this situation. By adding a local autorelease pool where the NSArray is being created and then draining that pool in the same method which creates the NSArray, only one NSArray is kept around at a time, drastically (like, 30MBs of RAM) reducing my memory footprint. Also, incidentally, locating the spot where I was not releasing one bit of allocated memory, thus the actual memory leak.
The most commonly used function now operates nearly 100x faster than it used to, probably because of vastly decreased memory usage.
Live and Learn.
Evan, your torture-testing idea alone is worth every penny of my subscription!
I haven't played with the iPhone SDK yet, but I'm puzzled about the autorelease pool. On a regular Mac, I thought it got cleaned out frequently, by the event loop. Not so on the iPhone?
The memory leak example and counter-example look strange. Doesn't dcsText release its text member when it is dealloc'd?
Posted by: Jon Reid | 2009.06.25 at 10:01 PM
As I understand it, you're confusing autorelease and garbage collection. What an autorelease pool does is keep a pointer to whatever objects you put in it, thus guaranteeing that those objects stay around throughout the lifetime of the autorelease pool (until it is drained). The iPhone does not have garbage collection.
The problem with the memory leak example is that the [[NSString alloc] init...] pair allocates a block of memory with a retainCount of 1. But without a pointer to that block of memory, it can't be released (which reduces the retainCount by 1, deallocating the block when the retainCount reaches 0).
So, on a theoretical basis, I could do a release on dcsText.text when I'm done with it, or changing it. Which means that, actually, what I've shown isn't necessarily a memory leak: it could be lazily released. But it's not automatically released.
Posted by: Evan Robinson | 2009.06.25 at 11:33 PM
OK, learning the new dot-notation for property access helps me understand. Assuming the 'text' property has been declared with the 'retain' property, then dcsText.text = foo calls an automatically generated setText: method, which does a retain. And hence the leak.
Posted by: Jon Reid | 2009.06.28 at 04:45 PM