KVO(Key-Value Observing, キー値監視) を行う … その1

どうもお久しぶりです。もともとマトモなアウトプットは無かったこのブログですが、そのマトモでないアウトプットも全部 Twitter に吐き出されているため、ホントに更新していませんでした。しかし、なんか急に書きたくなったので、脈絡もなく更新します。

7月あたりにふと作りたいiOSアプリを思い付いたので、Developer Program 契約3年目にして初のアプリを作り始めました。そして KVO を使おうとして「この API イケてねぇぞ! どうなってんだよ Apple さんよぉ……」と怒り狂い Web を散策した結果、良いエントリがあったので適当に翻訳しました。

Mike Ash 氏の Key-Value Observing Done Right というエントリで、2008年10月と少々古いですが、よくまとまっています。ではどうぞ。

キー値監視を行う

Cocoaのキー値監視機構は非常に強力で便利です。しかし残念なことに、そのAPIは本当にひどく、いくつかの点で本質的に壊れています。それがどのように壊れているのか、そしてより良くする方法を論じます。

何が壊れているのか

KVO API には大きく 3 つの問題があり、すべては複数レベルの階層構造を持つクラスへオブザーバーを登録することに関連しています。これはとても重要で、なぜなら NSObject (が実装している-bind:toObject:withKeyPath:options:) が常に監視対象になるからです。[?]*1

  1. -addObserver:forKeyPath:options:context:は起動するセレクタを変更することができません。


    NSNotificationCenter などの同様の API を見たとき、それは常に、指定されたイベントが発生したときに呼び出されるセレクタを渡して、オブザーバーを登録していることがわかります。これはあなた自身のメソッドにメッセージを送信するので、物事をスーパークラスから分離するのが非常に容易になります。KVO では、-observeValueForKeyPath:ofObject:change:context:をオーバーライドし、自分自身でメッセージを処理するかスーパークラスの実装を呼び出します。メッセージを処理するか上位のチェインに渡すかどうかの決定は、スーパークラスが同じオブジェクトとキーパスを登録しているかもしれないという事実によって複雑になります。

  2. コンテキストポインタは無意味です。


    これは、#1の結果です。起動するカスタムメソッドを指定することはできず、キーパスまたはオブジェクトからスーパークラスが通知に興味があるのか調べることもできないので、通知があなたのためのものなのかスーパークラスのためのものなのかを調べる手段が他に必要になります。コンテキストポインタがこれを行う方法です。スーパークラスからは使用することのできない一意ポインタを作成し、addObserver:...に渡さなければなりません。そして-observeValueForKeyPath:...の実装では、コンテキストポインタと先の一意ポインタを比較して確認しなければなりません。その結果、実際にコンテキストを保持するためのポインタをコンテキストポインタとして使用することはできません。

  3. -removeObserver:forKeyPath:は十分な引数を取っていません。


    このメソッドは、コンテキストポインタを取っていません。これは、スーパークラスと同じオブジェクト/キーパスの組み合わせで登録し、それが異なる寿命である場合に、あなたのオブザーバーのみを無効にする方法がないことを意味します。このメソッドを呼び出すと、あなたのものを無効にするか、スーパークラスのものを無効にするか、またはその両方を無効にします。

このような強力なツールが壊れているのはとても残念です。特に新しい API では伝統的な NSNotification とデリゲートに代わって、単に KVO をサポートするようになっている傾向が Apple にはあります。 完全な例は NSOperation で、NSOperation が完了したことの通知を得る唯一の方法は、KVO を使用してisFinishedプロパティを監視することです。

さて、私たちに出来ることはなんでしょうか?
私は役に立たない文句を言いたくないので、この問題を解決するためにクラスを書きました。私の公開 svn リポジトリから取得することができます:

svn co http://www.mikeash.com/svn/MAKVONotificationCenter/

また、上記のリンクをクリックすることで閲覧することができます。

それは、どのような仕組みでしょうか?それは、一意であることが保証できるポインタ、つまり self ポインタの利点を活用します。通知の対象となるオブジェクトを登録する代わりに、独自のヘルパーオブジェクトを各通知毎に作成して登録します。ヘルパーは通知を受信し、元のオブザーバーにそれを転送します。ヘルパーは各監視毎に固有のオブジェクトなので、単純なインスタンス変数として監視に関するメタデータを保持することができ、一意ポインタを必要とするコンテキストポインタに依存する必要がありません。ヘルパーは何もしませんが KVO 通知を待ち受け、オブジェクトの存続期間のあいだ監視は続きます。そして、そのスーパークラス、NSObject は何も監視しないことだけでなく、オブジェクトの存続期間だけ監視すると仮定することができます。[?]*2

MAKVONotificationCenterは、上記の3つのすべての欠陥をバイパスします:

  1. -addObserver:...メソッドで、監視対象のキーパスが更新されたときに起動されるセレクタを変更できます。スーパークラスは異なるセレクタを利用しているでしょうから、問題は解決されました。(Cocoaスーパークラス達が直接監視をするのに対して、 MAKVONotificationCenter はヘルパーを介して監視するので、確実に干渉しません) [?] *3

  2. オブザーバーのメソッドに渡されるuserInfoパラメータが提供されます。これは、監視についての必要な情報を含む任意のオブジェクトを指定できます。

  3. -removeObserver:...メソッドは、ターゲットとキーパスだけでなく、セレクターも取ります。これにより、サブクラスとスーパークラスの両方が同じオブジェクトの同じキーパスを登録している場合に、それぞれ独自のセレクタを指定することによって、他に影響を与えずに登録を解除することができます。

また、いくつかの興味深い機能があります。

+defaultCenterは、アクセス毎のロックを必要としないスレッドセーフなシングルトンを作成するために、 ロック不要の単純なアトミック呼び出しを使用しています。これは、事前の初期化、またはシングルトンへのアクセス毎にロックおよびアンロックを行うことなく、安全なシングルトンを実現する素敵なテクニックです。

少しよりよいAPIは、NSObject のカテゴリとして公開されています。これは、明示的にMAKVONotificationCenterのシングルトンをアクセスするよりも優れています。極端なケースでは、NSObjectのカテゴリ拡張だけを残して、MAKVONotificationCenterをヘッダから完全に取り除くことができました。

このコードは、基本的にはテストされていません。Tester.mの幾つかのテストコードが、私が行なった全てです。あなたは自分で試してみるまで、それを信用しないでください。実際のコードは約150行で多くのことは行なっていませんが、どのような場合でも「買い主の危険負担」です。

あなたのプロジェクトでこれを使用する場合は、適切なクレジットが成されている限りにおいて、それが可能です。また欠陥を発見した場合、パッチは大歓迎です。

そして、この MAKVONotificationCenter をベースにした GTMNSObject+KeyValueObserving.[hm] が Google Toolbox for Mac に含まれています。なので、僕は GTM のほうを使ってます。

で、これはこれで GC-enabled な環境で問題があるっぽい?(まぁiOSでは関係ありませんが)とか、せっかくだから Blocks/GCD 使いたいよね! とかまだトピックはあるので、気が向いたら「その2」を書きます。

*1:よくわからなかったのですが、要は全てのクラスはNSObjectを継承しているから大変だよ、って事でしょう。

*2:英語スキル低いので、よく読解できませんでした

*3:ここもよくわかりません