Apr 27

The UITableView class is a wonder of efficient memory management, if you use it correctly.

Here’s the standard template code that Xcode generates when you create a subclass of UITableViewController:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    // Set up the cell...

    return cell;
}

The keys here are the CellIdentifier variable and the call to dequeueReusableCellWithIdentifier, which enable the iPhone OS to reuse existing instances of UITableViewCell whenever possible.

(Don’t create a unique reuse identifier for each row as I’ve seen some developers do. Yes, it’s much easier to deal with asynchronous download of images for each row if you know how to uniquely identify the cell, and you know that the cell is still in memory. But that totally defeats the efficient memory management that UITableView is capable of.)

Under normal circumstances a UITableView will create one instance of a UITableViewCell per row that is visible on the screen. As you scroll, the cell instance that just rolled off the screen will be reused for the cell that is about to appear.

To verify that this memory management is working as it should, add a log statement each time a new cell is created:

if (cell == nil) {
    DLog(@"creating a new cell");
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}

When you run your app and start scrolling your table view, you should not see any creation of cells beyond the initial list (plus one). If you see “creating a new cell” log statements scrolling off the screen as you scroll the table view, you’ve got a problem.

If you just follow the standard Xcode template above, you should be fine. However if you’re loading a Nib for a custom table view cell layout using Apple’s recommended way, there’s an important detail you must not forget. (Tip of the hat to Jeff LaMarche for inspiring this blog post.)

Here’s the typical NIB loading code from Apple:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"CheckedTableViewCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        DLog(@"creating a new cell");

        // Load the table view cell from a Nib file.
        [[NSBundle mainBundle] loadNibNamed:@"CheckedTableViewCell" owner:self options:nil];

        // The checkedTableViewCell property is just a temporary placeholder for loading the Nib.
        cell = checkedTableViewCell;

        // We don't need this anymore, so set to nil.
        self.checkedTableViewCell = nil;
    }

    return cell;
}

The key here is that the CellIdentifier value must also be entered into Interface Builder, like this:

UITableViewCell-Identifier.png

If you don’t do this, then UITableViewCells will not be reused. (A telltale sign of this is that you’ll see lots of “creating a new cell” log messages.) There is no compiler or runtime warning if you fail to enter this critical piece of information into Interface Builder. So that log statement can be a useful warning.

(BTW, if you’re wondering what DLog is, then see this post: The Evolution of a Replacement for NSLog.)

written by Nick \\ tags: , , , ,

Mar 27

Developing apps for the iPad using the simulator is great. Assuming you have a decent Mac development environment the simulator is super fast and has seemingly unlimited amounts of memory. What more could you ask for?

Actually it would be much better if the simulator environment was closer to the device when it comes to speed and memory limitations. Especially memory.

Apple has never published how much RAM memory there is in any of the iPhone or iPod Touch devices in the past. And of course no such details are available for the iPad. You might assume that for a larger device there would be more memory available. Guess again.

The gorgeous 768 x 1024 display is begging to be filled with full screen images. Keep in mind that an image of that size will take up approximately 3 MB of precious memory. Keep 10-15 of them around in memory and you’re half-way to getting your app terminated because of memory usage.

Today is the last day to submit iPad apps for consideration for the new iPad App Store. I predict that we’ll see quite a few apps crashing on opening day because developers have not been able to test their app’s memory usage on an actual device with real world memory limitations.

written by Nick \\ tags:

Feb 12

It is a good practice to set a variable to nil after you release it:

[myVariable release], myVariable = nil;

If you don’t do this then you may experience bugs that are difficult to track down. Say that you inadvertently refer to myVariable after it has been released. Sometimes the memory pointed to by myVariable still has the old content of your object, and your app will run fine. Sometimes that memory location has been overwritten with something else, and you’ll get an unpredictable result.

One nice characteristic of Objective-C is that you can send messages to nil objects without error. (Unlike Java, for example, where NullPointerException is probably the most common exception thrown.) So if you have set myVaraible to nil and you then try to send a message to it with [myVariable something] then it will not fail. Nothing will happen of course, but at lest nothing will happen every time, which is much easier to track down than indeterministic behavior.

One might argue that sending a message to a nil object is the result of faulty code. Your code should know if an object is valid or nil. But here’s a simple counter-example:
If your class has properties with a retain declaration, then you should release those properties in the dealloc method. In many cases setting the value of a property to nil is perfectly valid. And thus sending release to that nil property in dealloc should be ok.

Keep in mind that the above conventions apply to Objective-C. If you’re calling Core Foundation functions you need to be more careful when dealing with nil.

For example, if you call the following function with a nil parameter, it will fail:

CFRelease(contactFirstName);

Before calling CFRelease you need to check the parameter value like this:

if (contactFirstName) CFRelease(contactFirstName);

written by Nick \\ tags: , ,

Feb 05

I was seeing this error message when testing an app on a device:

Mashable(1870,0x3940f7e0) malloc: *** mmap(size=3802099712) failed (error code=12)
*** error: can't allocate region
*** set a breakpoint in malloc_error_break to debug

Trying to allocate 3.8 GB of memory on an iPhone isn’t likely to succeed, although the simulator is all to happy to comply.

After some debugging I found this code:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSInteger numRows;

    NSArray *headlines = [[BlogService sharedInstance] headlines];
    if (headlines) {
        numRows = [headlines count];
    }

    return numRows;
}

Ouch! Returning a variable that has not been initialized and is therefore pointing to a random location in memory is not going to lead to anything good.

Moral of the story: Always initialize your variables. Test early and often on a device.

written by Nick \\ tags: