NSProxy和关键值观察 [英] NSProxy and Key Value Observing

查看:210
本文介绍了NSProxy和关键值观察的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

NSProxy 似乎可以很好地工作,因为那些尚不存在的对象。例如

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

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

上述代码将透明地传递代理所代表的目标的任何方法调用。然而,它似乎不处理KVO观察和目标上的通知。我尝试使用 NSProxy 子类作为传递给 NSTableView 的对象,但我得到以下错误。

 无法为观察者更新来自< NSTableCellView 0x105886a80>,
的关键路径objectValue.status,很可能是因为在没有发送适当的KVO通知的情况下,关键字objectValue的值已经改变了
。检查NSTableCellView类的
KVO合规性。有没有办法让透明 NSProxy

问题的症结在于关键价值观察的胆量存在于<$ c $ c> NSObject NSProxy 不会从 NSObject 继承。我有理由相信,任何方法都需要 NSProxy 对象保存自己的观察列表(即外面的人希望观察它)。这将单独添加对你的NSProxy实现非常重要。



观察目标



看起来你已经尝试过观察者的代理实际上观察真实的对象 - 换句话说,如果目标总是填充,并且你只是转发所有调用到目标,你也将转发 addObserver:... removeObserver:... 调用。这样的问题是,你开始时说:


NSProxy似乎可以很好地工作, $ b尚不存在


为了完整起见,我将描述这种方法的一些胆量,以及为什么它不能工作(至少对于一般情况):



为了这个工作,你的 NSProxy 以收集在设置目标之前调用的注册方法的调用,然后在设置目标时将它们传递到目标。当你认为你还必须处理删除时,这很快就变得毛茸茸;你不想添加一个随后被删除的观察(因为观察对象可能已被释放)。您也可能不希望您的跟踪观察结果的方法保留任何观察者,以免这创建意外的保留周期。我看到需要处理的目标值可能出现以下转换


  1. 目标是 nil on init,成为非 nil 后

  2. 目标设为非 - nil

  3. 目标设为非 - nil ,然后更改为另一个非 nil

  4. 目标是 nil (不在init),变为非 nil 后来

...我们在case#1遇到问题。如果KVO观察器只观察到 objectValue (因为它将永远是你的代理),我们可能就在这里,但是说观察者已经观察到一个keyPath通过你的代理/ real-object,例如 objectValue.status 。这意味着KVO机制将在观察的目标上调用 valueForKey:objectValue 并获取您的代理,然后它将调用 valueForKey:status 在您的代理上,将获得 nil 回来。当目标变为非 - nil 时,KVO将认为该值已从其下更改(即不符合KVO),您会得到您引用的错误消息。如果你有办法临时强制目标为 status 返回 nil ,你可以打开该行为,调用 - [target willChangeValueForKey:status] ,关闭行为,然后调用 - [target didChangeValueForKey:status] 。无论如何,我们可以在第1种情况下停止,因为它们有相同的缺陷:


  1. nil 不会做任何事情,如果你调用 willChangeValueForKey:它(即KVO机械永远不会知道更新其内部状态转换或从 nil

  2. 强制任何目标对象具有暂时停止的机制,并返回 nil from valueForKey:对于所有的键,当所谓的愿望是一个透明的代理时,似乎是一个相当繁重的要求。

  3. 甚至意味着调用setValue:forKey:on a代理与 nil 目标?我们保持这些价值观吗?等待真正的目标?我们扔?巨大的开放问题。

这种方法的一个可能的修改是使用替代目标,当真正的目标是 nil ,可能是一个空的 NSMutableDictionary ,并转发KVC / KVO调用到代理。这将解决无法在 nil 上有意义地调用 willChangeValueForKey:的问题。所有这一切,假设你保持你的观察列表,我不容乐观KVO将容忍以下序列将涉及设置目标这里情况#1:


  1. 外部观察员调用 - [proxy addObserver:...] ,代理转发到字典代理

  2. 代理调用 - 代理调用 - [代理willChangeValueForKey:代理

  3. 代理调用 - [newTarget addObserver:...] 新的目标

  4. 代理调用 - [newTarget didChangeValueForKey:]来平衡调用#2

我不清楚这并不会导致同样的错误。



我确实有几个替代的想法,但是#1是相当微不足道的,#2和#3都不够简单或充满信心,足以让我想烧掉时间来编写代码。但是,对于后代,如何:



1。使用 NSObjectController 作为您的代理



当然,它会通过一个额外的密钥,但这是一种 NSObjectController的整个原因,是吗?它可以有 nil 内容,并将处理所有的观察设置和拆除。它没有实现透明的,调用转发代理的目标,但是例如,如果目标是对于一些异步生成的对象具有替代,则可能相当直接地使异步生成操作递送最终对象到控制器。这可能是最省力的方法,但并不真正解决透明的要求。



2。为您的代理使用 NSObject 子类



NSProxy的特征不是它具有一些魔法 - 其主要特征是它没有(全部) NSObject code>实现。如果您愿意努力覆盖您不要所需的所有 NSObject 行为,并将他们分流回您的转发机制,您可以得到与 NSProxy 提供的相同的净值,但是KVO支持机制留在原地。从那里,这是一个问题,你的代理观察目标上观察到的所有相同的关键路径,然后转播 willChange ... didChange ... 来自目标的通知,以便外部观察者将其视为来自您的代理。



...真的很疯狂:



3。 (Ab)使用运行时将 NSObject KVC / KVO行为带入您的 NSProxy 子类



您可以使用运行时从 NSObject (即 class_getMethodImplementation([...])获得与KVC和KVO相关的方法实现, NSObject类],@selector(addObserver:...))),然后你可以添加这些方法(例如 class_addMethod([MyProxy class],@selector(addObserver: ...),imp,types))到您的代理子类。



这可能导致一个猜测和检查过程找出公共KVO方法调用的 NSObject 中的所有私有/内部方法,然后将它们添加到批发的方法列表中。似乎合乎逻辑的假设保持KVO观察值的内部数据结构不会在 NSObject NSObject.h 表示没有ivars - 不是那意味着任何这些天),因为这意味着每个 NSObject 实例将支付空间价格。另外,我看到很多C函数在KVO通知的堆栈跟踪。我想你可能得到一个点,你带来了足够的功能,NSProxy成为KVO的一流参与者。从这一点开始,这个解决方案看起来像基于 NSObject 的解决方案;你观察目标并重新广播通知,就好像它们来自你,另外伪造了对目标的任何更改的changeChange / didChange通知。您可以通过在输入任何KVO公共API调用时设置标记,然后尝试覆盖您调用的所有方法,直到您能够自动执行调用转发机制中的一些操作,这样您就可以当公共API调用返回时,清除标志 - 这将有助于确保带来这些方法不会破坏您的代理的透明度。



我怀疑这将跌倒是在KVO在运行时创建你的类的动态子类的机制。这个机制的细节是不透明的,可能会导致另一个长列车计算私人/内部方法引入从 NSObject 。最终,这种方法也是完全脆弱的,以免任何内部实现细节改变。



...总结



抽象地,问题归结为这样的事实,KVO期望在它的关键空间一致的,可知的,一致地更新(通过通知)状态。 (如果您想支持 -setValue:forKey:或可编辑的绑定,请将mutable添加到该列表中。)阻止脏的技巧,作为一级参与者意味着 NSObjects 。如果链中的其中一个步骤通过调用其他内部状态来实现它的功能,这是它的特权,但它将负责履行其所有的义务KVO合规性。



因此,我认为如果这些解决方案都值得付出努力,我会把我的钱放在使用 NSObject 作为代理,而不是 NSProxy 。为了得到你的问题的确切性质,可能是一种方法使一个 NSProxy 子类是KVO兼容,但它似乎就像它值得的。


NSProxy seems to work very well as stand-in objects for those that don't yet exist. For example.

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

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

The above code will transparently pass any method invocation to the target that the proxy represents. However, it doesn't seem to handle KVO observations and notifications on the target. I tried to use a NSProxy subclass as standing for objects to be passed to NSTableView, but I'm getting the following error.

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.

Is there a way to make transparent NSProxy that is KVO compliant?

解决方案

The crux of the issue is that the guts of Key-Value Observing lives in NSObject, and NSProxy doesn't inherit from NSObject. I'm reasonably confident that any approach will require the NSProxy object to keep its own list of observances (i.e. what outside folks are hoping to observe about it.) This alone would add considerable weight to your NSProxy implementation.

Observe the target

It looks like you've already tried having observers of the proxy actually observe the real object -- in other words, if the target were always populated, and you simply forwarded all invocations to the target, you would also be forwarding addObserver:... and removeObserver:... calls. The problem with this is that you started out by saying:

NSProxy seems to work very well as stand-in objects for those that don't yet exist

For completeness, I'll describe some of the guts of this approach and why it can't work (at least for the general case):

In order for this to work, your NSProxy subclass would have to collect invocations of the registration methods that were called before the target was set, and then pass them through to the target when it gets set. This quickly gets hairy when you consider that you must also process removals; you wouldn't want to add an observation that was subsequently removed (since the observing object could have been dealloc'ed). You also probably don't want your method of tracking observations to retain any of the observers, lest this create unintended retain cycles. I see the following possible transitions in target value that would need to be handled

  1. Target was nil on init, becomes non-nil later
  2. Target was set non-nil, becomes nil later
  3. Target was set non-nil, then changes to another non-nil value
  4. Target was nil (not on init), becomes non-nil later

...and we run into problems right away in case #1. We would probably be all right here if the KVO observer only observed objectValue (since that will always be your proxy), but say an observer has observed a keyPath that goes through your proxy/real-object, say objectValue.status. This means that the KVO machinery will have called valueForKey: objectValue on the target of the observation and gotten your proxy back, then it will call valueForKey: status on your proxy and will have gotten nil back. When the target becomes non-nil, KVO will have considered that value to have changed out from under it (i.e. not KVO compliant) and you'll get that error message you quoted. If you had a way to temporarily force the target to return nil for status, you could turn that behavior on, call -[target willChangeValueForKey: status], turn the behavior off, then call -[target didChangeValueForKey: status]. Anyway, we can stop here at case #1 because they have the same pitfalls:

  1. nil won't do anything if you call willChangeValueForKey: on it (i.e. the KVO machinery will never know to update its internal state during a transition to or from nil)
  2. forcing any target object to have a mechanism whereby it will temporarily lie and return nil from valueForKey: for all keys seems like a pretty onerous requirement, when the stated desire was a "transparent proxy".
  3. what does it even mean to call setValue:forKey: on a proxy with a nil target? do we keep those values around? waiting for the real target? do we throw? Huge open issue.

One possible modification to this approach would be to use a surrogate target when the real target is nil, perhaps an empty NSMutableDictionary, and forward KVC/KVO invocations to the surrogate. This would solve the problem of not being able to meaningfully call willChangeValueForKey: on nil. All that said, assuming you've maintained your list of observations, I'm not optimistic that KVO will tolerate the following sequence that would be involved with setting the target here in case #1:

  1. outside observer calls -[proxy addObserver:...], proxy forwards to dictionary surrogate
  2. proxy calls -[surrogate willChangeValueForKey:] because target is being set
  3. proxy calls -[surrogate removeObserver:...] on surrogate
  4. proxy calls -[newTarget addObserver:...] on new target
  5. proxy calls -[newTarget didChangeValueForKey:] to balance call #2

It's not clear to me that this won't also lead to the same error. This whole approach is really shaping up to be a hot mess, isn't it?

I did have a couple alternate ideas, but #1 is fairly trivial and #2 and #3 aren't simple enough or confidence-inspiring enough to make me want to burn the time to code them up. But, for posterity, how about:

1. Use NSObjectController for your proxy

Sure, it gums up your keyPaths with an extra key to get through the controller, but this is sort of NSObjectController's whole reason for being, right? It can have nil content, and will handle all the observation set up and tear-down. It doesn't achieve the goal of a transparent, invocation forwarding proxy, but for example, if the goal is to have a stand-in for some asynchronously generated object, it would probably be fairly straightforward to have the asynchronous generation operation deliver the final object to the controller. This is probably the lowest-effort approach, but doesn't really address the 'transparent' requirement.

2. Use an NSObject subclass for your proxy

NSProxy's primary feature isn't that it has some magic in it -- the primary feature is that it doesn't have (all) the NSObject implementation in it. If you're willing to go to the effort to override all NSObject behaviors that you don't want, and shunt them back around into your forwarding mechanism, you can end up with the same net value provided by NSProxy but with the KVO support mechanism left in place. From there, it's a matter of your proxy watching all the same key paths on the target that were observed on it, and then rebroadcasting willChange... and didChange... notifications from the target so that outside observers see them as coming from your proxy.

...and now for something really crazy:

3. (Ab)Use the runtime to bring the NSObject KVC/KVO behavior into your NSProxy subclass

You can use the runtime to get the method implementations related to KVC and KVO from NSObject (i.e. class_getMethodImplementation([NSObject class], @selector(addObserver:...))), and then you can add those methods (i.e. class_addMethod([MyProxy class], @selector(addObserver:...), imp, types)) to your proxy subclass.

This will likely lead to a guess-and-check process of figuring out all the private/internal methods on NSObject that the public KVO methods call, and then adding those to the list of methods that you wholesale over. It seems logical to assume that the internal data structures that maintain KVO observances would not be maintained in ivars of NSObject (NSObject.h indicates no ivars -- not that that means anything these days) since that would mean that every NSObject instance would pay the space price. Also, I see a lot of C functions in stack traces of KVO notifications. I think you could probably get to a point where you had brought in enough functionality for the NSProxy to be a first-class participant in KVO. From that point forward, this solution looks like the NSObject based solution; you observe the target and rebroadcast the notifications as if they came from you, additionally faking up willChange/didChange notifications around any changes to the target. You might even be able to automate some of this in your invocation forwarding mechanism by setting a flag when you enter any of the KVO public API calls, and then attempting to bring over all methods called on you until you clear the flag when the public API call returns -- the hitch there would be trying to guarantee that bringing over those methods didn't otherwise ruin the transparency of your proxy.

Where I suspect this will fall down is in the mechanism whereby KVO creates dynamic subclasses of your class at runtime. The details of that mechanism are opaque, and would probably lead to another long train of figuring out private/internal methods to bring in from NSObject. In the end, this approach is also completely fragile, lest any of the internal implementation details change.

...in conclusion

In the abstract, the problem boils down to the fact that KVO expects a coherent, knowable, consistently updated (via notifications) state across it's key space. (Add "mutable" to that list if you want to support -setValue:forKey: or editable bindings.) Barring dirty tricks, being first class participants means being NSObjects. If one of those steps in the chain implements it's functionality by calling through to some other internal state, that's its prerogative, but it'll be responsible for fulfilling all its obligations for KVO compliance.

For that reason, I posit that if any of these solutions are worth the effort, I'd put my money on the "using an NSObject as the proxy and not NSProxy." So to get to the exact nature of your question, there may be a way to make an NSProxy subclass that is KVO compliant, but it hardly seems like it would worth it.

这篇关于NSProxy和关键值观察的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆