Key Value Observing

From GNUstepWiki
Revision as of 14:50, 23 October 2006 by Cbv (talk | contribs) (add category)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Stefan Urbanek wrote:

Is anyone working on the Key-value observing?

I've just been updating KVC and thinking about KVO

http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueObserving/index.html

In the documentation one can read:

"Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. "

Is it difficult to implement it in GNUstep?

No more than in MacOSX.

If not, what is needed?

If noone is working on it and if someone skilled knows how to implement it or has some hints, please write notes for others here:

OK, see below ...

1. You need to write a class to manage observing of objects (call it GSKVN for instance). This class neeeds to have instance variables to hold -

a. a class pointer

b. information about all observers of the object and what sort of notifications they want to get.

@interface GSObservedProxy
{
    id       target;
    Class    targetClass;
    NSArray *observerInfo;
}
@end
@interface GSObserverInfo
{
    id       observer;
    NSArray *keys;     /* Observed keys */
}
@end


2. You need to create a lock protected NSMapTable which will contain pointers to observed objects as keys, and instances of the KVN class as values.

When you start observing an object, you create an instance of the GSKVN class using NSAllocateObject(), and store it in the map table.

Within the GSKVN instance, you store the class of the object being observed. You change the isa variable of the object being observed to be a pointer to the GSKVN class.

Pseudo-code:

@implementation GSObservedProxy
+ (GSObservedProxy *)proxyForObject:(id)anObject
{
    GSObservedProxy *proxy;

    /* get a proxy if exists. FIXME: this should be thread safe */
    proxy = [observedProxies objectForKey:anObject];

    if(!proxy)
    {
        proxy = [[GSObservedProxy alloc] initWithTarget:anObject];
        anObject->isa = [GSObservedProxy class];

        [observedProxies addObject:proxy forKey:anObject];
    }
}
- initWithTarget:(id)anObject
{
    target = anObject;
    targetClass = [anObject class];
    return self;
}
- addObserver:(id)anObserver
   forKeyPath:(NSString *)keyPath 
      options:(unsigned int)options
      context:(void *)context
{
    /* FIXME: add implementation here */
}
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath
{
     remove observer from observers
     if([observers count] == 0)
     {
         /* last observer was removed, we should put the target to its original state */
         target->isa = originalClass;
         [observedProxies removeObject:self];

         /* FIXME: somehow release and dealloc self */
     }
}
@end
@implementation NSObject(GSKeyValueObserving)
- addObserver:(id)anObserver
   forKeyPath:(NSString *)keyPath 
      options:(unsigned int)options
      context:(void *)context
{
    GSObservedProxy *proxy;

    /* get proxy for self */
    proxy = [GSObservedProxy proxyForObject:self];
    [proxy addObserver:anObserver forKeyPath:keyPath options:options context:context];
}
@end

Now, when a message is sent to the original object, it actually gets sent to the GSKVN class. This class doesn't implement the method, but it does implement things like -respondsToSelector:, -methodForSelector: -methodSignatureForSelector: and forwardInvocation:

So, the implementations of these methods in the GSKVN class will -

a. use the map table to look up the GSKVN instance corresponding to the object being observed.

b. use the class pointer stored in the GSKVN instance to look up methods and dispatch messages, intercepting those of interest and sending out notifications.

There are obviously a few other special cases - If the observed object receives a -dealloc message, the GSKVN instance must also remove itsself from the map table. If the observed object receives a -class message, the GSKVN instance must return the original class pointer. etc.

When there are no longer any observers watching an object, the GSKVN instance for the object is removed from the map table.

When the GSKVN object is removed from the map table, it must be deallocated using NSDeallocateObject()

Finally, the big special case ... When an observed object receives a -respondsToSelector: or -methodForSelector: etc message for a selector corresponding to an accessor method which is not implemented by the class of the observed object, the GSKVN class needs to have -respondesToSelector: return YES if there is an instance variable corresponding to it, and -methodForSelector: must return a method which is able to access the instance variables accordingly (looking at the _cmd argument to see which instance method to acccess). This is messy, but fairly strightforward.

One thing worth noting ... information for this case should be assembled when the object is first observed ... by examining all the ivars of the object and seeing whether corresponding accessor methods exist. There are two reasons for this -

1. performance ... you don't want to do that sort of thing often.

2. so that the appropriate selectors for the fake accessor methods can be registered with the runtime otherwise the KVC system would be unable to attempt to access the ivars via accessor methods, and might attempt to access them directly ... which would mean that the notification systtem would be unaware of changes.

Hope that this helps.