Asynchronous Networking on iPhone, Thanks NSOperation!

With the convenience of NSURLConnection, it is rarely difficult for an app to load data from somewhere on the Internet, especially using simple GET requests. Loading large amounts of data efficiently, however, can be quite challenging. Although there are a number of solutions available to keep the application responsive while some data is downloaded, today we’ll discuss how using simple asynchronous network operations can make your app much more responsive and efficient.

An example of a very bad way to do this would be to use the sendSynchronousRequest method:

  // process some stuff and we need to download an image
  NSData* imageData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
  // process the image data

While certainly easy, this approach has the important and massive limitation that it will completely block the UI while the image data is downloading. If the image is large, or the network is slow, this may lead users to believe that their application has crashed or frozen.

This leads us to individual threads for our network operations. Since this article is aimed at developing for the iPhone, which is currently limited to a single processor with a single core, we don’t see very much gain. In fact, depending on what these background threads are doing, and how you’re managing them, they may even be very detrimental to the performance of your application! Since we are not going to be using background threads, there is much less context switching that will have to be done and we will already see a performance gain.

The other option we have for managing our URLConnections is to use the connection in an asynchronous manner. This is the default type of request when we create a new NSURLConnection:

  NSURLConnection* newConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];

When we do this, the network connection automatically schedules the downloading of all the data with the runloop and will send callbacks to a delegate as the download progresses. These callbacks signal us when the connection gets a response from the server, when new data is available and when the download has completed. It also lets us know if there was any error establishing the connection. Using asynchronous operations mitigates all our concerns with either blocking the UI and with creating lots of threads and all the issues that come with multiple threads. However, there is a downfall—managing all these network connections!

That brings us to NSOperation and NSOperationQueue—these classes make it very easy to separate and manage tasks (for example, the downloading of images from a central server). Since network-downloads are high-latency tasks, they make great candidates for migration back to the main thread. However, we still don’t want to block the application UI while images are downloading, so we have to make sure that all our network operations run asynchronously. Also, since we’re using an operation queue, it is very easy to cancel operations in progress and to adjust the number of operations we allow to execute in parallel when the user’s bandwidth changes (e.g., when the WiFi becomes available on the device).

So now for some code. The first thing we need is a subclass of NSOperation. Since we are going to run the operation on the main thread, we have to make this a concurrent operation. That signals the NSOperationQueue to start the operation on the same thread that added it to the queue. For non-concurrent operations, NSOperationQueue will spawn a new thread and run the operation in that thread context (which we definitely want to avoid on the phone).

For this example, we will call our operation PictureDownloadOperation and define a few properties. We will create an error property that we can query later if the operation fails for some reason and a UIImage property that will be set when the operation completes. I’ll also add a new designated initializer that takes the URL, making using the operation simple and versatile.

Here is the full interface for the new operation:

@interface PictureDownloadOperation : NSOperation
{
  // New properties
  NSError* error;
  UIImage* image;
 
  // In concurrent operations, we have to manage the operation’s state
  BOOL executing_;
  BOOL finished_;
 
  // The actual NSURLConnection so that we can cancel if it we need to
  NSURL* connectionURL_;
  NSURLConnection* connection_;
  NSMutableData* data_;
}
 
@property(nonatomic, readonly, retain) NSError* error;
@property(nonatomic, readonly, retain) UIImage* image;
 
- (id)initWithURL:(NSURL*)url;

When creating non-concurrent operations (the ones that run in their own thread), all we have to do is override NSOperation’s main method. Concurrent operations aren’t really any harder, but we do have to do a little more set-up work. Instead of overriding main, we will override start and a few other methods.

So the methods we must implement for our concurrent operation to work are:
- start
- isConcurrent
- isExecuting
- isFinished

For the isConcurrent method, all we have to do is simply return YES and we’re done:

- (BOOL)isConcurrent
{
  return YES;
}

The other two state methods, isExecuting and isFinished are just as simple to write, basically they will just return the values of executing_ and finished respectively. It’s important to remember, however, that these two methods must be key-value-observing compliant, so when we change either variable, we must send the appropriate willChangeValueForKey: and didChangeValueForKey: messages.

The meat of the operation happens in the start method, where we kick-off the asynchronous download of the image data. The most important thing here is that we check to make sure that the operation has not been cancelled before we start. If it has, there is nothing for us to do. For convenience, I have created another method named done that cleans up the URL connection and sets the operation to finished and not executing.

Here is the start method:

- (void)start
{
  // Ensure this operation is not being restarted and that it has not been cancelled
  if( finished_ || [self isCancelled] ) { [self done]; return; }
 
  // From this point on, the operation is officially executing--remember, isExecuting
  // needs to be KVO compliant!
  [self willChangeValueForKey:@“isExecuting”];
  executing_ = YES;
  [self didChangeValueForKey:@“isExecuting”];
 
  // Create the NSURLConnection--this could have been done in init, but we delayed
  // until no in case the operation was never enqueued or was cancelled before starting
  connection_ = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:connectionURL_]
                                                                                          delegate:self];
}

The operation should also implement all the necessary NSURLConnectionDelegate methods. If we encounter an error, we will set the error property. If all the data loads successfully, we can just set the image property. Anyone interested in knowing when the images have loaded can simply observe isFinished on the operation and all of this can be easily managed with a network queue.

Once set up, the loads will be much more efficient than using a separate thread, simple to manage, and still won’t block or interrupt the UI. For the full source code and a simple example using the operation, see our SVN repository at http://code.9mmedia.com/svn/public/iphone/async-network-example/.

Comments

6 Responses to “Asynchronous Networking on iPhone, Thanks NSOperation!”


  1. 1 Farid

    Hi,

    I’ve also tried to do that a little bit while ago.

    But why reinvent the wheel ?

    Check this out…

    http://allseeing-i.com/ASIHTTPRequest/

    Farid from Paris, France

  2. 2 Greg

    Jason,

    Thanks for taking the time to show a nice simple working example. It definitely pushed me in the right direction. With the release of IOS4, however, it looks like a small change is needed in your start method…

    if (![NSThread isMainThread])
    {
    [self performSelectorOnMainThread:@selector(start)
    withObject:nil waitUntilDone:NO];
    return;
    }

    Greg

  3. 3 Makis

    Jason,

    you are a life savior!

    I just updated to iPhone SDK 4 and this issue was driving me mad. I would be very glad if you explained the solution you gave.

    Thanks in advance.

    Makis

  4. 4 Jean-Louis

    It seems to me that your code does run on a separate thread. If I read Apple’s doc properly, “concurrent” means “run in another thread” and the “start” method is needed in that case. Otherwise you override “main”.

  5. 5 Sam Stewart

    Hi,
    Great article; thanks!

    “When we do this, the network connection automatically schedules the downloading of all the data with the runloop and will send callbacks to a delegate as the download progresses.”

    This isn’t quite right. The asynchronous download is actually happening on another thread but is *sending* messages via the runloop of the current thread to keep things synchronized. In other words, the actual download isn’t happening in the runloop, only the messages.

    Hope this makes sense!

    Sam

  6. 6 Mark

    Hi

    Great example have you tried using uibackgroundtask to keep the download going even when the user quits the app

  1. 1 Did you know? SQL Server 2008 includes Backup Compression
  2. 2 iOS 5 NSURLConnection with NSOperationQueue – Providing UI Feedback | PHP Developer Resource

Leave a Reply