delegateやprotocolの勉強をしたいという人がいるので、なるべくわかりやすくここに書いておこう。propertyとか、delegateっていうのはObjective-Cで楽しいところでもある。
でも、delegateだけじゃなくて、他にもいろいろ非同期的な処理をやる方法あるんで、それも、まとめて説明する。
適当に思いつくだけ書くと、非同期的な処理をするために、Objective-Cでは以下のようなやり方がある。他にもあるかもしれないが、だいたいこれだけある。そして、どれを使ってもいい。
Objective-C、C/C++でも可能
・関数のアドレスを保持しておいてコールバックする。
・pthread条件変数を使う。(デッドロックやスレッド管理)
・非ブロッキングI/Oする。(データの検査コスト大、結局カーネル空間からユーザ空間に必要なデータをコピーするときにブロックされる)
・シグナルハンドラを設定し、シグナルを飛ばす。シグナル駆動I/O。(カーネルからユーザにコピーするときにブロック、シグナル管理むずい)
・poll(2)やselect(2)を使って多重化I/Oする。(カーネルからユーザに(略)
・aio.hのPosix非同期I/O関数を使って非同期I/Oする。(ブロック一切ないが実装むずい)
・libevのepollを使って非同期I/Oする。(現実的な解法)
Objective-Cだけが可能
・delegateコールバックを呼び出す(Cより安全なコールバック)
・KVOを使う(Cより安全かつ簡単なコールバック)
・NSNotificationを使う(ブロードキャスト通知、なんでもありになるので逃げ)
・Blocksを使う(クロージャ)
・GCDを使う(マルチスレッド処理、dispatch_sourceを使って非同期I/Oするのも可)
*これを読んだだれかこれの他言語版を書いてほしいと思う。*
余談だが、DMA(Direct memory Access)はドライバを開発していると使うことが多い。マイコンプログラム書く人も知っているだろう。DMAC(Direct Memory Access Controller)というチップが基板に実装されていて、2つのメモリの一方の特定の番地に書き込みをすると、もう一方のメモリにDMACがデータ転送する仕組みである。これをやることで、CPUがデータ転送するために待たなくていい。
あと、メモリの転送というのは非常に遅い。memcpy(3)の時間を測ってみた事がある人ならわかると思う。カーネルからユーザへの転送はさらに特別な考慮が必要で、よりコスト高なので、パフォーマンスを考えるときは、非ブロッキングといえども非常に遅い転送になることも考慮しないといけない。こういうことをふまえたときに、Objective-CのGCDを使った非同期I/Oって簡単だし素晴らしいよねーという気持ちになれると思う。
delegate
(使い道1)コールバックのためのdelegate
で、delegateというのはコールバックです。しかも、そのコールバックを受けるためにdelegateが実装されている必要があります。Javaのimplementと一緒。
Cのコールバック関数とやることは同じだが、関数ポインタを管理しなくていいし、通知先、渡されるオブジェクトに制限がある(@protocolで定義したものが制限になる)。1デリゲートは一本の矢印で書ける。基本的には、delegateで全て事足りるようになっていないといけないと思う。
delegateの通知を受ける先のハンドラが遠くてコード追いづらいというデメリット、あとdelegateのセットと、dealloc時のnil代入を忘れがちなので、そういうところ注意しないとダメだが。
そこらじゅうで説明のサイトあると思うけど、実装の例。
例えば、ファイルI/Oが完了したらdelegateが飛んでくるようなViewを実装する。
@protocol FileReadWriteDelegate //delegateの定義
@required //requiredが指定されてると必ず実装する必要がある
- (void)onDidOpenFile:(id)sender;
- (void)onDidCloseFile:(id)sender;
@optional //optionalは実装しなくてもいい
- (void)onDidReadFile:(id)sender;
@end
@interface CustomView:UIView <FileReadWriteDelegate> // delegate通知を受け取るために必要
@end
@interface FileManage:NSObject
@property id<FIleReadWriteDelegate> delegate; //コールバック先。FileReadWriteDelegateのデリゲートだけを指定できる
@end
@interface CustomVIew()
@property (retain) FileManage *fileManage;
@end
@implement CustomView
- (id)initWithFrame:(CGFrame)frame{
if(self = [super:initWithFrame:frame]){
_fileManage = [[FileManage alloc] init];
[_fileManage setDelegate:self]; //デリゲートがこちらを向くように指定する(dealloc時nil代入を忘れるな)
}
}
-(void)load:(NSString*)filePath{
[self.fileManage readFile:filePath];
}
- (void)dealloc{
[_fileManage setDelegate:nil]; //これを忘れると、CustomViewがdeallocされているのに、_fileManageがdelegateを飛ばそうとして落ちる。
[_fileManage release];
}
- (void)onDidOpenFile:(id)sender{
//delegateのハンドラ、だいたいはここでViewを操作する
}
- (void)onDidCloseFile:(id)sender{
//delegateのハンドラ、だいたいはここでViewを操作する
}
- (void)onDidReadFile:(id)sender{
//delegateのハンドラ、だいたいはここでViewを操作する
}
@end
@implement FileManage
- (void)readFile:(NSString*)filePath{
//ファイル開く
[self open:filePath];
[self.delegate onDidOpenFile:self]; //デリゲートで通知
//巨大なファイルを読む処理が入る.何秒もかかる。
[self read];
//ファイルが読み終わった
if([self.delegate respondsToSelector:@selector(onDidReadFile:)){//optionalなので実装されてないかもしれないのでチェックする
[self.delegate onDidReadFile:self]; //デリゲートで通知
}
//ファイル閉じる
[self close];
[self.delegate onDidCloseFile:self]; //デリゲートで通知
}
@end
イメージつかめてもらえただろうか?
delegateよりBlocks使えじゃなかったの?
以前にdelegateよりBlocksをどんどん使っていこうみたいなことを書いた。Blocksのだいたいの使い方は次の記事をみてもらったらいいと思う。
実際、Blocksで早く楽に書けるんだけど、それはそれで設計が自由になりすぎるというか、やりかた間違えると複雑になってしまう。皆でやるプログラミング向きではないっていうか・・
たとえば、MVCで、MにVを更新するBlockを持たせたりとかできるし、それ俺やってた。それも楽だけど、結局Vの処理をするためだけなのにMのコードも追わないとダメになる部分が増えたり、Blockの解放どこでやるかみたいなところを結局考えることになるし。Block解放するのいつ?とか。Vが解放されたタイミングか?Mが解放されたタイミングか?Blockはスタックかヒープかどこにあるのか?BlockとBlockを持つインスタンスが循環参照になっててdealloc走らないんじゃないか?このBLockの処理はMVCのどれ?みたいなのとか。
delegateは単なる矢印一本の関係なので設計はBlocksほど自由ではないから、delegate使っている限りは、設計もMVCとか、オブジェクト指向らしい感じを自然と保てる。やっぱり善し悪しかなぁと思う。
でも設計を崩さない範囲でBlocks使うけどね。
(使い道2)抽象化のためのdelegate
@requiredがあるdelegateを実装したクラスは、指定されたメソッドを必ず実装しなければいけないという制約がある。その場合は、命名を*Delegateではなく、*Protocolとするのが慣例だと思う。
KVO
Macの時代からずっとプログラミングしてる人には常識なんですが、iPhoneプログラマーは案外知ってないことの一つですね。なんでもかんでもdelegateする必要ないんです。
KeyValueObserverといって、プロパティの値が変わったら、そのプロパティを監視しているオブジェクトにKVO通知がいくんですね。これを使ったらdelegateよりも簡単に非同期処理ができてしまう罠。まぁdelegateより実際簡単ですね。
注意点としては、addObserverしたら、必ずremoveObserverしましょうということと、init,deallocでKVO使うのやめようということ、また、KVO意識しようということです。
さっきのdelegateのコールバックは、こんな感じに書き換えられる。
@interface CustomView:UIView
@end
@interface FileManage:NSObject
@property (readonly , getter=isLoaded) BOOL loaded //監視する変数
@end
@interface CustomVIew()
@property (retain) FileManage *fileManage;
@end
@implement CustomView
- (id)initWithFrame:(CGFrame)frame{
if(self = [super:initWithFrame:frame]){
_fileManage = [[FileManage alloc] init];
}
}
- (void)load:(NSString*)filePath{
[self.fileManage addObserver:self
forKeyPath:@"loaded"
options:nil
context:nil];
[self.fileManage readFile:filePath];
}
- (void)dealloc{
[self.fileManage removeObserver:self
forKeyPath:@"loaded"];
[_fileManage release];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
//KVOのハンドラ
FileManage *fileManage = object;//オブジェクトごと渡ってくる
//Viewを更新したりするといいと思う。
}
@implement FileManage
- (void)readFile:(NSString*)filePath{
//ファイル開く
[self open:filePath];
//巨大なファイルを読む処理が入る.何秒もかかる。
[self read];
//ファイルが読み終わった
self.loaded = YES; // KVO通知発生する。
//ファイル閉じる
[self close];
}
@end
NSNotification
NSNotificationは、1対多の通知機構。アプリ内であれば、notification nameさえ指定して、notification centerに登録していればどれだけオブジェクトが関係なくても受け取れる。逃げである。
これはもうだるいので適当に調べてほしいと思う。
Blocks
クロージャ。以前のブログで書いた。
NSArrayにたくさんaddObjectしておいて、あとから全部実行するなんてこともできる。
BlocksKitというライブラリが便利らしいので、それ使ったら、UI関連のものがだいたいBlocksで実装できるんですって。
GCD
kqueueのラッパー。以前のブログで書いた。
コンカレントキューと、シングルキューというものがある。
キューにBlockに書いたタスクを順番に入れていく。
同期か、非同期処理か選べる。
シングルキューは排他制御に使われる。mutexよりも非常に高速。
NSNetworkOperationはGCDをラッパーしたもので、HTTPの処理が書ける。
ちょっと後半ばてて失速してしまったが、また気が向いたら続きを書く。