Tips for improving performance of your iOS application

Any iOS application worthy of a spot on their user’s home screen is made of 3 key ingredients: a great idea, stunning design and smooth performance. In a previous post, we shared a few guidelines to make your app look pretty. Today, we have some simple tips on how to improve the performance of your iOS application. At Pulse, we obsess over every small hiccup in the application and spend countless nights staring at Instruments at the end of our release cycles. Here are some of our insights that might help you in your development process.

Downsize your image assets

Apps with good visual design always delight users. To achieve pixel perfect graphics, every iOS application ships with several image assets. It is crucial that these images are as small in size as possible. Let me elaborate with an example.

It is common practice to add a button to a nib file and set its background to point to an image. When the nib file is read from disk, iOS instantiates all the individual objects in the file, including that button. When it notices that the button’s background points to an image, it reads the image from disk, inflates it in memory and renders it as the background. The bigger the image, the slower it is to read it from disk. Since all this happens synchronously on the main thread, it slows down the app. Tip #1: Once you are satisfied with an asset, remember to always compress it to the smallest size possible, without any loss in quality, before adding it to the bundle. As a rule of thumb, I have always been able to compress icons down to at most 4kb on disk. Check out Core Animation in Practice, Part 2 from WWDC 2010 for more info on optimizing graphics on screen.

Defer main thread operations

It goes without saying that any task that doesn’t need to be executed on the main thread should be shipped to a background thread. NSOperationQueues or Grand Central Dispatch are two great tools for such tasks. With tasks running on the main thread, you need to be very careful that they don’t interfere with a user’s touches. Such tasks can be roughly classified into two groups:

  • View Updates: Any changes to your views need to happen on the main thread. iOS makes it very easy to defer these changes by the simple, do not call us, we’ll call you rule – Never call drawRect yourself. Just call setNeedsDisplay and iOS will re-render your view when the user has stopped scrolling.
  • Processing: There are some critical processing tasks that cannot be performed on a background thread, like saving a Core Data database, changing in-memory state, etc. Tip #2: Group such tasks into independent chunks and execute them in the Default Runloop mode. Eg:
[self performSelectorOnMainThread:@selector(processDataOnMainThread:)
withObject:dictionaryOfParameters
waitUntillDone:NO
modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]

When the user starts scrolling a scrollview or a tableview, the run loop mode is set to the Common modes. When the user stops scrolling, it is reset to the Default mode. Thus, if you use the vanilla [self processDataOnMainThread:dictionaryOfParams] call, the function will start executing regardless of whether the user is scrolling or not. But, with the API call above, iOS will wait for the user to stop scrolling before executing your function.

Avoid Memory Spikes

Every iOS developer dreads the ominous “Low Memory Warning”. In addition to being delivered if the app uses a lot of memory, Low Memory Warnings can also arise if the application’s memory suddenly spikes, even though the overall memory usage is quite small. If your application’s memory doesn’t go down after repeated memory warnings, iOS will kill your app! Tip #3: Always strive to keep your memory profile smooth. Some typical hot spots for memory spikes are:

  • App Launch: Load as few objects as you need. This will speed up launch and prevent memory warnings!
  • View Controller Initialization: New view controller objects are instantiated when they are pushed on the navigation stack or presented modally. Try to use as few views as possible. Or instantiate some views lazily, if you can.
  • UIWebview: UIWebview is notorious for using up a lot of memory very quickly, especially when loading HTML content with heavy images/videos. Its hard to completely control the memory profile with a UIWebview in your application, but loading data lazily is always a good rule of thumb.

Remember, If you keep your application’s memory profile steady and consistent, it will lead a long and healthy life! Check out Advanced Memory Analysis with Instruments for more info.

Avoid unnecessary caching of images

Throughout an iOS application, we need to refer to images in the bundle. More often than not, imageNamed: is an extremely simple and efficient way to do so. But, you should be aware that imageNamed: also caches any image it imports from the bundle. Thus, it is highly efficient for images that need to be reused throughout your application (like icons, background images for buttons etc.). But it can be an unnecessary memory hog for images that are used sparingly. Tip #4: For loading such images, we should instead read them directly from disk and release the memory when we are done using the image.

NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];

[image release];

As a rule of thumb, use imageNamed: with images that are used in UI elements and initWithContentsOfFile: for everything else. Here is a handy category we wrote on UIImage that automatically chooses the right image for retina display screens and reads them from disk.

UImage+ImageNamedFromDisk.h
UImage+ImageNamedFromDisk.m

I hope you find these tips useful in your own development. Please share your own insights into optimizing iOS applications by leaving comments below!

Concurrent Downloads using NSOperationQueues

Most iOS applications have to download data from the internet. Being a mobile developer, sparse resources, limited bandwidth and user-responsiveness needs make this a very interesting problem to tackle. If you are lucky, you might just have one url that you ping to download all the data for your application. But usually, one has multiple urls to download from simultaneously. Since Pulse aggregates your news from multiple sources, this is a particularly important part of the application. After a long search for an elegant and efficient solution for this problem, we discovered that NSOperations and NSOperationQueues make managing simultaneous downloads very easy. This post lists some of our learnings and gives code samples on how to implement it in your own app.

Why NSOperationQueues?

Ideally, you never want to block the main thread that handles user input. Hence, downloading data has to happen in a background thread. We have found that NSURLConnection is an excellent class to download data asynchronously. But, in order to maintain multiple connections simultaneously, one usually has to care about 3 features: (1) Throttling the number of simultaneous downloads (2) Prioritizing connections over one another (3) Easy cancellation and cleanup of such connections. These functions require a lot of bookkeeping that can get quite tedious. NSOperationQueues are extremely helpful in such situations.

An NSOperationQueue is essentially a pool of threads each of which runs a task described by NSOperation objects. It is extremely easy to wrap an asynchronous NSURLConnection in an NSOperation, as we shall see in the next section. Each NSOperation object can be given a priority and added to the queue.

[myOperation setQueuePriority:NSOperationQueuePriorityVeryHigh];
[operationQueue addOperation:myOperation];

An NSOperationQueue object allows you to specify the number of threads is should use and you can easily kill all operations.

[operationQueue setMaxConcurrentOperationCount:3];
[operationQueue cancelAllOperations];

Based on system resources and operation priorities, an NSOperationQueue runs all its operations till they finish. You can pause and resume it at any time, giving you complete control over running tasks in the background with minimal lines of code.

[operationQueue setSuspended:YES];
for (operation in newOperations) {
  [operationQueue addOperation:operation];
}
[operationQueue setSuspended:NO];

Asynchronous downloads using NSOperations

To wrap an NSURLConnection object in an NSOperation, we need to create an NSOperation subclass. When the operation starts, we can initiate an NSURLConnection and implement its delegate methods to collect downloaded data. An NSOperation uses 3 key variables to define its state: isExecuting, isFinished and isConcurrent. Since we want our downloads to run in parallel, we set isConcurrent to YES. Both isExecuting and isFinished need to be tracked in a key-value coding compliant manner. The operation is only considered finished when the isFinished property changes to YES. Check out DownloadURLOperation.h and DownloadURLOperation.m to learn more on how this is done.

New Subclass in Action

Now that we have our DownloadURLOperation subclass in place, we add its objects to operation queues and start downloading data. If an operation queue is empty, an operation starts executing as soon as it is added to the queue.

downloadOperation_ = [[DownloadURLOperation alloc] initWithURL:url];
// Add an observer to get notified when the download finishes
[downloadOperation_ addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew context:NULL];
[queue addOperation:downloadOperation_];

To process the downloaded data after an operation is finished, we need to observe KVO notifications.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)operation change:(NSDictionary *)change context:(void *)context {
  if ([operation isEqual:downloadOperation_]) {
    [downloadOperation_ removeObserver:self forKeyPath:@"isFinished"];
    [downloadOperation_ release];
    downloadOperation_ = nil;
    NSData *data = [downloadOperation_ data];
    NSError * error = [downloadOperation_ error];
    if (error != nil) {
      // handle error
    } else {
      // process data
    }
  }
}

Whenever necessary, remember to cancel the operation.

- (void)dealloc {
  if (downloadOperation_ != nil) {
    [downloadOperation_ removeObserver:self forKeyPath:@"isFinished"];
    [downloadOperation_ cancel];
    [downloadOperation_ release];
    downloadOperation_ = nil;
  }
  [super dealloc];
}

Benefits

Here is a quick summary of a some important benefits of wrapping NSURLConnections in NSOperations and using NSOperationQueues to manage simultaneous downloads.

  1. Throttling: NSOperationQueue allows you to set the maximum concurrent operations allowed to run. Thus, we can fine tune this number based on device performance and network constraints.
  2. Chained Downloads: If you need multiple downloads to happen one after the other in a sequence, using NSOperations allows you to execute chained downloads easily in a single class.
  3. Code Cleanliness: This pattern allows you to encapsulate downloading and processing data in the same class. For example: YAJLParserOperation.h and YAJLParserOperation.m parseJSON data on the fly as it is downloaded.

Although asynchronous NSURLConnection downloads happen on a background thread, its delegates are always called in the main thread in the NSDefaultRunLoopMode. This means that the delegates would never be called when the user is touching the interface (say while scrolling a tableview or tapping buttons). Even if a connection runs inside an NSOperation, this important property is maintained since we ensure that the connection starts on the main thread.

Here is some sample code that you can play around with to learn more. Please leave comments for any suggestions or improvements.