NSURLConnection with NSRunLoopCommonModes

indiantroy picture indiantroy · Apr 13, 2012 · Viewed 7.5k times · Source

I have written my own implementation of HTTPClient for my iOS app to download contents of specified URL asynchronously. The HTTPClient uses NSOperationQueue to enqueue the NSURLConnection requests. I chose NSOperationQueue because I wanted to cancel any or all on-going NSURLConnection at any point of time.

I did a good amount of research on how to implement my HTTPClient and I had two choices for executing NSURLConnection:

1) Execute each enqueued NSURLConnection on a separate secondary thread. NSOperationQueue executes each enqueued operation on a secondary thread in background and hence I didn't need to do anything explicitly to spawn secondary threads except starting my NSURLConnection in overridden start method of NSOperation subclass and running the runloop for the spawned secondary thread until either connectionDidFinishLoading or connectionDidFailWithError is called. It looks like below:

if (self.connection != nil) {
            do {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                         beforeDate:[NSDate distantFuture]];
            } while (!self.isFinished);
}

2) Execute each enqueued NSURLConnection on the main thread. For this inside the start method, I was using performSelectorOnMainThread and calling start method again on main thread. With this approach I was scheduling the NSURLConnection with NSRunLoopCommonModes as below:

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

I chose this second approach and implemented it. From my research, this second approach seemed better because it doesn't start a separate secondary thread for each NSURLConnection. Now at any point of time, in the application there could be many requests going on simultaneously and with the first approach, this means the same number of secondary threads will be spawned and will not return to the pool until the associated url requests finish.

I was under the impression that I am still running concurrently with the second approach by scheduling the NSURLConnection with the NSRunLoopCommonModes. In other terms with this approach I thought that I am using NSRunLoopCommonModes instead of multi-threading for concurrency so that the observers for NSURLConnection will call either connectionDidFinishLaunching or connectionDidFailWithError as soon as it can irrespective of what main thread is doing with UI at that point of time.

Unfortunately all my understanding proved wrong when this morning one of my colleagues showed me that with the current implementation, the NSURLConnection doesn't return until a scroll view on one of the view controllers stops scrolling. The NSURLRequest to get data is started when the scroll view is about to stop scrolling but even though it was completed before the scroll view stops calling, somehow the NSURLConnection doesn't calls back either connectionDidFinishLoading or connectionDidFailWithError until the scroll view stops scrolling completely. This means that the whole idea of scheduling NSURLConnection with NSRunLoopCommonModes on main thread to get real concurrency with UI operations (touches/scroll) proved wrong and the NSURLConnection still waits till the main thread is busy scrolling the scroll view.

I tried switching to first approach of using secondary threads and it works like a charm. The NSURLConnection still calls one of its protocol methods while the scroll view is still scrolling. This is clear because now NSURLConnection is not running on main thread so it would not wait for the scroll view to stop scrolling.

I really don't want to use first approach because it's expensive due to multi threading.

Could someone please let me know if my understanding about the second approach is not correct? If it is correct, what could be the reason for scheduling NSURLConnection with NSRunLoopCommonModes doesn't work as expected?

I would highly appreciate if the answer is little more descriptive because it is supposed to clear lot more doubts for me regarding how exactly the NSRunLoop and NSRunLoopModes work. Just to specify I have read the documentation for this many times already.

Answer

indiantroy picture indiantroy · Apr 13, 2012

It turned out that the issue was simpler than I imagined.

I was having this in the start method of NSOperation subclass

self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest
                                                              delegate:self];

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

Now the problem is the above initWithRequest:delegate: method actually schedules the NSURLConnection in default runloop with NSDefaultRunLoopMode and completely ignores the next line where I actually try to schedule it with NSRunLoopCommonModes. By changing above two lines with below worked as expected.

self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest
                                                              delegate:self startImmediately:NO];

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

[self.connection start];

The actual problem here was I must initialize the NSURLConnection using the constructor method with the parameter startImmediately. When I pass NO for the parameter startImmediately, the connection is not scheduled with the default run loop. It can be scheduled in the run loop and mode of choice by calling scheduleInRunLoop:forMode: method.

Now the NSURLConnection initiated from the method scrollViewWillEndDragging:withVelocity:targetContentOffset is calling its delegate methods connectionDidFinishLoading/connectionDidFailWithError while the scroll view is still scrolling and has not yet finished scrolling.

I hope this helps someone.