NSProxy 和键值观察

发布于 01-01 05:14 字数 804 浏览 4 评论 0原文

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.

有没有办法使透明的 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

夜声2025-01-08 05:14:20

问题的关键在于键值观察的核心存在于 NSObject 中,而 NSProxy 并不继承自 NSObject。我有理由相信任何方法都需要 NSProxy 对象保留自己的观察列表(即外部人员希望观察到的内容)。仅此一项就会为您的 NSProxy 实现增加相当大的重量。

观察目标

看起来您已经尝试过让代理的观察者实际观察真实的对象 - 换句话说,如果目标始终被填充,并且您只是将所有调用转发到目标,那么您也将转发 < code>addObserver:... 和 removeObserver:... 调用。问题在于你一开始就说:

NSProxy 似乎作为替代对象非常适合那些
还不存在

为了完整起见,我将描述这种方法的一些核心内容以及为什么它不起作用(至少对于一般情况):

为了使其起作用,您的 NSProxy 子类必须收集在设置目标之前调用的注册方法的调用,然后在设置目标时将它们传递给目标。当您考虑还必须处理删除时,这很快就会变得棘手;您不想添加随后被删除的观察(因为观察对象可能已被释放)。您可能也不希望跟踪观察的方法保留任何观察者,以免造成意外的保留周期。我看到需要处理的目标值中可能存在以下转换

  1. 初始化时目标为 nil,稍后变为非nil
  2. 目标设置为非nil< /code>,变为 nil
  3. Target 设置为非 nil,然后更改为另一个非 nil
  4. Target 为 nil< /code> (不在 init 上),变成稍后非 nil

...我们在情况#1 中立即遇到问题。如果 KVO 观察者只观察到 objectValue (因为它永远是你的代理),那么我们可能会没事,但是假设观察者观察到了一个通过你的代理/真实对象的 keyPath,比如说objectValue.status。这意味着 KVO 机制将在观察目标上调用 valueForKey: objectValue 并获取您的代理,然后它将在您的代理上调用 valueForKey: status 并会已恢复nil。当目标变为非nil时,KVO 将认为该值已从其下方更改(即不符合 KVO 要求),并且您将收到引用的错误消息。如果您有办法暂时强制目标针对 status 返回 nil,您可以打开该行为,调用 -[target willChangeValueForKey: status],关闭该行为,然后调用 -[target didChangeValueForKey: status]。无论如何,我们可以在案例 #1 处停止,因为它们有相同的陷阱:

  1. 如果您在其上调用 willChangeValueForKey:,则 nil 不会执行任何操作(即 KVO 机制将永远不知道在转换到 nil 或从 nil 转换期间更新其内部状态),
  2. 迫使任何目标对象拥有一种机制,使其暂时躺下并从 valueForKey 返回 nil:对于所有钥匙似乎相当繁重要求,当所表达的愿望是“透明代理”时。
  3. 在具有 nil 目标的代理上调用 setValue:forKey: 意味着什么?我们是否保留这些价值观?等待真正的目标?我们扔吗?巨大的开放问题。

对此方法的一种可能的修改是,当真实目标为 nil 时使用代理目标,可能是一个空的 NSMutableDictionary,并将 KVC/KVO 调用转发给代理。这将解决无法在 nil 上有意义地调用 willChangeValueForKey: 的问题。话虽如此,假设您已经维护了观察列表,我并不乐观地认为 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代码>存在的全部理由,对吗?它可以有 nil 内容,并将处理所有观察设置和拆除。它没有实现透明的调用转发代理的目标,但例如,如果目标是为某些异步生成的对象提供一个替代对象,那么让异步生成操作传递最终的对象可能会相当简单向控制器发出对象。这可能是最省力的方法,但并没有真正满足“透明”的要求。

2. 使用 NSObject 子类作为代理

NSProxy 的主要功能并不是它有什么魔力 - 主要功能是它没有(全部)NSObject 实现。如果您愿意努力覆盖您不想要的所有 NSObject 行为,并将它们分流回您的转发机制,那么您最终可能会与 NSProxy 提供的净值相同,但保留了 KVO 支持机制。从那里开始,您的代理将监视目标上观察到的所有相同关键路径,然后重新广播 willChange...didChange...来自目标的通知,以便外部观察者将其视为来自您的代理。

...现在来一些真正疯狂的事情:

3. (Ab)使用运行时将 NSObject KVC/KVO 行为引入到您的 NSProxy 子类中

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

这可能会导致猜测和检查过程,找出公共 KVO 方法调用的 NSObject 上的所有私有/内部方法,然后将它们添加到您批发的方法列表中。假设维护 KVO 遵守的内部数据结构不会在 NSObject 的 ivars 中维护(NSObject.h 表示没有 ivars - 但这并不意味着任何事情)似乎是合乎逻辑的这些天)因为这意味着每个 NSObject 实例都将支付空间价格。另外,我在 KVO 通知的堆栈跟踪中看到了很多 C 函数。我认为您可能已经为 NSProxy 引入了足够的功能,成为 KVO 的一流参与者。从那时起,这个解决方案看起来就像基于 NSObject 的解决方案;您观察目标并重新广播通知,就好像它们来自您一样,此外还围绕目标的任何更改伪造 willChange/didChange 通知。您甚至甚至可以在您的调用转发机制中自动执行其中的一些操作,方法是在输入任何 KVO 公共 API 调用时设置一个标志,然后尝试调用对您调用的所有方法,直到您调用为止。当公共 API 调用返回时清除该标志——其中的障碍是试图保证引入这些方法不会破坏代理的透明度。

我怀疑这会失败的地方在于 KVO 在运行时创建类的动态子类的机制。该机制的细节是不透明的,并且可能会导致另一长串找出从 NSObject 引入的私有/内部方法。最后,这种方法也是完全脆弱的,以免任何内部实现细节发生变化。

...总结 概括来说

,问题归结为这样一个事实:KVO 期望在其关键空间上有一个连贯的、可知的、持续更新的(通过通知)状态。 (如果您想支持 -setValue:forKey: 或可编辑绑定,请将“mutable”添加到该列表中。)除了肮脏的伎俩,成为一流参与者意味着成为 NSObjects。如果链中的这些步骤之一通过调用其他内部状态来实现其功能,这是它的特权,但它将负责履行 KVO 合规性的所有义务。

出于这个原因,我认为如果这些解决方案中的任何一个都值得付出努力,我会把钱花在“使用 NSObject 作为代理而不是 NSProxy 上。 ”因此,为了了解您的问题的确切性质,可能有一种方法可以创建一个符合 KVO 的 NSProxy 子类,但这似乎并不值得。

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.

牵强ㄟ2025-01-08 05:14:20

我没有完全相同的 OP 用例(没有绑定),但我的用例很相似:我正在创建一个 NSProxy 子类,它将自身呈现为实际从服务器加载的另一个对象。在加载期间,其他对象可以订阅代理,一旦对象到达,代理就会转发 KVO。

代理中有一个简单的 NSArray 属性,用于记录所有观察者。在加载真实对象之前,代理会在 valueForKey: 中返回 nil。当 realObject 到达时,代理在真实对象上调用 addObserver:forKeyPath:options:context:,然后通过运行时的魔力,遍历 addObserver:forKeyPath:options:context: 的所有属性。 code>realObject 并执行此操作:

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

这似乎有效,至少我还没有收到任何 KVO 合规性错误。但这确实有道理,首先所有属性都为零,然后它们从零变为实际值。这就像ipmcc在上面第一个声明中所说的那样,所以这篇文章只是一个确认!请注意,他提出的第二个代理实际上是不需要的,您只需要自己跟踪观察者即可。

I don't have the exact same use case (no bindings) of OP but mine was similar: I am creating an NSProxy subclass that presents itself as another object that is actually loaded from a server. During the load, other objects can subscribe to the proxy and the proxy will forward the KVO as soon as the object arrives.

There is a simple NSArray property in the proxy that records all observers. Until the real object is loaded, the proxy returns nil in valueForKey:. When the realObject arrives, the proxy calls addObserver:forKeyPath:options:context: on the real object and then, through the magic of the runtime, walks through all properties of realObject and does this:

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

This seems to work, at least I haven't gotten any KVO compliance errors yet. It does make sense though, first all properties are nil and then they change from nil to the actual value. It is all like ipmcc said in his first statement above, so this post is just a confirmation! Note that the second surrogate that he proposed actually isn't needed, you just have to keep track of observers yourself.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文