Asynchronous Programming in iOS

Paul A. Jungwirth

Illuminated Computing

PDX iOS

March 2014

https://github.com/pjungwir/ios-async-talk-xcode

Restaurant Review App

bread

https://github.com/pjungwir/ios-async-talk-xcode

With just one thread

Threading primitives

Herding cats

Deadlock

Thread deadlock

Options:

Higher-Level Alternatives

Async abstractions

Rules

Our Restaurant App

// branch: master

@implementation MyAppDelegate {
  NSArray *_restaurants;
}

- (BOOL) application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [self fetchRestaurants];
}

// . . .

@end

With a timer

// branch: master

- (BOOL) application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [self fetchRestaurants];

  [NSTimer scheduledTimerWithTimeInterval:60*60
                                   target:self
                                 selector:@selector(fetchRestaurants)
                                 userInfo:nil
                                  repeats:YES];
  // . . .
}

Sync HTTP Request

// branch: sync

- (void) fetchRestaurants {
  NSURL *url = [NSURL URLWithString:kAPIRestaurantsURL];
  NSURLRequest *req = [NSURLRequest requestWithURL:url];
  NSURLResponse *resp;
  NSError *err;
  NSData *d = [NSURLConnection sendSynchronousRequest:req
                                  returningResponse:&resp
                                              error:&err];
  if (d) {
    self->_restaurants = [MyRestaurant parseJSON:d];
  }
}

Mac beach ball

Async HTTP Request

// branch: async-notification

- (void) fetchRestaurants {
  NSURL *url = [NSURL URLWithString:kAPIRestaurantsURL];
  NSURLRequest *req = [NSURLRequest requestWithURL:url];
  NSOperationQueue *q = [NSOperationQueue mainQueue];
  [NSURLConnection sendAsynchronousRequest:req
                                       queue:q
                             completionHandler:
    ^(NSURLResponse *resp, NSData *d, NSError *err) {
      if (d) {
        self->_restaurants = [MyRestaurant parseJSON:d];
      }
    }];
}

Parsing the JSON

// branch: async-notification

@implementation MyRestaurant {
+ (NSArray *) parseJSON:(NSData *)d {
  NSMutableArray *restaurants = [NSMutableArray new];
  NSError *jsonError = nil;
  NSArray *restFile =
    [NSJSONSerialization JSONObjectWithData:d
                                    options:0
                                      error:&jsonError];
  for (NSDictionary *dict in restFile) {
    MyRestaurant *r = [[MyRestaurant alloc] initWithDictionary:dict];
    [restaurants addObject:r];
  }
  return restaurants;
}

Async parsing with NSOperation

// branch: async-notification

@implementation MyParseRestaurantsOperation {
    NSData *_data;
}

- (id)initWithData:(NSData *)d {
    if (self = [super init]) {
        self->_data = d;
        return self;
    }
    return nil;
}

- (void)main {
    self.restaurants = [MyRestaurant parseJSON:self->_data];
    [[NSNotificationCenter defaultCenter]
      postNotificationName:@"ParseRestaurantsOperationFinished"
                                                   object:self];
}

@end

Async parsing with NSOperationQueue

// MyAppDelegate.m:
// branch: async-notification

- (void)fetchRestaurants {
    NSURL *url = [NSURL URLWithString:kAPIRestaurantsURL];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    NSOperationQueue *q = [NSOperationQueue mainQueue];
    [NSURLConnection sendAsynchronousRequest:req
                                       queue:q
                           completionHandler:
     ^(NSURLResponse *resp, NSData *d, NSError *err) {
         if (d) {
             MyParseRestaurantsOperation *op =
               [[MyParseRestaurantsOperation alloc] initWithData:d];
             [[NSNotificationCenter defaultCenter] addObserver:self
                             selector:@selector(parsedRestaurants:)
                          name:@"ParseRestaurantsOperationFinished"
                                                         object:op];
             NSOperationQueue *background = [NSOperationQueue new];
             [background addOperation:op];
         }
     }];
}

Getting an NSNotification from our queue.

// MyAppDelegate.m:
// branch: async-notification

- (void)parsedRestaurants:(NSNotification *)n {
    [self performSelectorOnMainThread:@selector(updateRestaurants:)
      withObject:((MyParseRestaurantsOperation*)[n object]).restaurants
                                                      waitUntilDone:NO];
}

- (void)updateRestaurants:(NSArray *)restaurants {
    self->_restaurants = restaurants;
    [((MyViewController *)self.window.rootViewController).tableView reloadData];
}

setCompletionBlock to avoid notifications

// MyAppDelegate.m:
// branch: async-completion-block

  MyParseRestaurantsOperation *op =
    [[MyParseRestaurantsOperation alloc] initWithData:d];
  [op setCompletionBlock:^{
    [self performSelectorOnMainThread:@selector(updateRestaurants:)
                                          withObject:op.restaurants
                                                  waitUntilDone:NO];
  }];
  NSOperationQueue *background = [NSOperationQueue new];
  [background addOperation:op];

Retain Cycles

Retain Cycle

Blocks and retain cycles

https://developer.apple.com/videos/wwdc/2011/

If a block is copied, then it retains whatever it closed over.

If you are in turn retaining the block (perhaps indirectly), you have a retain cycle.

Usually it's a problem with self.

Watch out closing over _ivars! (self->_op)

setCompletionBlock copies the block

setCompletionBlock docs

setCompletionBlock without memory leak: "weak-strong dance"

// MyAppDelegate.m:
// branch: async-completion-block

  MyParseRestaurantsOperation *op =
    [[MyParseRestaurantsOperation alloc] initWithData:d];
  __weak MyParseRestaurantsOperation *weakOp = op;

  [d setCompletionBlock:^{
    MyParseRestaurantsOperation *strongOp = weakOp;
    if (!strongOp) return;
    [self performSelectorOnMainThread:@selector(updateRestaurants:)
                                    withObject:strongOp.restaurants
                                                  waitUntilDone:NO];
  }];
  NSOperationQueue *background = [NSOperationQueue new];
  [background addOperation:op];

Block as NSOperation

// MyAppDelegate.m:
// branch: async-block-as-nsoperation

- (void)fetchRestaurants {
    NSURL *url = [NSURL URLWithString:kAPIRestaurantsURL];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    NSOperationQueue *q = [NSOperationQueue mainQueue];
    [NSURLConnection sendAsynchronousRequest:req
                                       queue:q
                           completionHandler:
     ^(NSURLResponse *resp, NSData *d, NSError *err) {
         if (d) {
             NSOperationQueue *background = [NSOperationQueue new];
             [background addOperationWithBlock:^{
                 NSArray *restaurants = [MyRestaurant parseJSON:d];
                 [self performSelectorOnMainThread:@selector(updateRestaurants:)
                                                          withObject:restaurants
                                                               waitUntilDone:NO];
             }];
         }
     }];
}

Do the NSURLConnection callback on a different queue

// MyAppDelegate.m:
// branch: async-urlconnection-callback-different-queue

- (void)fetchRestaurants {
    NSURL *url = [NSURL URLWithString:kAPIRestaurantsURL];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    NSOperationQueue *q = [NSOperationQueue new];
    [NSURLConnection sendAsynchronousRequest:req
                                       queue:q
                           completionHandler:
     ^(NSURLResponse *resp, NSData *d, NSError *err) {
         if (d) {
             NSArray *restaurants = [MyRestaurant parseJSON:d];
             [self performSelectorOnMainThread:@selector(updateRestaurants:)
                                                      withObject:restaurants
                                                           waitUntilDone:NO];
         }
     }];
}

Async parsing with GCD

Set up a dispatch_queue with GCD:

// branch: async-gcd

@implementation MyAppDelegate {
  NSArray *_restaurants;
  dispatch_queue_t _restaurants_queue;
}

- (BOOL)application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self->_restaurants_queue = dispatch_queue_create("com.example.restaurants", NULL);
  [self fetchRestaurants];
}

Async parsing with GCD

Do our parsing on our own queue:

// branch: async-gcd

- (void)fetchRestaurants {
  NSURL *url = [NSURL URLWithString:kAPIRestaurantsURL];
  NSURLRequest *req = [NSURLRequest requestWithURL:url];
  NSOperationQueue *q = [NSOperationQueue mainQueue];
  [NSURLConnection sendAsynchronousRequest:req
                                     queue:q
                         completionHandler:
    ^(NSURLResponse *resp, NSData *d, NSError *err) {
      if (d) {
        dispatch_async(self->_restaurants_queue, ^{
          NSArray *restaurants = [MyRestaurant parseJSON:d];
          dispatch_async(dispatch_get_main_queue(), ^{
            self->_restaurants = restaurants;
            [((MyViewController *)self.window.rootViewController).tableView reloadData];
          });
        });
      }
    }];
}

References and Further Reading

Thanks!

Paul Jungwirth

pj@illuminatedcomputing.com

[contents]

deck.rb presentation

/

#