プロパティへの書き込みを外から監視する : Objective-C プログラミング

PROGRAM


プロパティへの書き込みを外から監視する

Objective-C では Key-Value Observing (KVO) という機能を使って、任意のインスタンスのプロパティが変更されたことを外から検知することができます。

プロパティへの書き込みを検出する

使い方は簡単で、まず、プロパティの変更を監視したいインスタンスの -addObserver:forKeyPath:options:context: メソッドを実行します。

// インスタンス _label の text プロパティが変更されるタイミングを検出します。

[_label addObserver:self forKeyPath:@"text" options:0 context:NULL];

そして、第 1 引数で指定したインスタンスのクラスに、次のメソッドを実装します。

// 監視対象のプロパティセッターが呼び出されると、このメソッドが実行されます。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

{

}

keyPath にはセッターが呼び出された監視対象のプロパティ名が(今回の例では @"text" が)指定されます。ofObject には監視対象のインスタンスが(今回の例では _label が)渡されます。

複数のインスタンスやプロパティを監視している場合には、どれが変更されてもこのメソッドが呼び出されるので、これら keyPath や ofObject の内容や、-addObserver:forKeyPath:options:context: の context に渡した情報を使って、どのプロパティが変更されたかを判断します。

 

メソッドの呼び出しを検出する場合

監視できるのは @property で定義したプロパティだけでなく、set で始まり引数を 1 つ取るメソッドも監視できます。

たとえば -setValue: というメソッドであれば、forKeyPath に @"value" を指定することで、このメソッドの呼び出しを監視することができます。

もちろん @property で定義した場合も、[obj setXXX] という形式での呼び出しでも検知されます。

 

NSMutableArray の追加や削除を検出する

NSMutableArray の検出は、プロパティの値を丸ごと差し替える場合は、上記の -addObserver:forKeyPath:options:context: メソッドを使う方法で検出できます。

ただ、NSMutableArray の場合、addObject や insertObject, removeObject, removeAllObjects などを使ってプロパティの内容を直接操作することも可能です。

 

このような場面も検出したい場合には、これまでの方法に加えて、mutableArrayValueForKeyPath: メソッドを使って、操作用の NSMutableArray を取得する必要があります。

たとえば、インスタンス obj が持つ array プロパティが NSMutableArray を返す場合、その変化を検出したい場合には次のようにします。

// インスタンス obj の array プロパティが変更されるタイミングを検出します。(必須)

[obj addObserver:self forKeyPath:@"array" options:0 context:NULL];

 

// そのうえで、編集用のインスタンスを取得します。

NSMutableArray* observingArray = [obj mutableArrayValueForKeyPath:@"array"];

このようにしたら、後は mutableArrayValueForKeyPath: メソッドで取得した NSMutableArray インスタンスを(ここでは observingArray を)に対して addObject: や removeObject: などを実行すると、その変化を、プロパティセッターを呼び出したときと同じ方法で検出できます。

// mutableArrayValueForKeyPath: で取得したインスタンスを操作したときも、このメソッドが実行されます。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

{

}

操作は mutableArrayValueForKeyPath: で取得したインスタンスに対してになりますけど、本来の obj.array の内容も自動的に変更されています。

 

また、このとき obj.array の内容をまるごと置き換えることも可能でした。

// プロパティの内容を置き換えたときも、従来通り検出メソッドが呼び出されます。

obj.array = [[NSMutableArray alloc] init];

このようにしたときも、わざわざ mutableArrayValueForKeyPath: を呼び出さなくても、置き換える前に取得した操作用のインスタンスを(ここでは observingArray を)使って、新しい obj.array を操作することができる様子です。

 

指定できるオプションと取得値

options に指定できる値

-addObserver:forKeyPath:options:context: の context では、通知方法を次の中から組み合わせて指定できます。

options に指定できる値 検知メソッドが呼び出されるタイミング 検知メソッドの change で受け取れる値
0 プロパティの処理が終わった直後(変更後)に検出メソッドが呼び出されます。 プロパティの操作の仕方を示す @"kind" を取得できます。(変更前:〇、変更後:〇、初回:〇)
NSKeyValueObservingOptionNew   変更直後にプロパティから取得した値を @"new" で取得できます。(変更前:×、変更後:〇、初回:〇)
NSKeyValueObservingOptionOld   変更直前にプロパティから取得した値を @"old" で取得できます。(変更前:〇、変更後:〇、初回:×)
NSKeyValueObservingOptionInitial -addObserver:forKeyPath:options:context: を実行したタイミング(初回)でも 1 度、検知メソッドが呼び出されます。 @"new" と @"kind" が取得できます。
NSKeyValueObservingOptionPrior 変更直前にも検出メソッドが呼び出されます。 変更直前の呼び出しに限り @"notificationIsPrior" として @1 が取得できます。(変更前:〇、変更後:×、初回:×)

これらのオプションを "|" 演算子を使って組み合わせて、-addObserver:forKeyPath:options:context: メソッドの options に指定します。

 

change で取得できる値

検出メソッドの change 引数で得られる NSDictionary では、@"kind" キーで次の値を取得できます。

NSKeyValueChangeSetting = 1
  • プロパティのセッターを呼び出したときに設定されます。
  • オプションに NSKeyValueObservingOptionInitial を付けて -addObserver:forKeyPath:options:context: メソッドを呼び出した場合にもこれが設定されます。
NSKeyValueChangeInsertion = 2
  • mutableArrayValueForKeyPath: メソッドで取得したインスタンスで addObject: メソッドや insertObject:atIndex: メソッドを実行した場合に設定されます。
  • change[@"new"] では、追加された要素 1 つだけの配列が取得できます。
  • change[@"old"] は設定されません。
  • addObjectsFromArray: メソッドを呼び出した場合も、追加する要素 1 つ 1 つに対して、検出メソッドが呼び出されます。
NSKeyValueChangeRemoval = 3
  • mutableArrayValueForKeyPath: メソッドで取得したインスタンスで removeObject: メソッドを実行した場合に設定されます。
  • change[@"new"] は設定されません。
  • change[@"old"] では、削除された要素 1 つだけの配列が取得できます。
  • removeAllObjects メソッドなどで複数の要素を削除した場合は、削除した要素 1 つに対して 1 回ずつ、検出メソッドが呼び出されます。
NSKeyValueChangeReplacement = 4
  • mutableArrayValueForKeyPath: メソッドで取得したインスタンスで replaceObjectAtIndex:withObject: メソッドを実行した場合に設定されます。
  • change[@"new"] では、置き換えられた新しい要素 1 つだけの配列が取得できます。
  • change[@"old"] では、置き換え前の古い要素 1 つだけの配列が取得できます。

これらのキーの値は NSUInteger 型で、change 引数内では NSNumber 型で表現されています。

 

複数スレッドでの検知されるタイミング

検出メソッドが呼び出されるタイミングは、監視対象のメソッドが呼び出されてから、終了されるまでの間になります。

  • 変更後にだけ呼ばれるオプション構成の場合は、プロパティの値が変更されてセッターの実行が終わってから、プロパティへの代入文を実行した次の行へ移るまでの間に、検出メソッドが呼び出されます。
  • 変更前にも呼ばれるオプション構成の場合は、プロパティへの代入文を実行した行から、プロパティのセッターが呼び出される前に、検出メソッドが呼び出されます。
  • NSKeyValueObservingOptionInitial オプションを指定した場合は -addObserver:forKeyPath:options:context: メソッドを実行して、次の行に移るまでに、検出メソッドが呼び出されます。

 

これらのタイミングは、どれも同期実行で、プロパティの変更などを実行したそのスレッド上で行われる様子でした。

 

なお、プロパティが atomic 指定を指定して @synthesize で実装した場合であっても、ロックされるのはインスタンス変数を書き換えるところだけで、その前後の検出メソッドの呼び出しはロックの外に置かれる様子でした。

そのため、NSKeyValueObservingOptionInitial オプションを指定して変更前と変更後とを検出しようとした場合で、複数のスレッドで同じプロパティに代入したときには、あるスレッドで変更前の検出メソッドが呼ばれた後、そのスレッドでの変更後の検出メソッドが呼ばれる前に、別のスレッドで変更前の検出メソッドが呼ばれる場合があります。

セッターやゲッターを自前でロックをした場合も、それよりも外側で検出メソッドが呼び出されるので、同じです。

[ もどる ]