Re: KVO and the observeValueForKeyPath bottleneck
Re: KVO and the observeValueForKeyPath bottleneck
- Subject: Re: KVO and the observeValueForKeyPath bottleneck
- From: Chris Kane <email@hidden>
- Date: Mon, 17 Jul 2006 18:00:43 -0700
On Jul 17, 2006, at 2:16 PM, Jakob Olesen wrote:
On 17/07/2006, at 18.09, Chris Kane wrote:
I'm just curious ... with the trivial approach above, and the other
ones involving the context being a keypath and so on ... has
anyone considered what will happen if a superclass or subclass (of
whatever class that is) is also doing KVO (on either same or
different keys) of the same objects?
Well, not until you brought it up.
If you are subclassing a foreign class without documented behavior,
the context pointer is useless. Basically you don't know if the
superclass is already observing an object, so the context pointer
could be mine or the superclass', who knows?
Further, if you are expecting to be subclassed, you must document
exactly which objects and key paths you are observing, otherwise the
subclass can accidentally overwrite your context pointer. (What
happens if you call addObserver twice on the same object/key path?)
Quick test: you get two callbacks, calling removeObserver removes
them one at a time, last in first out. (linked list?)
Calling addObserver twice with identical arguments (incl. context)
also gives you two callbacks.
So, if you don't trust your subclasses and superclasses, you are
given a void pointer that you may or may not have created yourself.
That is very close to useless.
This is what I would do, if I was writing class intended for
subclassing: Use
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
[self performSelector:(SEL)context withObject:[NSNotification
notificationWithName:keyPath object:object userInfo:change]];
}
and tell subclasses that they can depend on it being so. Tell
subclasses to not call super with a context pointer that is not a
valid selector. Know that one change may cause multiple callbacks.
When writing a subclass: Ignore context completely, dispatch on
keyPath and/or object
You and Matt Neuburg and Jim Correia (privately) have all now
identified all the parts of the problem and the solution. But I don't
like the "sub/superclasses can't use whatever context they like" that
results from your "document the behavior and requirements" approach.
So I'll present a slightly different solution.
When you and a superclass both register for KVO notifications, the
registrations are necessarily distinct, and you will potentially
receive multiple calls to observeValueForKeyPath:... for the same
changes, some due to your superclass, some due to your registrations.
The ones for you you need to process and not pass up to super. The
ones not for you you need to pass up to super and not process (that
is, not use or expect anything about the context). You cannot look at
the keypath, recognize it, and say "this is for me" and consume it,
because the keypath may be interesting to the superclass too, but you
can't pass all of those along either, because the superclass may not
be interested (and is certainly not interested in your context if this
invocation happens to contain your context). Similarly keypath+object
is not reliable to distinguish "mine" from "somebody else's".
The solution is that the context pointer must be used to provide a
globally unique value that you can recognize in your class. If you
recognize the value, this notification is for you, otherwise you pass
the method call up to super and return. You have to use a value that
the other classes in the hierarchy won't (or can't) use.
You cannot use NULL as the context pointer, because the superclass (or
a subclass) might also use NULL. NULL is a shared value. You can't
use selectors, either, because their values are global to the process,
and a sub or superclass could potentially use the same one. Plus,
recognizing many possible selector values would be a pain and time
consuming. It's better to pick one context value.
Matt Neuburg said:
So ClassB needs a way to distinguish the notifications that belong
to it (so
that it can pass along those that don't). The only way I can think
of to do
this is to include an unmistakeable token in the context. But what
would
this be - a hard-coded string? Yuck. m.
You can't include something *inside* the context, because you can't
safely look through the pointer at anything in particular, as the
pointer might not be pointing to the data structure you know about.
You have to generate a unique pointer value and act based on that. An
== comparison is dirt cheap if you can do it.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj
change:(NSDictionary *)change context:(void *)ctx {
if (ThisClassUniqueObservationContext != ctx) {
[super observeValueForKeyPath:keyPath ofObject:obj
change:change context:ctx];
return;
}
if ([keyPath isEqual:@"isBrillig"]) {
... and so on ...
}
... and so on ...
}
If you assume that @"" string constants are NOT globally uniqued in a
binary, but only to a given compilation unit, then
ThisClassUniqueObservationContext could be a constant string:
static NSString * ThisClassUniqueObservationContext = @"MyContext";
That's not necessarily a safe assumption, however. The following is
certainly bad, since CStrings are uniqued and shared within a binary:
static const char * ThisClassUniqueObservationContext = "MyContext";
What *I* have done when I needed to deal with this is create some
useless but unique data and use its address:
static char __MyContext = 0;
static char *ThisClassUniqueObservationContext = &__MyContext;
Note that if this were "const", that could thwart things, since
integer constants might also be uniqued and shared by the linker.
What could still happen is that the compiler could notice that this
data is static (local to this compilation unit) and never stored to
anywhere in the compilation unit and nothing ever writes through the
address to it (or ThisClassUniqueObservationContext) or passes the
address to a function or whatever, and decide in an optimizing kind of
way to make the data const for me, and possibly subject it to linker
uniquing. But that possibility doesn't keep me up at night.
But whatever way you dice it, "yuck". Usually I've not wanted the
context pointer for anything and just wanted to use NULL ... but for
this potential problem.
Alternative solution: Create your own, private helper object and
register that instead.
[...]
The KVO API expects observers to have a single personality, so that
is probably the way to go if you are in the middle of a class
hierarchy.
I've done that solution too. There you've made the observer different
rather than the context different. I've also used that to work around
"can't register self for notifications about self" issue, which should
someday be fixed. Once an object can register itself to receive
notifications about itself, this class/superclass/subclass collision
issue will probably become more common in practice than it is now.
Chris Kane
Cocoa Frameworks, Apple
_______________________________________________
Do not post admin requests to the list. They will be ignored.
Cocoa-dev mailing list (email@hidden)
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden