NSOperationSample
以下は、Mac OS X 10.5.8 上の Xcode 3.1.4 で説明しています。サンプルコードのバージョンは1.2です。
1 ReadMe.txt の日本語訳
「NSOperationSample」は、NSOperation と NSOperationQueue クラスを使う方法を実演する Cocoa のサンプルアプリケーションです。
NSOperation クラスは、単一のカプセル化された作業 (task) の実行を管理します。工程 (operation) は、通常、工程キュー (operation que) オブジェクト(NSOperationQueue クラスのインスタンス)にそれらを追加することによって予定に組み込まれますが、その start メソッドを明示的に呼び出すことによって、それらを直接実行することもできます。工程は、キャンセルされるか、あるいは、実行を完了するまで、工程キュー内にとどまります。
1.1 サンプルの必要条件
提供されている Xcode プロジェクトは、Mac OS X 10.5 以降で走っている Xcode v3.0 を使って作成されています。プロジェクトは、ユニバーサルバイナリを作成するでしょう。
1.2 サンプルについて
「NSOperationSample」は、特定の画像ファイルをファイルシステム内で検索することによって、NSOperation と NSOperationQueue クラスの使い方を実例で示します。1 つの NSOperation は、与えられたディレクトリの再帰的な検索のために作成され、別の NSOperation インスタンスは、見つかった各ファイルに対して作成され、そのファイルを調べるために使われます。それらの工程を管理するために、NSOperationQueue が使われ、そのため、ユーザーは検索を中止できます。
1.3 サンプルの使い方
単に、Xcode を使ってサンプルをビルドして実行します。画像ファイルの検索を開始するディレクトリを選択します。サンプルは、そのディレクトリ内で、すべての画像ファイルを再帰的に検索するでしょう。「Stop(中止)」をクリックすることで、検索を中止できます。
2 アプリケーション
まず、Xcode 上でビルドして実行してみます。アプリケーションを起動すると、次のようなウインドウが表示されます。
 |
| 起動直後 |
「Load Images...」ボタンを押すとシートが現れ、画像を検索するディレクトリを選択します。ここでは、デストップピクチャーフォルダを検索してみます。すると、以下のように検索結果が表示されます。
 |
| 検索中 |
ここで「Stop」がアクティブになっていると、その右側に進行状態が表示されていることに注意してください。ここでは、一瞬で検索が終わるため、「Stop」ボタンを押すのが難しいですが、きちんと中止ができるようになっていることが確認できます。
3 ファイル構成
プロジェクトを構成しているファイルは、以下のようになります。
 |
| ファイル構成 |
4 MainMenu.nib
MainMenu.nib を開くと、以下のようになります。
 |
| MainMenu.nib |
ここで、MyWindowController がインスタンス化されていることに注意してください。これにより、アプリケーションが起動し、MainMenu.nib が読み込まれた時に、インスタンスが作成されることになります。また、これは MainMenu.nib 内でアプリケーションの委任として設定されています。さらにウインドウの委任にも設定されています。
MyWindowController に対する接続は、以下のようになります。ウインドウ内のコントロールにアウトレットが接続されているのがわかります。このように、主な動作をつかさどっているのが MyWindowController クラスであることが判ります。
 |
MyWindowController の接続 |
5 MyWindowController クラス
5.1 インターフェイス宣言
MyWindowController.h
@interface MyWindowController : NSWindowController
{
IBOutlet NSTableView *myTableView; // 画像パスを保持するテーブル
NSMutableArray *tableRecords; // テーブルのデータソース
IBOutlet NSProgressIndicator *myProgressInd;
IBOutlet NSButton *myStartButton;
IBOutlet NSButton *myStopButton;
NSOperationQueue *queue;
// NSOperation のキュー(1つがファイルシステム解析に、2個以上が画像ファイルの読み込みに)
NSTimer *timer; // 進行表示のための更新タイマー
NSMutableString *imagesFoundStr;
// 見つかった画像の数を表示(NSTextField がこの値にバインドされている)
}
- (IBAction)startAction:(id)sender;
- (IBAction)stopAction:(id)sender;
@end
コメントがついてないものは簡単にわかると思います。
5.2 初期化
アプリケーションが起動され、MainMenu.nib が読み込まれた時、このクラスのインスタンスが1つ作成されます。その時に init メソッドが呼ばれます。
MyWindowController.m > init
- (id)init
{
[super init];
queue = [[NSOperationQueue alloc] init];
tableRecords = [[NSMutableArray alloc] init];
return self;
}
ここで、工程キューと、テーブルのデータソースとなる変更可能な配列が割り当てられて初期化されます。これに対応するのが dealloc で、そこでこれら 2 つが割り当て解除されます。
MyWindowController.m > dealloc
-(void)dealloc
{
[tableRecords release];
[queue release];
[super dealloc];
}
インスタンスが割り当てられ、初期された後で、nib ファイルの読み込みが完了し、awakeFromNib が呼ばれることになります。
MyWindowController.m > awakeFromNib
- (void)awakeFromNib
{
// NSOperation: "LoadOperation" によって画像ファイルが読み込まれた時の通知に登録
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(anyThread_handleLoadedImages:)
name:LoadImageDidFinish object:nil];
// テーブル行のダブルクリックが "doubleClickAction" を呼び出すことを確実にする
[myTableView setTarget:self];
[myTableView setDoubleAction:@selector(doubleClickAction:)];
}
ここでは、画像ファイル追加時の通知の登録と、テーブルの設定を行っています。サンプルが起動した後、ユーザーが「Load Images...」ボタンを押すと、このクラスのインスタンスに startAction: が送られます。
5.3 startAction: メソッド
startAction: メソッドは、以下のように実装されています。
MyWindowController.m > startAction:
- (IBAction)startAction:(id)sender
{
NSOpenPanel* openPanel = [[NSOpenPanel alloc] init];
[openPanel setResolvesAliases:YES];
[openPanel setCanChooseDirectories:YES];
[openPanel setAllowsMultipleSelection:NO];
[openPanel setCanChooseFiles:NO];
[openPanel setPrompt:@"Choose"];
[openPanel setMessage:@"Choose a directory that has a large number image files:"];
[openPanel setTitle:@"Choose"];
[openPanel beginSheetForDirectory:nil file:nil types:nil
modalForWindow:[self window] modalDelegate:self
didEndSelector:@selector(chooseDidEnd:returnCode:contextInfo:) contextInfo:nil];
}
まず、検索するディレクトリを選択させるためのシートを表示します。セレクタを指定しているので、ディレクトリが選択されれば、chooseDidEnd:returnCode:contextInfo: が呼ばれることになります。このメソッドを見てみましょう。
MyWindowController.m > chooseDidEnd: returnCode: contextInfo:
-(void)chooseDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
[panel orderOut:self];
[panel release];
if (returnCode == NSFileHandlingPanelOKButton)
{
// ユーザーはディレクトリを選択した、画像ファイルの検索を開始
[tableRecords removeAllObjects]; // テーブルデータをクリア
[self updateCountIndicator];
[myStopButton setEnabled:YES];
[myStartButton setEnabled:NO];
[myProgressInd setHidden:NO];
[myProgressInd startAnimation:self];
[self loadFilePaths:[[panel URL] path]]; // ファイル検索用 NSOperation を開始
// 回転進行表示のための更新タイマーを予定に組み込む
[self setTimer: [NSTimer scheduledTimerWithTimeInterval: 1.0
target: self
selector: @selector(updateProgress:)
userInfo: nil
repeats: YES]];
}
}
まず、テーブルをすべて消去し、updateCountIndicator を呼び出して、検索数をリセットしています。updateCountIndicator は、現在のテーブルの項目数から表示用の数値を設定するメソッドです。
MyWindowController.m > updateCountIndicator
// テーブル内の項目数を更新するために準備されたルーチン(いくつかの場所で使われる)
-(void)updateCountIndicator
{
// 見つかった画像の表示用文字列を設定
NSString *resultStr
= [NSString stringWithFormat:@"Images found: %ld", [tableRecords count]];
[self setResultsString: resultStr];
}}
それから、開始ボタンを隠し、中止ボタンを表示することで、ボタンの状態を変更しています。そして、隠されていた進行表示を表示させ、進行表示を開始します。
そして、このサンプルの中心となる NSOperation を開始させるためのメソッド loadFilePaths: を呼び出しています。
最後に、進行表示を定期的(1秒ごと)に更新するためのタイマーを設定し、開始しています。
loadFilePaths: は、次のようになっています。
MyWindowController.m > loadFilePaths:
-(void)loadFilePaths:(NSString *)fromPath
{
[queue cancelAllOperations];
// 前回がまだ実行中なら中止する
// GetPathsOperation を検索を開始するルートパスで開始する
GetPathsOperation* getPathsOp
= [[GetPathsOperation alloc] initWithRootPath:fromPath
operationClass:[LoadOperation class] queue:queue];
[queue addOperation: getPathsOp]; // これで「GetPathsOperation」が開始されることになる
[getPathsOp release];
// キューで保持されるので解放していることに注意、配列に追加する場合と同様
}
まず、キュー内で前回の工程が進行しているならすべて中止します。
それから、ユーザーが選択したパスで、GetPathsOperation クラスのインスタンスを確保して初期化しています。
次に、確保したインスタンスをキューへと追加しています。最後に、作成したインスタンスを解放していますが、これは配列に追加する時と同様で、追加した先で確保されるからです。
とりあえず、このクラスを一度離れて、この工程クラスがどのように実装されているかを見てみましょう。
6 GetPathsOperation クラス
6.1 インターフェイス宣言
ヘッダファイルは以下のようになっています。
GetPathsOperation.h
@interface GetPathsOperation : NSOperation
{
NSString *rootPath;
NSOperationQueue *queue;
Class opClass;
}
- (id)initWithRootPath:(NSString *)pp operationClass:(Class)cc
queue:(NSOperationQueue *)qq;
@end
インスタンス変数として、Class 型のものがあることに注意してください。rootPath は、選択されたディレクトリで、queue は、追加されることになる工程キューです。
6.2 初期化
MyWindowController クラスから初期化メソッドが呼ばれていました。これは、以下のようになっています。
GetPathsOperation.m > initWithRootPath: operationClass: queue:
- (id)initWithRootPath:(NSString*)pp
operationClass:(Class)cc queue:(NSOperationQueue*)qq
{
self = [super init];
// 工程クラスは -initWithPath: メソッドを持っていなければならない
if (![cc instancesRespondToSelector:@selector(initWithPath:)])
{
[self release];
return nil;
}
rootPath = [pp retain];
opClass = cc;
queue = [qq retain];
return self;
}
渡された工程クラスが -initWithPath: メソッドを持っているかどうかをチェックして、そうでなければ、自身を解放しています。メソッドがあれば、渡されたパスとキューをインスタンス変数に格納して保持します。この 2 つは、dealloc メソッドで解放されることになります。
6.3 main メソッド
GetPathsOperation クラスは、「単純な NSOperation サブクラスの定義」で説明されているように main メソッドだけを実装します。この工程が実行される時には、このメソッドが呼ばれることになります。
GetPathsOperation.m > main
- (void)main
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 独自の自動解放プールを作る
// 「rootPath」の内容で繰り返し
NSString* sourceDirectoryFilePath = nil;
NSDirectoryEnumerator* sourceDirectoryFilePathEnumerator
= [[NSFileManager defaultManager] enumeratorAtPath:rootPath];
while (sourceDirectoryFilePath = [sourceDirectoryFilePathEnumerator nextObject])
{
if ([self isCancelled])
// 定期的にキャンセルされたかチェック
{
break; // ユーザーはこの工程をキャンセルした
}
NSDictionary *sourceDirectoryFileAttributes
= [sourceDirectoryFilePathEnumerator fileAttributes];
// ファイル属性ディクショナリを取得
NSString *sourceDirectoryFileType
= [sourceDirectoryFileAttributes objectForKey:NSFileType];
// 取得したディクショナリから、ファイルタイプを取得
if ([sourceDirectoryFileType isEqualToString:NSFileTypeRegular] == YES)
// 通常ファイルなら
{
NSString *fullSourceDirectoryFilePath
= [rootPath stringByAppendingPathComponent:sourceDirectoryFilePath];
// 完全パスを作成
if (fullSourceDirectoryFilePath)
// パスが作成できたら
{
// NSOperation のサブクラス「 LoadOperation」を使用
LoadOperation *op
= [[LoadOperation alloc] initWithPath:fullSourceDirectoryFilePath];
[op setQueuePriority: 2.0]; // 二次的な優先度
(問題あり!)1
[queue addOperation: op]; // これは読み込み工程を開始させることになる
[op release];
if ([self isCancelled])
{
break; // ユーザーがこの工程をキャンセルした
}
}
}
}
[pool release];
}
まず、独自の自動解放プールを作っています。ガベージコレクションを使うなら、この部分は必要ありませんが、そうでなければ、自動解放プールはメインスレッド上で作られるため、他スレッド内で Cocoa コードを使用するなら、自動解放プールを作らなければなりません。
そして、ファイルシステム項目を受けとるための変数を宣言して、ディレクトリ列挙子を作成します。それから、その列挙子を使って、ファイル項目を1つずつ取得して処理します。
取軽した項目に対して、まず属性ディクショナリを取得します。このディクショナリは多数の要素を収容しています。ここでは、そのなかの NSFileType を取得しています。そのファイルタイプには、通常ファイル、ディレクトリ、シンボリックリンクなどの区別があります。ここでは、通常ファイルの場合のみ、続きの作業を行っています。注意するのは、ディレクトリには、いわゆるパッケージも含まれているということです。リッチテキストなどは、パッケージ構造になっているものがあるので、ここでは、そのなかにも入ってファイル項目を取得することになります。
通常ファイルなら、そのファイルのチェックは、別の工程オブジェクトを作成して、それにまかせています。これは LoadOperation クラスのインスタンスです。工程オブジェクト自体の使い方は、このインスタンス自身と同様です。設定後、キューに追加しているだけです。
注意するのは、1 の部分です。ここで、優先度を設定していて、コメントには second とあり、二次的と訳しましたが、リファレンスによると、優先度は正がより高く、負がより低く、何も指定しないとゼロになるということです。そのため、最初の工程キューは何も設定されなかったため、NSOperationQueuePriorityNormal、すなわちゼロが設定されているはずです。ここでは、それより高い優先度として 2.0 という数値を指定しているものと思われますが、リファレンスによると、設定値が決められた定数以外のものであった場合、定数値になるようにゼロの方向へと修正されると述べられています。NSOperationQueuePriorityNormal の次に高い優先度定数は NSOperationQueuePriorityHigh で、これは 4 です。そのため、2.0 という数値は、修正されて、ゼロにされてしまうと考えられます。つまり、結局、最初の工程オブジェクトと同じ優先度が設定されてしまうことになります。
キューへと追加した後で、ユーザーのキャンセルを調べています。ガイドでは、ユーザーキャンセルのみの場合、ミリ秒ごとにチェックすれば十分であると述べられています。この例の場合、このチェックの後、すぐに次の繰り返しが始まり、そこでキャンセルチェックが行われるため、ちょっと神経質すぎる気もします。
このクラスは、これで終わりです。ひたすらファイルを検索して見つけたものを別の工程オブジェクトに渡す作業を繰り返していくだけです。つぎは、通常ファイルが見つかった時に作成されて実行される、LoadOperation クラスを見てみましょう。
7 LoadOperation クラス
7.1 インターフェイス宣言
ヘッダファイルは以下のようになっています。
LoadOperation.h
// 見つかった画像ファイルをウインドウコントローラーに知らせる NSNotification 名
extern NSString *LoadImageDidFinish;
@interface LoadOperation : NSOperation
{
NSString* loadPath;
}
-(id)initWithPath:(NSString*)path;
@end
インスタンス変数は、渡された通常ファイルのパスを格納しておくものだけです。
7.2 初期化
初期化メソッドは以下のようになっています。
LoadOperation.m > initWithPath:
- (id)initWithPath:(NSString*)path
{
self = [super init];
loadPath = [path retain];
return self;
}
渡されたパスをインスタンス変数に格納しているだけです。dealloc も同様に、渡されたパスを解放しているだけです。
7.3 main メソッド
このクラスの作業の中心となる main メソッドは以下のようになります。
LoadOperation.m > main
// 画像ファイルか確認するため与えられたファイル(NSURL "loadURL")を調べる
// 画像ファイルなら、さらに調べ、そのファイル属性を報告する
//
// NSFileManager を使うこともできただろうが、より安全に
// ファイル属性を取得するためファイルマネージャー API を使うことにする
-(void)main
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (![self isCancelled])
{
// 画像ファイルかどうかを確認するためにテスト
if ([self isImageFile: loadPath])
{
// この例では、ファイル情報(修正日付、ファイルサイズ)だけを取得し、
// それをテーブルビューに報告する
FSRef ref;
Boolean isDirectory;
if (FSPathMakeRef((const UInt8 *)[loadPath fileSystemRepresentation], &ref,
&isDirectory) == noErr)
{
FSCatalogInfo catInfo;
if (FSGetCatalogInfo(&ref, (kFSCatInfoContentMod | kFSCatInfoDataSizes),
&catInfo, nil, nil, nil) == noErr)
{
CFAbsoluteTime cfTime;
if (UCConvertUTCDateTimeToCFAbsoluteTime(&catInfo.contentModDate, &cfTime)
== noErr)
{
CFDateRef dateRef = nil;
dateRef = CFDateCreate(kCFAllocatorDefault, cfTime);
if (dateRef != nil)
{
NSDateFormatter* formatter
= [[[NSDateFormatter alloc] init] autorelease];
[formatter setTimeStyle:NSDateFormatterNoStyle];
[formatter setDateStyle:NSDateFormatterShortStyle];
NSString *modDateStr = [formatter stringFromDate:(NSDate*)dateRef];
// 通知用の情報を作成
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
[loadPath lastPathComponent], @"name",
[loadPath stringByDeletingLastPathComponent], @"path",
modDateStr, @"modified",
[NSString stringWithFormat:@"%ld",
catInfo.dataPhysicalSize], @"size",
nil];
if (![self isCancelled])
{
// このサンプルの目的のために、ここで、情報を外へとポストし、
// 興味をもつかもしれなものには誰(この場合 MyWindowController)
// でもそれを取得させる
[[NSNotificationCenter defaultCenter]
postNotificationName:LoadImageDidFinish object:nil userInfo:info];
}
CFRelease(dateRef);
}
}
}
}
}
}
[pool release];
}
まず、独自の自動解放プールを作っています。それから、このクラスのメソッドである isImageFile: を使って、画像ファイルかどうかをチェックし、画像ファイルなら、通知用の情報を取得して、それらをディクショナリに格納した後で、通知を行っています。
通知用の名前である LoadImageDidFinish は、この実行ファイルの最初に宣言されていることに注意してください。
LoadOperation.m 冒頭
// ウインドウコントローラーに見つかった画像ファイルを知らせる通知名
NSString *LoadImageDidFinish = @"LoadImageDidFinish";
MyWindowController クラスの awakeFromNib で通知に登録していたので、この通知により、MyWindowController のインスタンスが結果を受けとることになります。
isImageFile: メソッドについては、このサンプルの目的からは外れますので、ここでは詳しく説明しません。単純に言えば、ファイルの UTI を取得して、それが画像入出力フレームワークでサポートされているものかどうかを調べて画像ファイルかどうかを判断しています。
これまで説明してきたように、2 つの工程クラスによって、検索が行われて、見つかった通常ファイル 1 つ 1 つに別の工程クラスのインスタンスが作られ、そこで画像かどうかが調べられて、画像ファイルの時だけ通知が行われます。
この通知は、メインスレッド上の MyWindowController インスタンスによって取得され、そこで処理されることになるわけです。これ以降の動作を見ていきましょう。
8 MyWindowController クラスふたたび
8.1 通知を受けとるメソッド
さて、画像ファイルが見つかったら、通知が行なわれます。awakeFromNib 内で登録したとき、通知を処理するセレクタとして anyThread_handleLoadedImages: を登録しました。このメソッドを見てみましょう。
MyWindowController.m > anyThread_handleLoadedImages:
// このメソッドは、テーブルビューとデータソースを更新するために
// 任意のありうるスレッド(任意の NSOperation)から呼び出される
//
// 通知は、テーブルビューに追加する画像ファイルの情報を収容している
// NSDictionary を収容している
- (void)anyThread_handleLoadedImages:(NSNotification *)note
{
// 主スレッド上でテーブルビューを更新
[self performSelectorOnMainThread:@selector(mainThread_handleLoadedImages:)
withObject:note waitUntilDone:NO];
}
ユーザーインターフェースの更新は、主スレッドから行うのが良いとされています。そこで、どのスレッドから呼び出されてもいいように、このメソッドは、主スレッド上のメソッドへと受けとった情報を渡しています。渡される先のメソッドは以下のようになっています。
MyWindowController.m > mainThread_handleLoadedImages:
// 主スレッド上でテーブルのデータソースを修正するために使われるメソッド
// NSArrayController が変更されるたびに、
// これはテープルに自身の更新を行わせることになる
//
// 通知には、テーブルビューに追加するための画像ファイルの情報を
// 収容した NSDictionary が収容されている
- (void)mainThread_handleLoadedImages:(NSNotification *)note
{
// 保留の NSNotifications は、実行を待っている間に、溜まる可能性がある
// そして、ユーザーがキューを中止すれば、
// 処理すべき残った保留の通知を持つことになるかもしれない
//
// そのため、連続的にテーブルビューに見つかった画像ファイルを追加するつもりなら
// 「アクティブな」実行中の NSOperation がキュー内にあることを確認する
// そうしないと、残った通知を漏れさせてしまう
if ([myStopButton isEnabled])
{
[tableRecords addObject:[note userInfo]];
[myTableView reloadData];
// 見つかった画像の数を示す文字列を設定
[self updateCountIndicator];
}
}
ここでは、まだ検索が行われていることを確認するために、中止ボタンが有効かどうかを調べています。これにより、ユーザーがキャンセルを行っていないことを確認し、それから見つかった画像の追加を行います。追加自体は簡単です。自身のデータソースである配列に情報を追加し、テーブルに更新を要求しています。その結果、このクラスのデータソースメソッドが呼び出されることになりますが、説明することもないでしょう。
これで、開始ボタンから始まった、検索の流れは理解できました。単に NSOperation を作成して、それをキューに追加していくだけの簡単な流れです。このように、NSOperation を使えば、簡単にマルチスレッドのアプリケーションを作成することができます。ただし、注意しなければならないのは、上の 2 つのメソッドに示されているように、ユーザーインターフェースの更新は、主スレッド上で行ったほうがいいということ。そして、別スレッドで実行される可能性のある NSOperation の main メソッド内では、ガベージコレクションを使わないなら、独自の自動解放プールを作成しておくこと。などなどです。他にも使う API のスレッド安全性などにも注意したほうがいいでしょう。
中止など、他のメソッドもありますが、全て適切な時にキューを中止したり、といったことに関するものです。これまでの話を理解していれば、それほど難しい動作ではないので、ここでは説明しません。
管理人:神吉 秀典 E-mail: