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.
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.
Scheduling a run loop source doesn't allow the source's callbacks to run concurrently with other source's callbacks.
In the case of a network communication, things which the kernel handles, like receiving and buffering packets, happens concurrently regardless of what your app does. Then, the kernel marks a socket as readable or writable which may, for example, wake a select()
or kevent()
call -- if the thread was blocked in such a call. If your thread was doing something else, like handling scroll events, then it won't notice the socket's readability/writability until execution returns to the run loop. Only then will NSURLConnection
's run loop source call its callback, letting NSURLConnection process the socket state change, and possibly call your delegate methods.
Next is the question of what happens when a run loop has multiple sources and more than one is ready. For example, there are more scroll events in the event queue and also your socket is readable or writable. Ideally, you might like a fair algorithm to service run loop sources. In reality, it's possible that GUI events are prioritized over other run loop sources. Also, run loop sources can have an inherent priority ("order") relative to other sources.
Usually, it is not critical that, say, an NSURLConnection
be serviced instantly. It's usually OK to allow it to wait for the main thread's run loop to get around to it. Consider that, for the same reason that NSURLConnection
's run loop source won't be serviced while scrolling, there's no way that processing it on a background thread could have a user-visible effect. For example, how would it affect the UI of your app? It would use -performSelectorOnMainThread:..
or something like that to schedule the update. But that is just as likely to be starved as the NSURLConnection
run loop source.
However, if you absolutely can't abide this possible delay, there's a middle ground between scheduling your NSURLConnection
s on the main thread or scheduling them all on separate threads. You can schedule all of them on the same thread but not the main thread. You can create a single thread that you park in its run loop. Then, where you are currently doing -performSelectorOnMainThread:...
, you could instead do -performSelector:onThread:...
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With