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 的核心存在于 中NSObject,而NSProxy不是继承自NSObject. 我有理由相信,任何方法都将要求NSProxy对象保留自己的观察列表(即外部人员希望观察到的列表)。仅此一项就会为您的 NSProxy 实现增加相当大的权重。

观察目标

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

NSProxy 似乎可以很好地作为那些尚不存在的对象的替代对象

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

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

  1. 目标处于初始化状态,之后nil变为非nil
  2. 目标设置为非,稍后nil变为nil
  3. 目标设置为非nil,然后更改为另一个非nil
  4. 目标是nil(不在初始化时),变为非nil以后

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

  1. nil如果你调用它,它不会做任何事情willChangeValueForKey:(即 KVO 机器永远不会知道在转换到或从 时更新其内部状态nil
  2. 强制任何目标对象有一个机制,它会暂时撒谎并nil从 valueForKey 中返回:对于所有键来说似乎是一个非常繁重的要求,当声明的愿望是“透明代理”时。
  3. 在具有nil目标的代理上调用 setValue:forKey: 甚至意味着什么?我们是否保留这些价值观?等待真正的目标?我们扔吗?巨大的开放问题。

对这种方法的一种可能的修改是在真正的目标是 时使用代理目标nil,也许是一个空的NSMutableDictionary,并将 KVC/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内容,并将处理所有观察设置和拆卸。它没有实现透明的调用转发代理的目标,但是例如,如果目标是为某个异步生成的对象提供一个替代,那么让异步生成操作传递最终结果可能相当简单反对控制器。这可能是最省力的方法,但并没有真正解决“透明”的要求。

NSObject2.为您的代理使用子类

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

...现在是一些非常疯狂的事情:

3. (Ab)使用运行时将NSObjectKVC/KVO 行为引入你的NSProxy子类

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

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

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

...综上所述

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

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

于 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在他上面的第一个声明中所说的那样,所以这个帖子只是一个确认!请注意,实际上不需要他提出的第二个代理,您只需要自己跟踪观察者即可。

于 2014-02-13T18:48:41.340 回答