Now that the veil of secrecy has been lifted, I think it’s time to share at least one bit of information that I’ve gleaned whilst writing my new iPhone application: how to do fast tables.
NOTE: If you’re not already familiar with how UITableView works, the rest of this might just sound like jibberish. But I think there’s still valid life advice in here, so please read on!
In fact, one of the reasons I even started to write my app was to prove that I could write an app that does super-fast tables. And in my app (which, yes, is still not-to-be-named), I even do my own rich text layout for every cell. And every cell is a different height. Even I was skeptical that I’d be able to pull of fast tables, but it worked out. Phew!
How did I accomplish this? Well, by applying a lot of what I’ve learned over the years. And learning a bit about how UIView performs.
Use Views Sparingly
I’m not quite sure what’s going on in UIView. Maybe it’s that all views are backed by Core Animation layers, but drawing using a bunch of views is a sure-fire way to slow yourself down for straight on scrolling performance.
For simple table views (settings, or profiles, etc.) you probably can get away with using views with subviews. But we want smoothness as the user is scrolling through hundreds of items. In these cases, if you need to write a custom cell, you should use one view that draws everything directly.
Use Opaque Views
If you have a transparent cell, this can slow you down. If a view is marked opaque, the view system knows it doesn’t have to render the area behind it and blend your view onto it. This seems to make a marked difference at times. I just ran into this myself and switched to filling the view with the background color of the table and it is noticeably smoother now.
Note that this can work against you if you want to use standard cell highlighting, as your item won’t turn blue since the standard highlighting relies on transparency. But it does work great for instances where the items aren’t clickable. In my case, it was a message view modeled after the SMS application.
Be Lazy and Cache Often
Quite simply: don’t do anything until you don’t need to do it. And when you do need to do it, try to do it only once.
As I mentioned I render rich text. Sadly, there’s no built-in way to do this at present, so I have to lay the text out myself. This is not really cheap. So I have a layout object that lays itself out and then I keep that object around all pre-calculated. This is very much the way you’d use something like ATSUI or NSLayoutManager. You do the hard work once, then store it off for later. Then when I draw the cell, I just run along the lines of text and render them. I also calculate the rectangles of everything I will draw ahead of time. Then rendering is just a quick operation.
If you are drawing images, try to cache them at the size you’ll render them. If you are resizing them every time you draw them, you’re in for a world of hurt. In my situation, they weren’t too much bigger than my target size and they’re never bigger than a certain size, so I didn’t even bother. But if the images could be any size, it’d be really important to resize and cache them someplace.
Another thing I needed to do was cache the heights of the cells. If you have a table with variable-height cells, the table view will run through every item in the table and ask it how high it is before it renders anything. In my situation, with all the layout I do, that is pretty costly (it can actually take up to 2 seconds on the device). So I cache the heights. As new items come in and go out of my list, I only need to calc the new items, so after the first reload, all subsequent reloads are really fast since I already have the values.
Another cache I use is for the background for my cells for one of my ‘looks’. In this case, I use
[UIImage stretchableImageWithLeftCapWidth:topCapHeight:] to give me an image I can stretch across the background. BTW, this is one of the most useful image functions in there. It basically takes an image and separates into a piece that acts as an end cap and a stretchable part. There’s effectively a one-pixel band that gets repeated as it stretches. But I digress…
What I ended up doing was taking these images and, because my cells could only be certain heights, as I knew I was using one font size and the cells would be based on the number of lines, there were only a finite number of heights I’d need (I believe 5). So as I encountered the heights, I created a cached version of the stretched image using code like:
UIImage* background = [[self balloonImage] stretchableImageWithLeftCapWidth:0 topCapHeight:35]; UIGraphicsBeginImageContext( CGSizeMake( rect.size.width, rect.size.height ) ); rect.origin.x = rect.origin.y = 0; [background drawInRect:rect]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();
And then I just take that image and store it in the cache. Very simple. I also stored that as the class level so all views can use the cache.
Nothing’s ever free, of course, so keep in mind that if you cache too much, you might find yourself running out of memory. Be prepared to listen to memory warnings via the
didReceiveMemoryWarning method and dump your cache if needed.
And finally, also keep in mind that the table view only keeps the visible cells around. Every time a cell comes into view, it asks you for a new cell. You can reuse cells, but even that only will get you so much. So don’t assume that once your list has completely been scrolled through that everything is loaded. You cannot rely on such behavior. This piece of information caused me to change my strategy a bit until I arrived at what I am describing here.
How Do You Know What To Cache?
Fortunately, I’ve done this for a while, and I know a lot of the gotchas. But no matter how much you know or think you know, the best way to get real answers is to use a tool like Instruments. It is indispensable for finding bottlenecks in your code. For my app, I just kept sampling my application and finding things I should work on. If you aren’t using sampler or something similar, you really should.
Next time I might cover performance in general. How to make your applications fast and not impede the user-experience. I’ll also talk about how to handle low-memory. This can be a bit tricky at times.
UPDATE: Since writing this I’ve sped things up a little bit by going one step further.