Understanding The iOS Main Thread

by

If there’s one rule to remember in iOS native programming, it is this: UI components can only be properly manipulated on main thread. Keep this in mind and I’m sure it will spare you from headaches in the future.

Let’s dwell deeper.

Consider the case where you want to handle a tap of a button:

UIButton *btnStart = ...;
[btnStart addTarget:self action:@selector(actionStart) forControlEvents:UIControlEventTouchUpInside];

When the button is tapped, actionStart will be called. Since this is a UI action, it’s the main thread that handles the tap event and correspondingly runs actionStart method. Once it’s finished with the method, it will continue handling other events in its queue.

What interesting in this case is the time it takes for actionStart to finish. Since all UI events are handled by only one main thread, it’s possible for actionStart to take up as much processing time as possible, leaving other events waiting. When this occurs, you’d see that your app becomes non-responsive. Any tap or swipe gestures are completely ignored. Well, they are not really ignored; they are just placed in queue and waiting to be served by main thread that’s currently busy with actionStart.

If actionStart needs to perform lengthy operation, everyone says do it asynchronously. But what does that really mean? Is the main thread involved? If it’s not, how and when can you update UI elements?

Consider the following snippet. It uses the AFNetworking library to download data from a web server.

- (void)actionStart {
 NSURL *url = [NSURL URLWithString:@"http://..."];
 NSURLRequest *request = [NSURLRequest requestWithURL:url];

 AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

 [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
  // successful… do something
 } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
  // failed… do something
 }];

 [operation start];
}

All the methods used inside actionStart are non-blocking methods. When [operation start], AFNetworking connects to the web server and download data on a background thread (not the main thread). Now, the question is, which thread will execute the success or the failure block? Is it main thread? For AFNetworking, the callbacks are called by main thread. However, keep in mind that not all libraries behave the same way. There’s no way of knowing without either the library indicating it in a document or you looking at the source code. In this case, it’s safe to add UI related code in the success and failure blocks.

Let’s modify actionStart to this:

- (void)actionStart {
 NSURL *url = [NSURL URLWithString:@"http://..."];
 NSURLRequest *request = [NSURLRequest requestWithURL:url];
 …
 NSData *response = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&urlResponse
                                                      error:&error];
 // do something
 btnCancel.hidden = YES;
}

You can immediately spot that actionStart will block until sendSynchronousRequest completes. While it’s blocking, your app can’t respond to a user’s interaction.

Let’s modify this to non-blocking by using NSOperationQueue.

- (void)actionStart {
 NSOperationQueue *q = [NSOperationQueue new];
 [q addOperationWithBlock:^{
  NSURL *url = [NSURL URLWithString:@"http://..."];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSData *response = [NSURLConnection sendSynchronousRequest:request
                                           returningResponse:&urlResponse
                                                       error:&error];
  // do something
  btnCancel.hidden = YES;
 }];
}

actionStart will complete almost immediately, freeing up main thread to do other tasks. In the meantime, NSOperationQueue will spawn a new thread to handle the block that pulls data from a web server. This version of actionStart behaves similar to the AFNetworking version. Both execute a lengthy operation using a background thread. However, if you look closely, there’s a runtime problem with this code. It’s trying to manipulate (hiding btnCancel button) a UI object in a background thread. When such a situation occurs, two things may happen and neither of them are good: nothing happens to the UI elements or your app crashes.

The easiest way to fix this is to make the UI update code into a separate method and have the main thread run it.

- (void)actionStart {
 NSOperationQueue *q = [NSOperationQueue new];
 [q addOperationWithBlock:^{
  NSURL *url = [NSURL URLWithString:@"http://..."];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSData *response = [NSURLConnection sendSynchronousRequest:request
                                           returningResponse:&urlResponse
                                                       error:&error];
  // do something
  [self performSelectorOnMainThread:@selector(hideButton) withObject:nil waitUntilDone:NO];
 }];
}

- (void)hideButton {
 btnCancel.hidden = YES;
}

You can also use grand central dispatch and execute the UI updating code in the main thread:

- (void)actionStart {
 NSOperationQueue *q = [NSOperationQueue new];
 [q addOperationWithBlock:^{
  NSURL *url = [NSURL URLWithString:@"http://..."];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSData *response = [NSURLConnection sendSynchronousRequest:request
                                           returningResponse:&urlResponse
                                                       error:&error];
  // do something
  dispatch_async(dispatch_get_main_queue(), ^{
   btnCancel.hidden = YES;
  });
 }];
}

Using Notification Center

It’s a common approach to have a global notification handling UI updates. Let’s modify the previous example and use notification to update UI.

- (void)actionStart {
 NSOperationQueue *q = [NSOperationQueue new];
 [q addOperationWithBlock:^{
  NSURL *url = [NSURL URLWithString:@"http://..."];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSData *response = [NSURLConnection sendSynchronousRequest:request
                                           returningResponse:&urlResponse
                                                       error:&error];
  // do something
  [[NSNotificationCenter defaultCenter] postNotificationName:@“hide_button” object:nil];
 }];
}

Assume we have already registered the notification handler to call the following method:

- (void)hideButtonNotification {
 btnCancel.hidden = YES;
}

Logically, this arrangement should work. A tap of the button will cause “hide_button” event to trigger. And it, in turn, will execute the handler hideButtonNofication which hides btnCancel. This works well IF the notification is posted in main thread. However, in the example, it’s actually posted by a background thread which will be the one who runs hideButtonNotification. To solve this problem, you can either post the notification in the main thread or put the UI update code in main thread.

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *