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
- 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, 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:
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 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:
- 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 #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.
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.
来源:https://stackoverflow.com/questions/9054970/nsproxy-and-key-value-observing