NSProxy和关键值观察 [英] NSProxy and Key Value Observing
问题描述
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
以收集在设置目标之前调用的注册方法的调用,然后在设置目标时将它们传递到目标。当你认为你还必须处理删除时,这很快就变得毛茸茸;你不想添加一个随后被删除的观察(因为观察对象可能已被释放)。您也可能不希望您的跟踪观察结果的方法保留任何观察者,以免这创建意外的保留周期。我看到需要处理的目标值可能出现以下转换
- 目标是
nil
on init,成为非 nil 后
- 目标设为非 -
nil
- 目标设为非 -
nil
,然后更改为另一个非nil
值
- 目标是
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种情况下停止,因为它们有相同的缺陷:
nil
不会做任何事情,如果你调用willChangeValueForKey:
它(即KVO机械永远不会知道更新其内部状态转换或从nil
)
- 强制任何目标对象具有暂时停止的机制,并返回
nil
from valueForKey:对于所有的键,当所谓的愿望是一个透明的代理时,似乎是一个相当繁重的要求。
- 甚至意味着调用setValue:forKey:on a代理与
nil
目标?我们保持这些价值观吗?等待真正的目标?我们扔?巨大的开放问题。
这种方法的一个可能的修改是使用替代目标,当真正的目标是
nil
,可能是一个空的NSMutableDictionary
,并转发KVC / KVO调用到代理。这将解决无法在nil
上有意义地调用willChangeValueForKey:
的问题。所有这一切,假设你保持你的观察列表,我不容乐观KVO将容忍以下序列将涉及设置目标这里情况#1:
- 外部观察员调用
- [proxy addObserver:...]
,代理转发到字典代理
- 代理调用
- 代理调用
- [代理willChangeValueForKey:
代理
- 代理调用
- [newTarget addObserver:...]
新的目标
- 代理调用
- [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 toNSTableView
, 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
, andNSProxy
doesn't inherit fromNSObject
. I'm reasonably confident that any approach will require theNSProxy
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:...
andremoveObserver:...
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
- Target was
nil
on init, becomes non-nil
later- Target was set non-
nil
, becomesnil
later- Target was set non-
nil
, then changes to another non-nil
value- 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, sayobjectValue.status
. This means that the KVO machinery will have calledvalueForKey: objectValue
on the target of the observation and gotten your proxy back, then it will callvalueForKey: status
on your proxy and will have gottennil
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 returnnil
forstatus
, 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:
nil
won't do anything if you callwillChangeValueForKey:
on it (i.e. the KVO machinery will never know to update its internal state during a transition to or fromnil
)- 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".- 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 emptyNSMutableDictionary
, and forward KVC/KVO invocations to the surrogate. This would solve the problem of not being able to meaningfully callwillChangeValueForKey:
onnil
. 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:
- outside observer calls
-[proxy addObserver:...]
, proxy forwards to dictionary surrogate- proxy calls
-[surrogate willChangeValueForKey:
] because target is being set- proxy calls
-[surrogate removeObserver:...
] on surrogate- proxy calls
-[newTarget addObserver:...]
on new target- proxy calls
-[newTarget didChangeValueForKey:
] to balance call #2It'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 proxySure, 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 havenil
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) theNSObject
implementation in it. If you're willing to go to the effort to override allNSObject
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 byNSProxy
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 rebroadcastingwillChange...
anddidChange...
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 yourNSProxy
subclassYou 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 ofNSObject
(NSObject.h
indicates no ivars -- not that that means anything these days) since that would mean that everyNSObject
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 theNSObject
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 beingNSObjects
. 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 notNSProxy
." So to get to the exact nature of your question, there may be a way to make anNSProxy
subclass that is KVO compliant, but it hardly seems like it would worth it.这篇关于NSProxy和关键值观察的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!