A Day In The Life

とあるプログラマの備忘録

複数のHTTPリクエストを投げるときはNSOperationを使おう

現在、iPhone SDKで簡単なマッシュアップアプリを作成中です。マッシュアップアプリなので一度の操作でいろんなところにHTTPリクエストを投げる必要があるのですが、そんな時にNSOperationを使うとものすごく便利です。NSOperationを使うことで複数のHTTPリクエストを同時並行で処理できるようになります。
使い方はいたって簡単でNSOperationクラスのサブクラスを用意して、そこでHTTPリクエストの処理をしてあげるだけです。

@interface RequestOperation : NSOperation {
  NSURL *url;
  NSMutableData *responseData;
  BOOL isExecuting, isFinished;
}
- (id)initWithURL:(NSURL *)targetUrl;
@end

@implementation RequestOperation
  :
  : KVO 系のオーバラードメソッドの実装は省略します。
  :
- (id)initWithURL:(NSURL *)targetUrl {
  self = [super init];
  if (self) {
    url = [targetUrl retain];
  }
  isExecuting = NO;
  isFinished = NO;
  return self;
}
- (void)dealloc {
  [url release], url = nil;
  [super dealloc];
}
- (void)start {
  [self setValue:[NSNumber numberWithBool:YES] forKey:@"isExecuting"];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
  if (conn != nil) {
    // iOS4 以降注意!
    // NSURLConnection は RunLoop をまわさないと並列実行モードで動かない
    do {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                                     beforeDate:[NSDate distantFuture]];
    } while (isExecuting);
  }
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
  NSLog(@"%@", @"レスポンス");
  responseData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
  [responseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
  NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
  NSLog(@"%@", responseString);
  [responseData release];
  [responseString release];
  [self setValue:[NSNumber numberWithBool:NO] forKey:@"isExecuting"];
  [self setValue:[NSNumber numberWithBool:YES] forKey:@"isFinished"];
}
@end

isConcurrentメソッドをオーバライドしてYESを返すようにしないと以下のようなエラーが出るので注意してください(デフォルトではisConcurrentはNOになっています)。

_NSAutoreleaseNoPool(): Object 0x18a140 of class NSURLConnection autoreleased with no pool in place - just leaking

isConcurrentがNOの場合同期処理しかできなくなります。NSURLConnectionクラスのconnectionWithRequest:delegate:メソッドを呼ぶと非同期のイベントが発生するのでここは必ずYESにしてください(私はこれで2時間ハマりました)。
あとはNSOperationQueueのインスタンスを生成し処理を依頼すればOKです。

@interface HogeViewController : UIViewController {
  NSOperationQueue* _queue;
}

// ボタンクリック処理
- (IBAction)buttonClicked:(id) sender;

@end

@implementation HogeViewController

- (IBAction)buttonClicked:(id) sender {
  _queue = [[NSOperationQueue alloc] init];
  // 1つ目のリクエスト
  [RequestOperation *ope1 = [[RequestOperation alloc] initWithURL:[NSURL URLWithString:@"http://www.flickr.com"]];
  [ope1 autorelease];
  // 2つ目のリクエスト
  [RequestOperation *ope2 = [[RequestOperation alloc] initWithURL:[NSURL URLWithString:@"http://www.yahoo.com"]];
  [ope2 autorelease];
  // 処理が開始される
  [_queue addOperation:ope1];
  [_queue addOperation:ope2];
}

@end

NSOperationQueueインスタンスのaddOperation:メソッドを呼び出すと処理が開始されます。1つ1つの処理は別スレッドで処理されます。

この記事の内容を元にLinkedWordという英英辞書アプリを作成しました。

この記事の内容にXMLパーサーを組み合わせて画像検索ができる英英辞書アプリを作成しました。

関連記事

ここで紹介したコードはいくつか実装を省略しています。完全な実装は以下の記事を参考にしてください

2011年7月5日追記

NSURLConnection のコールバックメソッドは RunLoop をまわさないと並列実行モードで動かないのでコードを修正しました。iOS4 で仕様が変わったみたいです。