Objective-CでKVOの隠れた危険を克服する

うまくいかない可能性のあるものと、うまくいかない可能性のあるものとの主な違いは、うまくいかない可能性のあるものがうまくいかない場合、通常、取得または修復が不可能になることです。

-ダグラス・アダムス



Objective-Cは1983年以来存在しており、C ++と同じ時代です。 ただし、後者とは異なり、iOS 2.0(革新的なiPhoneのオペレーティングシステムの新しいバージョン)のリリース後、2008年にのみ人気を博し始めました。これには、サードパーティの開発者が作成したアプリケーションをユーザーが購入できるAppStoreアプリケーションが含まれていました。

Objective Cの継続的な成功は、iOSデバイスの人気とAppStoreでの比較的簡単な販売だけでなく、標準ライブラリと言語自体の両方を改善するためのAppleの多大な努力によっても確保されました。

TIOBEの評価による 、Objective-Cは2013年の初めまでにC ++を上回り、CとJavaに次ぐ3位になりました。



現在、Objective-Cには、最初のiPhoneの4年前に存在していたKVCKVOなどの比較的古い機能と、 ブロック (Mac OS 10.6およびiOS 4で導入されたブロック)や自動参照カウント (ARC 、Mac OS 10.7およびiOS 5)で利用できます。これにより、以前に深刻な問題を引き起こした問題を簡単に解決できます。



KVOは、観察対象オブジェクトの実装に観察者のタイプに関する知識を導入することなく、あるオブジェクト(観察者)で別のオブジェクト(観察対象)の状態の変化に即座に応答できるテクノロジです。 Objective-Cでは、KVOとともに、この問題を解決する方法がいくつかあります。



1. 委任 -オブジェクト指向プログラミングの一般的なパターン。特定のプロトコルを実装する任意のオブジェクト(デリゲートと呼ばれる)へのリンクがオブジェクトに渡されるという事実で構成されます -セレクターの固定セット。 その後、オブジェクトの実装は、ケースに適切なデリゲートに「手動で」メッセージを送信します。 たとえば、 UIScrollViewscrollViewDidScroll :セレクターを呼び出すことにより、contentOffsetプロパティの値の変更をデリゲートに通知します。

すべてのプロトコルセレクターのパラメーターの1つが、それを呼び出すオブジェクトへの参照を作成することをお勧めします。これにより、同じオブジェクトが同じクラスの複数のオブジェクトのデリゲートである場合、メッセージの発信元を区別することができます。



2. ターゲットアクション 。 この手法と委任の違いは、特定のプロトコルを実装する「デリゲート」の代わりに、特定のイベントが発生したときに呼び出されるセレクターがそのセレクターと共に渡されることです。 この手法はUIControlの子孫で最もよく使用されます。たとえば、ユーザーがこのコントロールを切り替えたときに呼び出されるターゲットアクションペアにUISwitchオブジェクトを割り当てることができます(UIControlEventValueChangedイベント)。 1つの「ターゲット」オブジェクトが異なるソース(たとえば、複数のUISwitch)からの同じイベントに応答する必要がある場合、このようなソリューションは委任よりも便利です。



3.コールバックブロック。 このソリューションは、監視対象のオブジェクトへのリンクがオブザーバーオブジェクト自体ではなく、ブロックに送信されるという事実に基づいています。 原則として、このブロックはインストールされた場所と同じ場所に作成されます。 さらに、ブロックの実装は、それが定義されているスコープのローカル変数の値をキャプチャできるため、別のメソッドを追加して実装内のコンテキストを復元する必要がなくなります。

このアプローチと以前のアプローチの重要な違いは、デリゲートまたはターゲットへの参照が弱い(弱い参照)場合、ブロックへのリンクが強く(通常は唯一のものであることが判明)、プログラマーはブロックが実装されるたびに注意する必要があることですブロックは弱いリンク上のオブジェクトをキャプチャしました。 そうしないと、周期的な強力な接続とメモリリークが発生する可能性があります。

最初の2つの手法と同じように、それをブロックの引数の1つと呼ぶオブジェクトへのリンクを作成することをお勧めしますが、理由は少し異なります。 とにかくブロックがコンテキストからこのリンクをキャプチャできるという事実にもかかわらず、誤って強力なリンクを使用してオブジェクトを取得したりこのリンクが初期化されたnilを取得したりするのは簡単です。



4. NSNotificationCenterを使用すると、文字列名と任意のオブジェクトで構成されるクラスのメソッドからアラート(NSNotification)を送信できます 。 このようなアラートは、この名前でアラートをサブスクライブするエンティティと(オプションで)エンティティによって受信されます。 通知のサブスクライブは、target-actionの原則またはコールバックブロックを使用して実装されます。

以前のアプローチとは異なり、NSNotificationCenterを使用すると、オブジェクト間の依存関係が弱くなり、追加の作業なしで同じアラートに複数のオブジェクトに署名できます。



5. NSKeyValueObservingはNSObjectクラスに実装された非公式のプロトコルであり、addObserver:forKeyPath:options:context:セレクターを呼び出すことにより、任意のオブジェクト(オブザーバー)に署名して、指定した他のオブジェクト(オブザーバブル)の指定したキーパスの値を変更できます。 その後、値が変更されるたびに、オブザーバーは、委任パターンと同様にobserveValueForKeyPath:ofObject:change:context:メッセージを受け取ります。

したがって、KVOを使用すると、単一の属性だけでなく、通常は変更を加えずに、監視対象オブジェクトの複合キーパスの値の変更に無制限の数のオブジェクトをサブスクライブできます。



KVOの明らかな力にもかかわらず、開発者の間ではあまり人気がなく、多くの場合、他のソリューションが利用できない場合にのみ少なくとも再利用として扱われます。 この嫌悪の理由を理解(および修正)するために、KVOの使用例をいくつか検討してください。



title属性とisFavorite属性を持つETRDocumentクラスがあるとします



@interface ETRDocument : NSObject @property (nonatomic, copy) NSString *title; @property (nonatomic) BOOL isFavorite; @end
      
      







ドキュメントに関する情報を表示する表形式のセルを実装したい



 @class ETRDocument; @interface ETRDocumentCell : UITableViewCell @property (nonatomic, strong) ETRDocument *document; @property (nonatomic, strong) IBOutlet UILabel *titleLabel; @property (nonatomic, strong) IBOutlet UIButton *isFavoriteButton; - (IBAction)toggleIsFavorite; @end @implementation ETRDocumentCell - (void)updateIsFavoriteButton { self.isFavoriteButton.selected = self.document.isFavorite; } - (void)toggleIsFavorite { self.document.isFavorite = !self.document.isFavorite; [self updateIsFavoriteButton]; } - (void)setDocument:(ETRDocument *)document { _document = document; self.titleLabel.text = self.document.title; [self updateIsFavoriteButton]; } @end
      
      





isFavoriteの値は、ボタンを押すだけでなく、セルの外部の何らかの方法でも変更できることがわかったとします。 これはセルの外観には影響しませんが、修正する必要があります。 isFavoriteを変更すると、セルを手動で見つけてupdateIsFavoriteButtonを呼び出して更新できますが、これによりクラス間の不要な通信が作成され、セルのカプセル化が解除されます。 したがって、ドキュメントへの変更のためにセル自体に署名することにします。 ドキュメントのデリゲートにすることも、isFavoriteが変更されたときに通知を送信することもできますが、代わりにKVOを使用する場合、ドキュメントクラスに変更を加える必要はありません。追加のロジックはすべてセルクラスにカプセル化されます。



 - (void)startObservingIsFavorite { [self.document addObserver:self forKeyPath:@"isFavorite" options:0 context:NULL]; } - (void)stopObservingIsFavorite { [self.document removeObserver:self forKeyPath:@"isFavorite"]; } - (void)setDocument:(ETRDocument *)document { [self stopObservingIsFavorite]; _document = document; [self startObservingIsFavorite]; self.titleLabel.text = self.document.title; [self updateIsFavoriteButton]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self updateIsFavoriteButton]; }
      
      





起動-すべてが機能し、セルはisFavoriteの変更に応答します。 さらに、toggleIsFavoriteからupdateIsFavoriteButtonの呼び出しを削除することもできます。 ただし、アプリケーションがEXC_BAD_ACCESSでクラッシュするため、テーブルを閉じていずれかのドキュメントのisFavorite値を変更する必要があります。

どうしたの? NSZombieEnabledを有効にして、手順を繰り返してみましょう。 今回は、落下したときに、より意味のあるメッセージを受け取ります

***-[ETRDocumentCell retain]:割り当て解除されたインスタンス0x8bcda20に送信されたメッセージ



実際、KVOのドキュメントを見ると、 次のことがわかります

注:キー値を監視するaddObserver:forKeyPath:options:context:メソッドは、監視対象オブジェクト、監視対象オブジェクト、またはコンテキストへの強い参照を維持しません。 必要に応じて、監視オブジェクト、監視オブジェクト、およびコンテキストへの強い参照を維持する必要があります。



監視は、監視者、監視対象オブジェクト、またはコンテキストへの強い参照を作成しません。 ただし、これらのオブジェクトの1つが削除されたときに何が起こるかについてはドキュメントには記載されていません。



KVOのコンテキストはCからの通常のポインターです。Objective-Cオブジェクトを指していても、KVOはそれを考慮しません。メッセージを送信したり、その存続期間を追跡したりしません。 したがって、コンテキストが削除されると、「hanging」リンクがobserveValueForKeyPathに渡され、メッセージを渡そうとすると、私たちが持っているのと同様の結果になります。 ただし、この例ではコンテキストを使用しませんでした。 さらに、コンテキストにはわずかに異なる「真の」目的があることがさらに明らかになります。



監視対象オブジェクトが削除された場合、監視を停止する代わりに(結局、値は変更できなくなります)、警告がコンソールに表示されます。



キー値オブザーバーがまだ登録されている間に、クラスETRDocumentのインスタンス0xac62490の割り当てが解除されました

それで。 観測情報が漏洩し、誤って他のオブジェクトに添付される可能性さえあります。

NSKVODeallocateBreakにブレークポイントを設定して、デバッガーでここで停止します。 現在の観測情報は次のとおりです。

<NSKeyValueObservationInfo 0xaaa77e0>(

<NSKeyValueObservance 0xaaa77a0:オブザーバー:0xaaa2100、キーパス:isFavorite、オプション:

<新しい:いいえ、古い:いいえ、前:いいえ>コンテキスト:0x0、プロパティ:0xabf12e0>





その後、アプリケーションは予測できない方法で動作し、遅かれ早かれクラッシュします。 ただし、この例では、セルには監視対象オブジェクトへの強い参照が格納されており、セルが削除されるまで削除できません。



オブザーバーが削除されると、KVOは「ハング」リンクを保存し(ARC用語のunsafe_unretained修飾子に対応)、変更が行われたときにメッセージを送信します。 これがまさにこの例で起こることです。 将来のバージョンでは、「unsafe_unretained」の動作がより安全な「weak」に置き換えられ、オブザーバーへの「hanging」リンクが自動的にリセットされる可能性があります。

このクラッシュを修正するには、deallocからstopObservingIsFavoriteを呼び出すだけです。



セルのロジックを単純化する方法があります。 セルは、ドキュメントのキーパス「isFavorite」を監視する代わりに、セル自体のキーパス「document.isFavorite」を監視できます。 その結果、リンクされたドキュメントのisFavorite属性が変更されたときと、ドキュメントへのリンクが変更されたときにセルに通知されます。 この場合、deallocからremoveObserverを呼び出す必要がありますが、現在のドキュメントを変更するたびに監視を停止および開始する必要はありません。

さらに進んで、isFavoriteだけでなくタイトルも観察できます。 これにより、setDocument:をオーバーライドする必要がなくなりますが、さらに別のKVOの不便が発生します。



 @implementation ETRDocumentCell - (void)awakeFromNib { [super awakeFromNib]; [self addObserver:self forKeyPath:@"document.isFavorite" options:0 context:NULL]; [self addObserver:self forKeyPath:@"document.title" options:0 context:NULL]; } - (void)dealloc { [self removeObserver:self forKeyPath:@"document.isFavorite"]; [self removeObserver:self forKeyPath:@"document.title"]; } - (void)toggleIsFavorite { self.document.isFavorite = !self.document.isFavorite; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"document.isFavorite"]) { self.isFavoriteButton.selected = self.document.isFavorite; } else if ([keyPath isEqualToString:@"document.title"]) { self.titleLabel.text = self.document.title; } } @end
      
      





他の2つの場所で複製された文字列を比較する1つの方法でのケースの古い(しかしあまり親切ではない)分析。 これは見苦しいだけでなく、他の「コピーアンドペースト」と同様にエラーを含んでいます。



悪いことは何も起こらず、すべてが機能することを期待して、これで停止できます。 そして今、それは本当に機能します。 しかし、遅かれ早かれ、まだ何か悪いことが起こる可能性があり、デバッガで数時間後には、KVOを混乱させない方が良いと確信するようになります。

何が起こるのでしょうか? サンプルを少し複雑にして、ドキュメントを表示するための別のテーブルを作成することにしましたが、ドキュメントのタイトルと同じボタンを含むもう少し洗練されたセルがありますが、他の変更とともに背景色が変更されますドキュメントが選択されているかどうかによります。

すでに行われた作業が無駄にならないように、古いセルから新しいセルを継承することにします。

セルの背景を変更するには、同じKVOテクニックを使用します。



 @implementation ETRAdvancedDocumentCell - (void)awakeFromNib { [super awakeFromNib]; [self addObserver:self forKeyPath:@"document.isFavorite" options:0 context:NULL]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self updateBackgroundColor]; } ...
      
      





素晴らしい、背景の色が変わります。 しかし、ボタンの強調表示が停止し、タイトルの更新が停止し、updateBackgroundColorの呼び出しも頻繁になりすぎました。 明らかに、ETRAdvancedDocumentCellは、自身の観測とETRDocumentCell観測の両方に関連するobserveValueForKeyPathメッセージを受け取ります。 このドキュメントには何が書かれていますか? 例の1つのコード内のコメントで、 次の行を見つけます。

スーパークラスの実装*を実装する場合は、必ず呼び出してください*。

NSObjectはメソッドを実装しません。



もちろん、ETRDocumentCellはobserveValueForKeyPathを実装することを知っています。つまり、呼び出す必要があります。

[super observeValueForKeyPath:keyPath ofObject:object change:change context:context] from ETRAdvancedDocumentCell。



しかし、すべては親クラスから実装を呼び出すことに限定されません。 ETRAdvancedDocumentCell自体がサブスクライブした変更を処理し、他の変更のみを親クラスに渡す必要があります。 明らかに、keyPathとオブジェクトの値をチェックするだけでは十分ではありません。親クラスは、同じオブジェクト(self)のまったく同じkeyPath(document.isFavorite)にサブスクライブされます。 これは、コンテキスト引数の非常に「真の」目的が現れる場所です。



 static void* ETRAdvancedDocumentCellIsFavoriteContext = &ETRAdvancedDocumentCellIsFavoriteContext; @implementation ETRAdvancedDocumentCell - (void)awakeFromNib { [super awakeFromNib]; [self addObserver:self forKeyPath:@"document.isFavorite" options:0 context:ETRAdvancedDocumentCellIsFavoriteContext]; } - (void)dealloc { [self removeObserver:self forKeyPath:@"document.isFavorite" context:ETRAdvancedDocumentCellIsFavoriteContext]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ETRAdvancedDocumentCellIsFavoriteContext) { [self updateBackgroundColor]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } ...
      
      





ETRAdvancedDocumentCellIsFavoriteContext静的変数には、独自のアドレスを含むメモリの固定領域へのポインターが含まれています。 これにより、この方法で宣言されたすべての変数に異なる値が保証されます。



明らかに、コンテキストの表示とともに監視も停止する必要があります。 対応するメソッドがiOS 5でのみ追加され、その前にコンテキスト引数なしのオプションのみが追加されたことは興味深いです。 これにより、他のパラメーターで区別できない観測の1つを正しく終了することができなくなりました。



しかし、ETRDocumentCellについてはどうでしょう。それからsuperを呼び出す必要がありますか? UITableViewCellクラスはobserveValueForKeyPathセレクターを実装していますか? あなたは試行錯誤に頼ることができ、スーパーを呼び出して、例外で期待されるドロップを得ることができます



***キャッチされない例外 'NSInternalInconsistencyException'によるアプリの終了、

理由: '<ETRDocumentCell:0x8d3c540; baseClass = UITableViewCell;

フレーム=(0 0; 320 64); 自動サイズ変更= W; レイヤー= <CALayer:0x8d3c730 >>:

-observeValueForKeyPath:ofObject:change:context:メッセージは受信されましたが、処理されませんでした。

キーパス:document.title

観測されたオブジェクト:<ETRDocumentCell:0x8d3c540; baseClass = UITableViewCell;

フレーム=(0 0; 320 64); 自動サイズ変更= W; レイヤー= <CALayer:0x8d3c730 >>

変更:{

kind = 1;

}

コンテキスト:0x0 '

***最初の呼び出し呼び出しスタック:



0 CoreFoundation 0x0173b5e4 __exceptionPreprocess + 180

1 libobjc.A.dylib 0x014be8b6 objc_exception_throw + 44

2 CoreFoundation 0x0173b3bb + [NSException raise:format:] + 139

3 Foundation 0x0118863f-[NSObject(NSKeyValueObserving)observeValueForKeyPath:ofObject:change:context:] + 94

4 ETRKVO 0x00002e35-[ETRDocumentCell observeValueForKeyPath:ofObject:change:context:] + 229

5 Foundation 0x0110d8c7 NSKeyValueNotifyObserver + 362

6 Foundation 0x0110f206 NSKeyValueDidChange + 458

...



コールバックを削除します。 しかし、親クラスが次のバージョンでobserveValueForKeyPathの実装を開始しない(またはその逆もしない)という保証はどこにありますか? 親クラスを自分で実装する場合でも、子クラスでスーパーコールを追加または削除することを忘れる危険があります。 最も信頼できる解決策は、実行中に適切なチェックを実行することです。 これは、クラスがrespondsToSelector:をオーバーライドしないため、常に[YES]を返す[super respondsToSelector:...]を呼び出してもまったく行われず、superで呼び出すことはselfを呼び出すようなものです。 これは、少し長い式[[ETRDocumentCell superclass] instancesRespondToSelector:...]を使用して行われます。 しかし、結局のところ、ドキュメンテーションは私たちを欺いており、[[NSObject class] instancesRespondToSelector:@selector(observeValueForKeyPath:ofObject:change:context :)]はYESを返し、対応する実装は上記の例外と同じ責任を負います。 2つのオプションがあることがわかります:superを呼び出さないで、親クラスのロジックを壊すリスクを回避するか、例外を取得し、余分な何かをスキップするリスクがあるコードで呼び出されることが保証されていない観測に対してのみsuperを呼び出します。



 static void* ETRDocumentCellIsFavoriteContext = &ETRDocumentCellIsFavoriteContext; static void* ETRDocumentCellTitleContext = &ETRDocumentCellTitleContext; @implementation ETRDocumentCell - (void)awakeFromNib { [super awakeFromNib]; [self addObserver:self forKeyPath:@"document.isFavorite" options:0 context:ETRDocumentCellIsFavoriteContext]; [self addObserver:self forKeyPath:@"document.title" options:0 context:ETRDocumentCellTitleContext]; } - (void)dealloc { [self removeObserver:self forKeyPath:@"document.isFavorite" context:ETRDocumentCellIsFavoriteContext]; [self removeObserver:self forKeyPath:@"document.title" context:ETRDocumentCellTitleContext]; } - (void)toggleIsFavorite { self.document.isFavorite = !self.document.isFavorite; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ETRDocumentCellIsFavoriteContext) { self.isFavoriteButton.selected = self.document.isFavorite; } else if (context == ETRDocumentCellTitleContext) { self.titleLabel.text = self.document.title; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @end
      
      





与えられた例から、KVOを正しく実装するためには、多くの自明ではない非自明なアクションを実行する必要があることがわかります。 これらのレベルの一部が標準またはサードパーティのライブラリに実装されている場合、または製品自体がそのクラスの一部からの継承を前提とするライブラリである場合、継承のすべてのレベルで一貫した方法で何を行う必要がありますか?

さらに、プログラマーは、observerクラスのすべてのアクティブな監視を明確に監視して、それらがobserveValueForKeyPathで処理され、適切なタイミングで停止することを確認する必要があります(たとえば、オブザーバーが削除されたとき)。 これは、いくつかの場所でのリンクされたコードの多様性(コンテキストの定義、観測の追加、削除、処理)によって複雑になり、観測の存在を検証することが不可能であり、存在しない観測を停止しようとすると例外が発生します:



***キャッチされない例外「NSRangeException」によるアプリの終了、

理由: 'オブザーバー<ETRAdvancedDocumentCell 0x1566cdd0>を削除できません

<ETRAdvancedDocumentCell 0x1566cdd0>からのキーパス「document.title」

オブザーバーとして登録されていないためです。



多くの場合、viewDidLoad、vewDidUnload、viewWillAppear、viewDidAppear、viewWillDisappear、またはviewDidDisappearのいずれかのメソッドの実装内にオブザーバーとして自分自身を追加し、これらのメソッドの別の監視を停止するUIViewControllerを見つけることができます。 ただし、特にカスタムコンテナビューコントローラを使用する場合、特にshouldAutomaticallyForwardAppearanceMethodsがNOを返す場合、これらの呼び出しの厳密な一致を保証するものはありません。 特に、UINavigationControllerスタックに含まれるコントローラーのこれらの呼び出しのロジックは、ナビゲーションスタックを逆方向に移動するための対話型ジェスチャーの導入により、iOS 7で変更されました。 また、オブザーバブルとして渡されるオブジェクトへの参照は、これらの呼び出し間で変更される可能性があります。

その結果、一部の開発者は次のようなソリューションの使用を真剣に提案することさえあります



 @try { [self.document removeObserver:self forKeyPath:@"isFavorite" context:DocumentCellIsFavoriteContext]; } @catch (NSException *exception) {}
      
      





このようなものを見ると、幼少期にVisual Basicで「On Error Resume Next」という行を書き、「創造物」が奇跡的に落ちなくなったのを思い出します。



書かれたすべてから、KVOは非常に強力なテクノロジーであり、不便なだけでなく、それを使用するアプリケーションにとって致命的なAPIを介してアクセスできるということです。 このような状況はプログラミングの分野では珍しいことではありません。確実な解決方法は、実装内のすべての欠点を分離して中和する、より便利で安全なインターフェースを作成することです。



KVOの場合、継承とremoveObserverの両方の問題の根本は、1つのオブザベーションがプログラマを追加した後にそのアイデンティティを失うことです。 「具体的にこの観測」を停止する代わりに、開発者は「指定された基準を満たすすべての観測」を停止するように強制されます。 さらに、そのような観察がいくつかある場合もあれば、まったくない場合もあります。 observeValueForKeyPathの実装でも同じことが起こります。オブジェクトとキーによる観測を区別するのが十分でない場合、特定のコンテキストに頼らなければなりません。 しかし、コンテキストでさえ、観測を追加する特定の動作を決定するのではなく、実行されるコードの行を決定するだけです。 同じオブザーバー、監視対象オブジェクト、およびキーパスで同じコード行が2回呼び出された場合、これら2つの呼び出しの結果は区別できません。 同様に、継承の問題は、親クラスと子クラスがKVOの実装の詳細に関連しているという事実によっても発生します(KVOの観点からは、オブジェクトは同じオブザーバーであるため、確実にカプセル化する必要があります)。



これらの考慮事項から、KVOをより信頼性の高い方法で使用するには、個々の観測値に識別情報を与える必要があります。つまり、観測値ごとに個別のオブジェクトを作成する必要があります。 同じオブジェクトは、標準のKVOインターフェイスの観点からオブザーバーになる必要があります。 正確に1つのオブジェクトの1つのkeyPathを観察し、この観察をそれ自身のライフタイムに明確にリンクすることにより、このオブジェクトは上記の危険から確実に保護されます。 観測値の変更に関するメッセージを受信した場合、彼が行うことは、記事の冒頭に示されている最初の3つの方法のいずれかによって別のオブジェクトに通知することだけです。

そのようなオブジェクトを実装してみましょう。



 @interface ETRKVO : NSObject @property (nonatomic, unsafe_unretained, readonly) id subject; @property (nonatomic, copy, readonly) NSString *keyPath; @property (nonatomic, copy) void (^block)(ETRKVO *kvo, NSDictionary *change); - (id)initWithSubject:(id)subject keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(void (^)(ETRKVO *kvo, NSDictionary *change))block; - (void)stopObservation; @end static void* ETRKVOContext = &ETRKVOContext; @implementation ETRKVO - (id)initWithSubject:(id)subject keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(void (^)(ETRKVO *kvo, NSDictionary *change))block { self = [super init]; if (self) { _subject = subject; _keyPath = [keyPath copy]; _block = [block copy]; [subject addObserver:self forKeyPath:keyPath options:options context:ETRKVOContext]; } return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ETRKVOContext) { if (self.block) self.block(self, change); } // NSObject does not implement observeValueForKeyPath } - (void)stopObservation { [self.subject removeObserver:self forKeyPath:self.keyPath context:ETRKVOContext]; _subject = nil; } - (void)dealloc { [self stopObservation]; } @end
      
      





代替ソリューションは、 ReactiveCocoaライブラリで見つけることができます。ReactiveCocoaライブラリは、Objective-Cのプログラミングパラダイムを根本的にシフトすると主張しており、やや時代遅れのMAKVONotificationCenterにあります。

さらに、NSNotificationCenter:と同じ理由で同様の変更が行われました。iOS4では、通知サブスクリプションを識別するオブジェクトを返すaddObserverForName:object:queue:usingBlock :メソッドが追加されました。



ETRKVOインターフェースは、オプションの動作と引数の変更を考慮することにより、いくらか簡素化できます。

NSKeyValueObservingOptionsは、次のフラグを組み合わせることができるビットマスクです。





最初の2つは、観察された属性の古い値と新しい値がchange引数に存在する必要があることを示しています。 これは、わずかな減速を除いて、マイナスの結果を引き起こすことはありません。

NSKeyValueObservingOptionInitialを指定すると、オブザベーションが追加されるとすぐにobserValueForKeyPathが呼び出されますが、これは一般的には役に立ちません。

NSKeyValueObservingOptionPriorを指定すると、値を変更した後だけでなく、その前にもobserValueForKeyPathが呼び出されます。 ただし、NSKeyValueObservingOptionNewフラグが指定されている場合でも、新しい値は送信されません。 これの必要性は非常にまれに満たすことができ、ほとんどの場合、何らかの「松葉杖」を実装するプロセスでのみ発生します。

したがって、常にオプションとして渡すことができます(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)。



引数(NSDictionary *)の変更には、次のキーが含まれる場合があります。





最初の2つには、対応するオプションで要求できる同じ古い値と新しい値が含まれています。スカラー型の値はNSNumberまたはNSValueでラップされ、nilの代わりにシングルトンオブジェクト[NSNull null]が転送されます。

次の2つは、変更可能なコレクションを観察する場合にのみ必要です。これは、おそらく悪い考えです。

最後のキーは、NSKeyValueObservingOptionPriorオプションで行われた前の変更呼び出しの場合にのみ送信されます。

したがって、キーNSKeyValueChangeNewKeyおよびNSKeyValueChangeOldKeyのみを考慮し、それらの値を拡張形式でブロックに渡すことができます。

したがって、ETRKVOは次のように変更できます。



 - (id)initWithSubject:(id)subject keyPath:(NSString *)keyPath block:(void (^)(ETRKVO *kvo, id oldValue, id newValue))block { self = [super init]; if (self) { _subject = subject; _keyPath = [keyPath copy]; _block = [block copy]; [subject addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:ETRKVOContext]; } return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ETRKVOContext) { if (self.block) { id oldValue = change[NSKeyValueChangeOldKey]; if (oldValue == [NSNull null]) oldValue = nil; id newValue = change[NSKeyValueChangeNewKey]; if (newValue == [NSNull null]) newValue = nil; self.block(self, oldValue, newValue); } } // NSObject does not implement observeValueForKeyPath }          NSObject,    : - (ETRKVO *)observeKeyPath:(NSString *)keyPath withBlock:(void (^)(ETRKVO *kvo, id oldValue, id newValue))block;
      
      





keyPathは多くの場合、対応するゲッターに一致するプロパティ名であるため、keyPath文字列の代わりにこのゲッターのセレクターを使用する方が便利です。この場合、オートコンプリートが機能し、書き込み時またはプロパティの名前変更時にミスをする可能性が低くなります。



 - (ETRKVO *)observeSelector:(SEL)selector withBlock:(void (^)(ETRKVO *kvo, id oldValue, id newValue))block { return [[ETRKVO alloc] initWithSubject:self keyPath:NSStringFromSelector(selector) block:block]; }
      
      





このクラスとカテゴリを使用してセルを書き換えます



 @interface ETRDocumentCell () @property (nonatomic, strong) ETRKVO* isFavoriteKVO; @property (nonatomic, strong) ETRKVO* titleKVO; @end @implementation ETRDocumentCell - (void)awakeFromNib { [super awakeFromNib]; typeof(self) __weak weakSelf = self; self.isFavoriteKVO = [self observeKeyPath:@"document.isFavorite" withBlock:^(ETRKVO *kvo, id oldValue, id newValue) { weakSelf.isFavoriteButton.selected = weakSelf.document.isFavorite; }]; self.titleKVO = [self observeKeyPath:@"document.title" withBlock:^(ETRKVO *kvo, id oldValue, id newValue) { weakSelf.titleLabel.text = weakSelf.document.title; }]; } - (void)dealloc { [self.isFavoriteKVO stopObservation]; [self.titleKVO stopObservation]; } - (void)toggleIsFavorite { self.document.isFavorite = !self.document.isFavorite; } @end @interface ETRAdvancedDocumentCell () @property (nonatomic, strong) ETRKVO* advancedIsFavoriteKVO; @end @implementation ETRAdvancedDocumentCell - (void)awakeFromNib { [super awakeFromNib]; typeof(self) __weak weakSelf = self; self.advancedIsFavoriteKVO = [self observeKeyPath:@"document.isFavorite" withBlock:^(ETRKVO *kvo, id oldValue, id newValue) { [weakSelf updateBackgroundColor]; }]; } - (void)dealloc { [self.advancedIsFavoriteKVO stopObservation]; } ...
      
      





ETRKVOの完全な実装とサンプルはこちらから ダウンロードできますが、ここで



明らかでないトリックは、メモリリークを防ぐためにweakSelfを使用することです。ブロックが強力なリンクによって自己をキャプチャした場合、強力なリンクサイクルが形成されます:ETRDocumentCell→isFavoriteKVO→ブロック→ETRDocumentCell。ただし、ブロックを積極的に使用している場合、弱いリンクを使用してオブジェクトをキャプチャすることはすでに習慣になっているはずです。



ETRKVOクラスのオブジェクトは、セルがそれらへの参照を失った後に削除されますが(それら自体は削除されます)、リンクをカウントするときにガベージコレクションを待つような効果はありませんが、それでもリンクが自動解放になった場合、すぐに削除が行われない場合があることに注意してくださいプール。したがって、ETRKVOオブジェクトまたは監視対象オブジェクトが削除される前に、常に手動でstopObservationを呼び出す必要があります。一連の異なる観測に同じプロパティを使用する場合、そのセッターでstopObservationを呼び出すと便利です。



 - (void)setIsFavoriteKVO:(ETRKVO *)isFavoriteKVO { [_isFavoriteKVO stopObservation]; _isFavoriteKVO = isFavoriteKVO; } - (void)dealloc { self.isFavoriteKVO = nil; }
      
      





自動参照カウントが弱いKVOリンクを準拠した方法でゼロにできる場合、つまり、値を監視しているオブジェクトにこれが通知される場合、監視の手動終了の要件は緩和できます。現時点では、iOS 7ではこれは不可能ですdeallocメソッドの実装を置き換えるような「ダーティトリック」を考慮しない限り)。



変更ハンドラーは、変更が発生した実行スレッドと同じスレッドで呼び出されることを忘れないでください。別のスレッドから変更できるオブジェクトを観察することが正当化され、マルチスレッドに対する軽薄な態度の結果ではない場合、通常、ハンドラーコードはdispatch_asyncでラップする必要があります。。この場合、外部ブロックは厳密に定義されたストリームに関連するオブジェクト(たとえば、UIView、UIViewController、NSManagedObject)を強力なリンクでキャプチャしないという事実に特に注意する必要があります



監視対象の値が変更されたときにハンドラーが起動しない場合、監視対象の属性がKVOに準拠していない可能性があります。そのようにする方法は、KVOコンプライアンスおよび従属キーの登録のドキュメントセクションで徹底的に説明されています。プロパティに標準の(合成された)プロパティセッターを使用せず、セッターを自分で定義した場合でも、このプロパティはKVOに準拠したままであることを別に言及する価値があります。



KVOの潜在的な危険性をすべて認識し、それらの一部を無効化したとしても、いかなる場合でもKVOを無駄に使用しないでください。テクノロジーの悪用は、「[Technology Name] hell」と呼ばれる現象を引き起こします。KVOを使用して作成されたオブジェクト間の接続は非常に弱く見え、制御不能になりますが、本当に痛いことがあります。私たちの場合、「KVO hell」は、予期しない結果やパフォーマンスの低下につながる予測プロセッサの予測不可能な雪崩のようなトリガー、またはスタックオーバーフローで終わる循環呼び出しでさえ表現できます。



  1. 2013年11月のTIOBEプログラミングコミュニティインデックス
  2. Key-Valueコーディングプログラミングガイド
  3. Key-Value Observingプログラミングガイド
  4. ブロックプログラミングトピック
  5. ARCリリースノートへの移行
  6. Concepts in Objective-C Programming: Delegates and Data Sources
  7. Programming with Objective-C: Working with Protocols
  8. Concepts in Objective-C Programming: Target-Action
  9. stackoverflow: How to cancel NSBlockOperation
  10. Notification Programming Topics
  11. NSKeyValueObserving Protocol Reference
  12. iOS Debugging Magic
  13. NSHipster: Key-Value Observing
  14. ReactiveCocoa
  15. MAKVONotificationCenter
  16. Weak properties KVO compliance
  17. Method Swizzling
  18. Grand Central Dispatch (GCD) Reference
  19. Simple and Reliable Threading with NSOperation



All Articles