Wednesday, September 5, 2012

On ASIHTTPRequest replacement


When it comes to making HTTP requests, the most used library historically was ASIHTTPRequest. But since it's development was abandoned by it's creator many developers are seeking for a good replacement.



I have personally used ASIHTTPRequest for some time but always regretted that the library seemed over-bloated and tend to be annoyed by the obligation of adding many linked-against frameworks to my projects.

As I was rewriting many of the conceptual objects I use in my applications to deliver them as Open Source material, I fell on the ASIHTTPRequest case once more. I had made a URL loader class but it was using ASIHHTPRequest and while reviewing the use case of the class I began to think it could be worth spending at most a day and see if I could come with a self-made self-content easy to use replacement for the ASIHTTPRequest calls.

This post is the reviewed summary of this day.



NEW! The class has been updated on github! You may checkout the new features and also check this blog post on the subject: http://orion98mc.blogspot.com/2012/09/seamless-downloads-with-mccurlconnection.html 


 Spec

Before getting my hands in the programming phase I usually prefer to write the results I want to achieve. Most of the time this specifications phase consist of writing a sample code using the code I want to create. Something like:

/*

   [MCCURLConnection connectionWithRequest:[NSURLRequest requestWithURL:aURL]
                                onResponse:^(NSURLResponse *r){ ... }
                                    onData:^(NSData *chunk){ ... }
                                onFinished:^(NSError *e, NSInteger status){ ... }];


*/


All in all, this doodling lead me to this kind of spec:

- iOS 4+ (I don't support iOS 3.X anymore... Yay!)
- Asynchronous (non blocking)
- Block based. I tend to avoid delegation when possible.
- Can manage concurrency
- Can cancel requests
- Very sparse API
- Easy to use
- Lightweight

Perimeter

Since I want to be able to manage concurrency it seems straightforward to use NSOperationQueue for this. 

I need to manage the loading of an URL so NSURLConnection with NSURLRequest seems a good investigation path. Also, NSURLConnection is asynchronous and handles cancelation.

Blocks imply iOS4+

NSURLConnection

NSURLConnection is a sparse class that handles the loading of an URL with delegation. So to begin I created a NSObject subclass and added a NSURLConnection retained property to it such that I can set the delegate of NSURLConnection to the object itself. This way I can forward delegate callbacks to the callback blocks.

NSRunLoop

NSURLConnection can operate in a custom RunLoop. By default it will schedule it's activity in the RunLoop of the main thread. The problem is that if you want to run the delegate blocks in a custom thread you have two choices:

* either create a new RunLoop and schedule the NSURLConnection object inside of it
* or dispatch_async the delegate blocks from the delegate methods

I want to provide an object that can offer main-thread-free callbacks if the user requires it. I have so many times tried to stick any non GUI processing out of the main thread that this object deserves I take some time to figure out how to achieve this. So I will go for option 1, and schedule the NSURLConnection in a custom RunLoop.

Thanks to UIKit, a custom RunLoop is automatically created in a new NSThread. So, if I submit a NSBlockOperation to a custom NSOperationQueue to create the connection, the queue will spawn (or dispatch to) a new Thread to execute the operation and thus, inside this executed block, a separate RunLoop will be running.

Example of code run inside the operation block to setup the NSURLConnection:

self.connection = [[[NSURLConnection alloc]initWithRequest:request delegate:self startImmediately:NO]autorelease];
    [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [connection start];

Notice the startImmediately:NO which allow me to schedule the connection in a custom RunLoop. The current RunLoop is the one of the thread we are in.

One extra step is required to get the whole thing going. If I leave the connection like this, the thread will exit without having forward any delegate calls because nothing keeps the thread running.
To keep the current thread running let's force the RunLoop to run in the NSDefaultRunLoopMode until the connection is finished or canceled:

while (!(finished || canceled)) {
  [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
}

and they lived happily ever after

After few debugging and few refactoring the object is born.

It uses NSURLConnection and NSOperationQueue to fulfill the NSURLRequest. The full code is available here on github.

Example usage:

GET www.google.com

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com]];
[MCCURLConnection connectionWithRequest:request
                             onResponse:^(NSURLResponse *response){ NSLog(@"Got response: %@", [(NSHTTPURLResponse *)response allHeaderFields]); }
                                 onData:^(NSData *chunk){ NSLog(@"Got chunk: %@", [[[NSString alloc]initWithData:chunk encoding:NSUTF8StringEncoding]autorelease]); }
                             onFinished:^(NSError *error, NSInteger status){ NSLog(@"finished: %@ (status: %d)", error, status); }];


GET www.google.com on a separate thread

NSOperationQueue *queue = [[[NSOperationQueue alloc] init]autorelease];
queue.maxConcurrentOperationCount = 2;
 
MCCURLConnection *context = [MCCURLConnection contextWithQueue:queue onRequest:nil];
 
[context connectionWithRequest:request
                    onResponse:nil
                        onData:nil
                    onFinished:^(NSError *error, NSInteger status) { ... }];

iOS, set the network activity indicator

static int count = 0;
[MCCURLConnection setOnRequest:^(BOOL started) {
  if (started) count++;
  else count--;    
  [[UIApplication sharedApplication]setNetworkActivityIndicatorVisible:count > 0];
}];

Connection timeout and other settings...

MCCURLConnection is not magical! It doesn't set default behaviors under the hood. For example the connection timeout is set by UIKit, I think it's 60s for both OSX and iOS. You may change this behavior at NSURLRequest creation time.

Conclusion

If you wish to replace ASIHTTPRequest with MCCURLConnection you will need to add some extra code since it does only the bare minimum. I personally prefer objects with limited actions but I understand that this is not the case for everyone especially if you need to setup a project very quickly.

Anyhow, it is easy to extend MCCURLConnection with additional constructors or wrappers by adding a category. The same applies for missing delegate callbacks.

NEW! The class has been updated on github! You may checkout the new features and also check this blog post on the subject: http://orion98mc.blogspot.com/2012/09/seamless-downloads-with-mccurlconnection.html 

No comments:

Post a Comment