14

NSProxyまだ存在していないものの代用オブジェクトとして非常にうまく機能するようです。例えば。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

上記のコードは、プロキシが表すターゲットにメソッド呼び出しを透過的に渡します。ただし、ターゲットでのKVOの監視と通知を処理していないようです。NSProxy渡されるオブジェクトの略としてサブクラスを使用しようとしましたNSTableViewが、次のエラーが発生します。

Cannot update for observer <NSAutounbinderObservance 0x105889dd0> for
 the key path "objectValue.status" from <NSTableCellView 0x105886a80>,
 most likely because the value for the key "objectValue" has changed
 without an appropriate KVO notification being sent. Check the 
KVO-compliance of the NSTableCellView class.

NSProxyKVOに準拠した透明にする方法はありますか?

4

2 に答える 2

20

問題の核心は、Key-Value Observingの内臓がに存在しNSObjectNSProxyから継承されないことですNSObjectNSProxyどのアプローチでも、オブジェクトが独自の遵守リスト(つまり、外部の人々がそれについて観察することを望んでいるもの)を保持する必要があると私は合理的に確信しています。これだけで、NSProxyの実装にかなりの重みが加わります。

ターゲットを観察する

プロキシのオブザーバーに実際のオブジェクトを実際に監視させようとしたようです。つまり、ターゲットに常にデータが入力されていて、すべての呼び出しをターゲットに転送しただけの場合は、転送addObserver:...removeObserver:...呼び出しも行われます。これに伴う問題は、あなたが次のように言うことから始めたことです。

NSProxyは、まだ存在していないオブジェクトの代用オブジェクトとして非常にうまく機能しているようです。

完全を期すために、このアプローチの根幹のいくつかと、それが機能しない理由(少なくとも一般的なケースでは)について説明します。

これが機能するためには、NSProxyサブクラスは、ターゲットが設定される前に呼び出された登録メソッドの呼び出しを収集し、ターゲットが設定されたときにそれらをターゲットに渡す必要があります。削除も処理する必要があると考えると、これはすぐに厄介になります。後で削除された監視を追加することは望ましくありません(監視オブジェクトの割り当てが解除されている可能性があるため)。また、観測を追跡する方法でオブザーバーを保持したくない場合もあります。これにより、意図しない保持サイクルが発生しないようにします。処理する必要のあるターゲット値の次の可能な遷移が表示されます

  1. ターゲットはnil初期化されていましたが、nil遅くなりません
  2. ターゲットが設定されていないnilnil後でなります
  3. ターゲットが非値に設定された後、別の非値nilに変更されましたnil
  4. ターゲットはnil(初期化されていない)、nil後でないようになります

...そして、ケース#1の場合はすぐに問題が発生します。KVOオブザーバーがobjectValue(常にプロキシになるため)監視するだけであれば、おそらくここで大丈夫ですが、オブザーバーがプロキシ/実オブジェクトを通過するkeyPathを監視したとしましょうobjectValue.status。これは、KVO機構がvalueForKey: objectValue観測のターゲットを呼び出してプロキシを取り戻した後、プロキシを呼び出しvalueForKey: statusnil戻ってきたことを意味します。ターゲットが非になるとnil、KVOはその値がその下から変更されたと見なし(つまり、KVOに準拠していない)、引用したエラーメッセージが表示されます。ターゲットを一時的に強制的に戻す方法がある場合は、nilそのstatus動作をオンにして、-[target willChangeValueForKey: status]、動作をオフにしてから、を呼び出します-[target didChangeValueForKey: status]。とにかく、同じ落とし穴があるので、ケース#1でここで停止できます。

  1. nilあなたがそれを呼び出しても何もしませんwillChangeValueForKey:(つまり、KVO機械はへの移行中またはからの移行中にその内部状態を更新することを決して知りませんnil
  2. ターゲットオブジェクトに、一時的に嘘をついてvalueForKeyから戻るメカニズムを強制するnil:すべてのキーについて、指定された要望が「透過プロキシ」である場合、かなり面倒な要件のように思われます。
  3. nilターゲットを持つプロキシでsetValue:forKey:を呼び出すとはどういう意味ですか?それらの値を維持しますか?本当のターゲットを待っていますか?投げますか?巨大な未解決の問題。

このアプローチに対する1つの可能な変更は、実際のターゲットがnilおそらく空の場合にサロゲートターゲットを使用し、 NSMutableDictionaryKVC/KVO呼び出しをサロゲートに転送することです。これにより、意味のある呼び出しができないという問題が解決さwillChangeValueForKey:nilます。とはいえ、観測のリストを維持していると仮定すると、KVOがケース#1の場合にここでターゲットを設定することに関連する次のシーケンスを許容することは楽観的ではありません。

  1. 外部のオブザーバーが呼び出し-[proxy addObserver:...]、プロキシが辞書の代理に転送します
  2. -[surrogate willChangeValueForKey:ターゲットが設定されているため、プロキシ呼び出し]
  3. 代理呼び出し-[surrogate removeObserver:...]代理
  4. -[newTarget addObserver:...]新しいターゲットでのプロキシ呼び出し
  5. プロキシコール-[newTarget didChangeValueForKey:]コール#2のバランスを取る

これが同じエラーにつながることはないかどうかは私にはわかりません。このアプローチ全体は、本当にホットな混乱になりつつありますね。

私にはいくつかの代替案がありましたが、#1はかなり些細なことであり、#2と#3は十分に単純でも、自信を持って刺激するものでもないので、それらをコーディングするために時間を費やしたくなります。しかし、後世のために、どうですか:

1.NSObjectControllerプロキシに使用する

確かに、それはコントローラーを通過するための追加のキーでkeyPathsを強化しますが、これはある種のNSObjectController's理由ですよね?コンテンツを含めることができnil、すべての観測のセットアップと破棄を処理します。透過的な呼び出し転送プロキシの目標は達成されませんが、たとえば、非同期で生成されたオブジェクトの代用を目標とする場合、非同期生成操作で最終的なものを配信するのはおそらくかなり簡単です。コントローラへのオブジェクト。これはおそらく最も手間がかからないアプローチですが、実際には「透明な」要件に対応していません。

2.NSObjectプロキシにサブクラスを使用します

NSProxy's主な機能は、魔法が含まれていることでありません。主な機能は、 (すべての)NSObject実装が含まれていないことです。NSObject不要なすべての動作をオーバーライドし、それらを転送メカニズムに戻す努力を惜しまない場合はNSProxy KVOサポートメカニズムを残したまま、提供されたものと同じネット値を得ることができます。場所。そこから、プロキシがターゲット上で監視されたものと同じキーパスをすべて監視し、ターゲットからの再ブロードキャストwillChange...didChange...通知を行って、外部の監視者がそれらをプロキシから送信されたものと見なすようにします。

...そして今、本当にクレイジーな何かのために:

3.(Ab)ランタイムを使用して、NSObjectKVC/KVOの動作をNSProxyサブクラスに取り込みます

ランタイムを使用して、KVCおよびKVOに関連するメソッド実装を(ie)から取得し、NSObjectそれらのメソッド(ie )をプロキシサブクラスにclass_getMethodImplementation([NSObject class], @selector(addObserver:...))追加できます。class_addMethod([MyProxy class], @selector(addObserver:...), imp, types)

NSObjectこれは、パブリックKVOメソッドが呼び出すすべてのプライベート/内部メソッドを把握し、それらを卸売りするメソッドのリストに追加するという推測とチェックのプロセスにつながる可能性があります。KVOの遵守を維持する内部データ構造は、すべてのインスタンスがスペース価格を支払うことを意味するため、のivarsでは維持されないと仮定するのは論理的ですNSObject( ivarsがないNSObject.hことを示します-それは最近何も意味しません) 。NSObjectまた、KVO通知のスタックトレースに多くのC関数が含まれています。NSProxyがKVOのファーストクラスの参加者になるのに十分な機能を導入できたと思います。その時点から、このソリューションは次のようになります。NSObjectベースのソリューション; ターゲットを観察し、通知が自分からのものであるかのように再ブロードキャストし、さらに、ターゲットへの変更に関するwillChange/didChange通知を偽造します。KVOパブリックAPI呼び出しのいずれかを入力するときにフラグを設定し、パブリックAPIのときにフラグをクリアするまで、呼び出されたすべてのメソッドを引き継ぐことで、呼び出し転送メカニズムでこの一部を自動化できる場合もあります。コールリターン-これらのメソッドを引き継ぐことでプロキシの透明性が損なわれないことを保証しようとする問題があります。

これが失敗するのではないかと思うのは、KVOが実行時にクラスの動的サブクラスを作成するメカニズムです。そのメカニズムの詳細は不透明であり、おそらくから持ち込むためのプライベート/内部メソッドを理解するための別の長い列につながるでしょうNSObject。結局、このアプローチも完全に脆弱であり、内部実装の詳細が変更されないようにします。

...結論は

要約すると、問題は、KVOが、キースペース全体で一貫性があり、知識があり、一貫して更新される(通知を介して)状態を期待しているという事実に要約されます。(バインディングをサポートまたは編集できるようにする場合は、そのリストに「可変」を追加し-setValue:forKey:ます。)汚いトリックを除いて、ファーストクラスの参加者であることは。であることを意味しNSObjectsます。チェーン内のこれらのステップの1つが、他の内部状態を呼び出すことによってその機能を実装する場合、それはその特権ですが、KVOコンプライアンスのすべての義務を果たす責任があります。

NSObjectそのため、これらのソリューションのいずれかが努力する価値がある場合は、「プロキシとしてではなく、を使用する」ことにお金をかけると思いますNSProxy。したがって、質問の正確な性質を理解するために、KVOに準拠したサブクラスを作成する方法があるかもしれませNSProxyんが、それだけの価値があるとは思えません。

于 2013-01-03T15:13:41.317 に答える
1

OPのまったく同じユースケース(バインディングなし)はありませんが、私のものは似ていました。サーバーから実際にロードされる別のオブジェクトとして表示されるNSProxyサブクラスを作成しています。ロード中、他のオブジェクトはプロキシにサブスクライブでき、プロキシはオブジェクトが到着するとすぐにKVOを転送します。

NSArrayプロキシには、すべてのオブザーバーを記録する単純なプロパティがあります。実際のオブジェクトがロードされるまで、プロキシはnilで戻りますvalueForKey:。到着するrealObjectと、プロキシはaddObserver:forKeyPath:options:context:実際のオブジェクトを呼び出し、ランタイムの魔法を使って、のすべてのプロパティをウォークスルーし、realObjectこれを実行します。

    id old = object_getIvar(realObject, backingVar);
    object_setIvar(realObject, backingVar, nil);
    [realObject willChangeValueForKey:propertyName];
    object_setIvar(realObject, backingVar, old);
    [realObject didChangeValueForKey:propertyName];

これは機能しているようですが、少なくともKVOコンプライアンスエラーはまだ発生していません。ただし、最初はすべてのプロパティがnilであり、次にnilから実際の値に変更されます。上記の最初のステートメントでipmccが言ったように、この投稿は単なる確認です。彼が提案した2番目の代理は実際には必要ないことに注意してください。必要なのは、オブザーバーを自分で追跡することだけです。

于 2014-02-13T18:48:41.340 に答える