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];
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.
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.