Sunday, June 14, 2015

iOS RunLoop, NSURLConnection, NSURLSession

iOS provides several ways to execute task in background thread,  like operation queue or Grand Central Dispatch, usually it is not required to fully understand ios RunLoop to make the function work, but in more complex cases, understanding RunLoop is necessary to make things work as expected.

iOS runloop is similar to Windows EventLoop, its main purpose is keeping the thread from exit when it is still needed, but only actively handle the events when the events are arrived.

For the main thread, the ios system will create a RunLoop automatically after starting the app, so the application can keep running, you can attach a runloop observer to the MainRunLoop from AppDidFinishLoadWithOption to verify it using the below method. Whenever any touch event happens on the screen, the Runloop Observer will be noticed for the event.


+ (void) addRunloopObserver:(NSRunLoop*)runloop {
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0,
            ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity){
                                                                       static unsigned long count = 0;
                                                                       NSString* strAct;
                                                                       if (activity == kCFRunLoopEntry){
                                                                           strAct = @"kCFRunLoopEntry";
                                                                       }
                                                                       else if ( activity == kCFRunLoopBeforeTimers){
                                                                           strAct =@"kCFRunLoopBeforeTimers";
                                                                       }
                                                                       else if ( activity == kCFRunLoopBeforeSources ){
                                                                           strAct =@"kCFRunLoopBeforeSources";
                                                                       }
                                                                       else if (activity ==  kCFRunLoopBeforeWaiting  ) {
                                                                           strAct =@"kCFRunLoopBeforeWaiting";
                                                                       }
                                                                       else if (activity == kCFRunLoopAfterWaiting){
                                                                           strAct =@"kCFRunLoopAfterWaiting";
                                                                       }
                                                                       else if (activity == kCFRunLoopExit){
                                                                           strAct =@"kCFRunLoopExit";
                                                                       }
                                                                       else {
                                                                           strAct =@"unknown";
                                                                       }
                                                                                                                                            
                                                                       NSLog(@"activity %lu: %@", ++count, strAct);
                                                                   });
CFRunLoopRef loop = [runloop getCFRunLoop];
CFRunLoopAddObserver(loop, observer, kCFRunLoopCommonModes);

}

But if other cases related to the background thread, you will need to start the runloop listener by yourself if you need to keep the thread alive to handle the runLoop event. 

Note when you call [NSRunLoop currentRunLoop], it will create and return a RunLoop object if not existing, so have a RunLoop object for the current thread does not mean the RunLoop has been started to listen for the RunLoop event, and or when or under what condition the RunLoop will stop listening the RunLoop event, and exit the thread method. 

Pay attention when starting while loop using NSRunloop's Run:untilData or beforeDate method, there must be at least one event source already scheduled into the runloop, and the event source's runmode must include the currnt runloop's runmode, this will make the runloop method to believe it need to wait for something form the event source. Otherwise, the runloop sees there is no any event source is scheduled, and it will immediately exit and enter the Run method again to create a dead loop. Add a NSTimer or NSUrlConnection will make the runloop wait for the signal of the event. A good practice is adding the event source in NSRunLoopCommonModes, and start the RunLoop's run method in NSDefaultRunLoopMode

NSThread:
If the background task is started by NSThread's InitWithTask method, then the RunLoop listener is not started. For example, if you start a NSTimer and add it to the new thread's current RunLoop, the timer event will never fires, as the thread method will immediately exit. In order to receive the Timer event, the thread method has to start a while loop to keep listen the runloop events and also prevent the thread from exiting, until all the thread works are finished. 

  while (shouldKeepRunning){
        [loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        NSLog(@"NSLoop returned : %@", [NSDate date]);
    }

Similarly, if you starts a NSURLConnection and schedule it in the thread's runLoop as below, then the connection data delegate method will never be called as the thread already exits without holding by the Runloop's while loop. Add the above while loop in the thread method will get the response. Once the connectionDidFinishLoading is called the nsUrlConnection will remove itself from the RunLoop event source automatically.
    conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
    [conn scheduleInRunLoop:threadRunloop forMode:NSRunLoopCommonModes];
    [conn start];


NSOperationQueue
NSOperationQueue for async operation is similar to NSThread, if an operation is scheduled in a background thread by NSOperationQueue, then the NSRunLoop is not started in the worker thread.  For exmple, the NSTimer added into the operation block thread's RunLoop will not get its timer event fired.

NSURLConnection uses NSOperationQueue as alternative for RunLoop. There are two different ways to use it for NSUrlConnection. When NSURLConnection sendAsynchronousRequest method is used, it uses a block to get the response, but the method does not take a delegate parameter, so the caller cannot handle authentication or connection status event. . 
+ (void)sendAsynchronousRequest:(NSURLRequest*) request     queue:(NSOperationQueue*) queue   completionHandler:);

In order to handle authentication request and other connection delegate event with NSOperation for NSURLConnection, setDelegateQueue method is needed. The connection delegate method will be called each time on a thread method, and there is no need to use RunLoop to drive the connection event.


GCD (Grant Central Dispatch)
GCD has the same behavior as NSOperationQueue when handle async dispatch, actually NSOperation uses GCD internally, so it is expected to have the same behavior as NSOperationQueue.

However, the dispatch main queue is unique, as it needs to communicate with the existing main thread. As the thread already exists and is running, the only way to let the thread executes a block or method is injecting an event into its NSRunLoop. That is why handling the main queue or main NSOperationQueue, the task is posted into the application's main RunLoop to be executed, the task is added with mode of NSRunLoopCommonModes.


NSURLSession
Unlike NSURLConnection, NUURLSession no longer gives developer the option to use a RunLoop to handle authentication and delegate event. So developers do not need to worry about whether the RunLoop has been created and started, whether the Runloop modes match the event source, whether the RunLoop thread has exited. That is a major difference between NSURLConnection and NSURLSession. Code using NSURLConnection scheduleInRunLoop method definitely should be updated to use NSOperationQueue to handle the delegate event.

NSObject
Most [NSObject performSelector ...] posts the selector into the thread's NSRunLoop, so the target thread needs to have a NSRunLoop already started and listen for event source to work. Those methods can be used when you need to communicate with an already started long last thread, the common way to do so is the target thread starts an NSRunLoop, and caller can posting a event into its RunLoop listener to run a task, besides the application's main thread, another example is UIWebView's NSURLProtocol client thread.

[NSObject performSelectorInBackground...] method is different from other performSelector method, it just creates an background thread and runs the selector without a NSRunLoop in the target thread.

1 comment:

  1. Great article to know insight of Run loop and other related topics.

    ReplyDelete