A Day In The Life

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

NSOperation 非並列実行モードと並列実行モードの使い分け

バックグラウンド処理を手軽に実行できる NSOperation クラスですがこのクラスをきちんと理解して使うには NSOperationQueue クラスについて理解する必要があります。
前回の記事で NSOperationQueue の使い方について説明しました。まだお読みでない方はこちらの記事を先に目を通しておくことをお勧めします。

今回は NSOperation について NSURLConnection を使ったサーバ通信プログラムを例に説明していきます。

NSOperation には2つの実行モードが存在する

NSOperation クラスには「非並列実行モード」と「並列実行モード」の2つの実行モードがあります。
非並列実行モードは処理の終了を NSOperationQueue に任せるのに対し、並列実行モードはプログラマが任意のタイミングで処理を終了させます。非並列実行モードは単純な処理しか出来ませんが、並列実行モードだと HTTP 通信処理を非同期で実行したり、XML 文書を SAX パーサで処理するなんてことも出来ます。

モードによる実装方法の違い

NSOperation クラスの構造は以下のようになっています。
NSOperationクラス図
モードの違いにより NSOperation のサブクラス実装方法が異なります。それぞれのモードでオーバライドする必要のあるメソッドは以下の通りです。

非並列実行モード
  • main
並列実行モード
  • start
  • isConcurrent
  • isExecuting
  • isFinished

非並列実行モードは出来ることが限られているぶん実装が単純です。並列実行モードいろいろ出来ますが実装は少し複雑です。

非並列実行モードの実装例

非並列実行モードで HTTP 通信をするプログラムの例です。コンストラクタで接続先 URL を受け取り main メソッドで処理を行います。

@interface HttpOperation : NSOperation {
    NSURL *url;
}
- (id)initWithURL:(NSURL *)targetUrl;
@end

@implementation HttpOperation
- (id)initWithURL:(NSURL *)targetUrl {
  self = [super init];
  if (self) {
    url = [targetUrl retain];
  }  
  return self;
}
- (void)dealloc {
  [url release], url = nil;
  [super dealloc];
}
- (void)main {
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSURLResponse *response = nil;
  NSError *error = nil;
  NSData *data = [NSURLConnection sendSynchronousRequest:request 
      returningResponse:&response 
                            error:&error];
  if (error == nil) {
    NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@", responseString);
    [responseString release];
  }
}
@end

実行例はこんな感じです。

NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
HttpOperation *ope1 = [[HttpOperation alloc] initWithURL:[NSURL URLWithString:@"http://www.flickr.com"]];
HttpOperation *ope2 = [[HttpOperation alloc] initWithURL:[NSURL URLWithString:@"http://www.yahoo.com"]];
[queue addOperation:ope1];
[queue addOperation:ope2];
[ope1 release];
[ope2 release];

NSOperation と Key-Value Observing

並列実行モードの実装例の前に Key-Value Observing (キー値監視、以下 KVO と略します)について理解する必要があります。 NSOperation は KVO の仕組みを使って処理の実行や終了を監視をします。
KVO とはオブジェクトのプロパティの値を監視する仕組みです。この仕組みを使うとオブジェクトのプロパティに変更がされたときに通知してくれます。

NSOperation は並列実行モード、非並列実行モードともにこの仕組みを使っています。並列実行モードは KVO のことをプログラマがあまり意識しなくても実装できるようになっています。
一方、非並列実行モードの場合は KVO に関連した以下のメソッドを実装する必要があります。

  • automaticallyNotifiesObserversForKey
    プロパティの値が変更されたら通知を送るように設定します。実装例ではisExecuting プロパティと isFinished の値が変更されたら通知を送るように設定しています。
  • isConcurrent
    処理を非同期で実行するかどうかを設定します。実装例では YES を返すように実装します。YES に設定しないとメインスレッド以外で動かなくなります。
  • isExecuting
    処理が実行中かどうか判定するメソッドです。
  • isFinished
    処理が終了したかどうか判定するメソッドです。

並列実行モードの実装例

コンストラクタで URL を受け取り start メソッドで処理を行います。NSURLConnection クラス connectionWithRequest メソッドを呼ぶと非同期で HTML ページの読み込みが開始されます。

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

@implementation HttpOperation
// 監視するキー値の設定
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString*)key {
  if ([key isEqualToString:@"isExecuting"] || 
      [key isEqualToString:@"isFinished"]) {
    return YES;
  }
  return [super automaticallyNotifiesObserversForKey:key];
}
// YES を返さないとメインスレッド以外で動かなくなる
- (BOOL)isConcurrent {
  return YES;
}
- (BOOL)isExecuting {
  return isExecuting;
}
- (BOOL)isFinished {
  return isFinished;
}
- (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 connectionWithRequest:request delegate:self];
  if (conn != nil) {
    // NSURLConnection は RunLoop をまわさないとメインスレッド以外で動かない
    do {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    } while (isExecuting);
  }
}
// レスポンスヘッダ受け取り
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
  responseData = [[NSMutableData alloc] init];
}
// データの受け取り
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
  [responseData appendData:data];
}
// 通信エラー
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
  NSLog(@"%@", @"エラー");
  [self setValue:[NSNumber numberWithBool:NO] forKey:@"isExecuting"];
  [self setValue:[NSNumber numberWithBool:YES] forKey:@"isFinished"];
}
// 通信終了
- (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

start メソッドで処理開始を通知するため isExecuting プロパティの値を YES に変更します。

[self setValue:[NSNumber numberWithBool:YES] forKey:@"isExecuting"];

connectionDidFinishLoading メソッドまたは connection: didFailWithError: メソッドで処理を終了させるので isExecuting プロパティの値を NO に isFinished プロパティの値を YES に変更します。

[self setValue:[NSNumber numberWithBool:NO] forKey:@"isExecuting"];
[self setValue:[NSNumber numberWithBool:YES] forKey:@"isFinished"];

このようにすることで NSOperationQueue に処理の実行中と終了を通知することができます。isExecuting = YES や isFinished = YES のような書き方だと通知されないので注意してください。
なお実行例は非並列実行モードの時と同じなので省略します。

NSOperation で定義した処理の終了を監視する

例えば 処理の前にプログレスバーを表示して処理が終わったらバーを消すような場合、KVO を使って isFinished プロパティを監視することで実現できます。非並列実行モード、並列実行モードともに実装方法は同じです。

- (void)doSomething {
  NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
  HttpOperation *ope1 = [[HttpOperation alloc] initWithURL:[NSURL URLWithString:@"http://www.google.com"]];
  // キー値監視を追加する
  [ope1 addObserver:self forKeyPath:@"isFinished" 
                     options:NSKeyValueObservingOptionNew context:nil];
  HttpOperation *ope2 = [[HttpOperation alloc] initWithURL:[NSURL URLWithString:@"http://www.yahoo.com"]];
  // キー値監視を追加する
  [ope2 addObserver:self forKeyPath:@"isFinished" 
                     options:NSKeyValueObservingOptionNew context:nil];
  [queue addOperation:ope1];
  [queue addOperation:ope2];
  [ope1 release];
  [ope2 release];
  // プログレスバー表示処理省略...
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object 
                                       change:(NSDictionary*)change context:(void*)context {
  NSLog(@"%@", @"Operation end");
  // キー値監視を解除する
  [object removeObserver:self forKeyPath:keyPath];
  // プログレスバーを閉じる処理省略...
}

main メソッドの処理が終わると ope1 オブジェクトの isFinished プロパティの値が変わります。isFinished プロパティの値をKVOで監視し通知を受け取ります。