Streaming thumbnails smoothly using HTTP in your iPhone app

06 Oct 2010

HTTP Streaming

Network enabled apps such as news aggregators, blogging clients often load lots of small files, such as thumbnails, trough HTTP connection. Usually we expect that such application is: (1) responsive (does not block scrolling while loading), (2) it does load thumbnails almost realtime.

I suppose the first thing everybody tries is calling [UIImage imageWithData:[NSData dataWithContentsOfURL:someURL]]. There is nothing wrong with it, except it make your UI stutter and load images painfully slow. After trying that one may think of using background threads, NSOperationQueue, or something like that, but still any modern web browser loads those images much faster when they are embedded into regular web page than your application. Wonder why?

Man behind the bar at HTTP server

HTTP 1.1 introduces feature called persistent connections – later backported also to HTTP 1.0 in the form of an extra header Connection: keep-alive sent by the client. This is the clue of the solution here.

You may think of HTTP server working thread serving you as bartender taking your order and bring back drinks. First your connection is waiting in the queue to the bar, once bartender dispatches all orders of clients in the front of you, he asks for yours. Then (during the connection) you have the bartender (almost) exclusively serving you. Once you disconnect, the bartender focuses his attention on the next client, so if you decide to make another request short while afterwards you need to go back in the queue and wait again for your turn.

Back in HTTP 1.0 times, server was taking only one order, sending back response and disconnecting. But since HTTP 1.1 you may send many orders to the server one after the other (or even parallel). You can also stay connected while you are non sending requests, just in case you want something extra. All modern web browsers follow simple rule of trying to execute all the requests using existing persistent connection while not making more than 2-4 connections to single server.

Thats why putting your thumbnails download in background threads or NSOperationQueue does not do any better, but just pollutes remote HTTP server with many connections, that anyway will not be served altogether. Moreover the overall time you gonna spend waiting for response for all requests will be significantly longer than time use with single persistent connection sending requests one-by-one.

CFNetwork HTTP connection pooling

It is better then to keep your application single threaded, but instead use good event driven download mechanism based on persistent connections. The first candidate for that is NSURLConnection which unfortunately has some problems with keeping persistent connections to HTTP 1.0 servers via keep-alive header, even it uses internally CFNetwork. This is the reason I will focus on more low level solution directly based on CFNetwork's CFReadStream object and CFReadStreamCreateForHTTPRequest function.

CFNetwork documentation states that this framework tries to pool and reuse existing HTTP connections for newly created CFReadStream if there is still other CFReadStream connected to the same server. This implies two facts: (1) CFNetwork does queue all read requests to the same server where possible trough single connection (2) if only you keep the CFReadStream reference to the server (i.e. using global variable), so every time you open new CFReadStream, it will reuse connection kept inside previously used CFReadStream.

Below I attach ready to use implementation of Fetch class (under MIT-like license) that does all of that for you. All you need to do is to [Fetch fetchURL:someURL delegate:self tag:someTagForYourComfort]. The connection itself does retain Fetch instance and releases it once the request if over, so you do not need to retain it yourself. The delegate (also retained in case it is deallocated before request is over) should implement 3 methods:

- (void)fetch:(Fetch *)fetch didFailWithError:(NSError *)error;
- (void)fetchDidFinishLoading:(Fetch *)connection;
- (void)fetch:(Fetch *)fetch didReceiveStatusCode:(NSInteger)statusCode contentLength:(NSInteger)contentLength;

HINT #1: Once you receive didReceiveStatusCode, you should assign fetch.data property (retained) some NSMutableData instance otherwise it will not store downloaded data anywhere.

HINT #2: If you wish to cancel Fetch request you may use -[Fetch cancel].

Since iPhone/iPod Touch is pretty quick to do image decoding in main thread once you get the data, creating image with [UIImage imageWithData:fetch.data] in fetchDidFinishLoading and binding the image to UITableView cell works for me fine.

Implementation part that deserves the attention is initialization part:

- (id)initWithURL:(NSURL *)_url
         delegate:(id<FetchDelegate>)_delegate
              tag:(NSInteger)_tag
{
    // Copy properties
    self.delegate = _delegate;
    tag = _tag;

    CFHTTPMessageRef request = CFHTTPMessageCreateRequest(
                                                          kCFAllocatorDefault,
                                                          CFSTR("GET"),
                                                          (CFURLRef)_url,
                                                          kCFHTTPVersion1_1);
    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Keep-Alive"), CFSTR("30"));

    stream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request);
    CFRelease(request);
    CFStreamClientContext context = {
        0, (void *)self,
        CFClientRetain,
        CFClientRelease,
        CFClientDescribeCopy
    };
    CFReadStreamSetClient(stream,
                          kCFStreamEventHasBytesAvailable |
                          kCFStreamEventErrorOccurred |
                          kCFStreamEventEndEncountered,
                          FetchReadCallBack,
                          &context);
    CFReadStreamScheduleWithRunLoop(stream,
                                    CFRunLoopGetCurrent(),
                                    kCFRunLoopCommonModes);

    // In meantime our persistent stream may be closed, check that.
    // If we won't do it, our new stream will raise an error on startup
    // FIXME: This is a bug in CFNetwork!
    if (persistentStream != NULL) {
        CFStreamStatus status = CFReadStreamGetStatus(persistentStream);
        if (status == kCFStreamStatusNotOpen ||
            status == kCFStreamStatusClosed ||
            status == kCFStreamStatusError) {
            CFReadStreamClose(persistentStream);
            CFRelease(persistentStream);
            persistentStream = NULL;
        }
    }

    CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanTrue);
    CFReadStreamOpen(stream);

    if (persistentStream != NULL) {
        CFReadStreamClose(persistentStream);
        CFRelease(persistentStream);
        persistentStream = NULL;
    }

    streamCount++;

    return self;
}

Few notes about initialization part of Fetch instance:

  1. It forces using 30 seconds keep-alive trough CFHTTPMessageSetHeaderFieldValue

  2. It retains itself using CFReadStreamSetClient

  3. It uses main thread (current) runloop with CFReadStreamScheduleWithRunLoop

  4. It maintains persistentStream global reference to last used stream, and replaces it by its own connection (so persistentStream always points to the last one)

  5. Finally it forces persistent connections, even they may be default (or not?) using CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanTrue);`

So this is all folks. Waiting for your comments. I hope you will find this post useful.

Posted in iOS, Objective-C, Programming