通过 gnustep objc 开源实现探究 KeyValueObserving 背后的实现

探究 KeyValueObserving 背后的实现

KVO PG

Key-Value Observing Implementation Details

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.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

官方文档明确说明,开发者不要通过isa 指针来判断对象类型,而要通过 class 方法来判断,原因是 KVO 的底层实现中,isa 会指向中间类来完成 KVO 的调用逻辑。为验证该说法,我们建立一个 Account 类,并观察 Account 对象的 isa 指针类型。

Account account = [Account new];

gdb:
print account->isa
输出为: Account 类,isa 和 class 是一致的,看来 isa 还是指向了我们认为的类

//我们创建对象并观察之
Account account = [Account new];
[self addObserver:self forKeyPath:keyPath options:
     NSKeyValueObservingOptionNew |
     NSKeyValueObservingOptionOld
             context:nil];

gdb:
print account->isa
输出:NSKVONotifying_Account

我们看到此时的 isa 指针指向的是一个名为 NSKVONotifying_Account 的类,说明该类是为 Account 扩展 KVO 机制的中间类

gnustep 源码

gnustep 是 NeXTStep 的一个开源实现版本,虽然 NeXTStep 的源码并未公开,但是通过 gnustep 我们也能一窥究竟,虽然具体实现不相同,但是能够明白运作机制,也可以开阔视野。

通过 NSKeyValueObserving.m 文件,能找到类 GSKVOBase,这就是实现 KVO notify 通知的具体类了。


- (Class) class { return class_getSuperclass(object_getClass(self)); } - (void) setValue: (id)anObject forKey: (NSString*)aKey { Class c = [self class]; void (*imp)(id,SEL,id,id); imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd]; if ([[self class] automaticallyNotifiesObserversForKey: aKey]) { [self willChangeValueForKey: aKey]; imp(self,_cmd,anObject,aKey); [self didChangeValueForKey: aKey]; } else { imp(self,_cmd,anObject,aKey); } }

我们可以看到 setValue:forKey: 的实现中,直接通知了被观察对象。也就是说,被观察的对象,会被 isa-swizzling,isa 指向了

到这里已经真相大白,runtime 在调用 - (void) addObserver:(NSObject*)anObserver forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext 时通过 runtime 进行了 isa swizzling(object_setClass 改变 isa 类指引的类模板)。
另外,在 removeObserver: 的时候,runtime 也会恢复 isa 的类指向。

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  GSKVOPathInfo         *pathInfo;
  GSKVOObservation      *observation;
  unsigned              count;

  if ([anObserver respondsToSelector:
    @selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
    {
      return;
    }
  [iLock lock];
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
  if (pathInfo == nil)
    {
      pathInfo = [GSKVOPathInfo new];
      // use immutable object for map key
      aPath = [aPath copy];
      NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
      [pathInfo release];
      [aPath release];
    }

  observation = nil;
  pathInfo->allOptions = 0;
  count = [pathInfo->observations count];
  while (count-- > 0)
    {
      GSKVOObservation      *o;

      o = [pathInfo->observations objectAtIndex: count];
      if (o->observer == anObserver)
        {
          o->context = aContext;
          o->options = options;
          observation = o;
        }
      pathInfo->allOptions |= o->options;
    }
  if (observation == nil)
    {
      observation = [GSKVOObservation new];
      GSAssignZeroingWeakPointer((void**)&observation->observer,
    (void*)anObserver);
      observation->context = aContext;
      observation->options = options;
      [pathInfo->observations addObject: observation];
      [observation release];
      pathInfo->allOptions |= options;
    }

  if (options & NSKeyValueObservingOptionInitial)
    {
      /* If the NSKeyValueObservingOptionInitial option is set,
       * we must send an immediate notification containing the
       * existing value in the NSKeyValueChangeNewKey
       */
      [pathInfo->change setObject: [NSNumber numberWithInt: 1]
                           forKey:  NSKeyValueChangeKindKey];
      if (options & NSKeyValueObservingOptionNew)
        {
          id    value;

          value = [instance valueForKeyPath: aPath];
          if (value == nil)
            {
              value = null;
            }
          [pathInfo->change setObject: value
                               forKey: NSKeyValueChangeNewKey];
        }
      [anObserver observeValueForKeyPath: aPath
                                ofObject: instance
                                  change: pathInfo->change
                                 context: aContext];
    }
  [iLock unlock];
}