美文网首页源码解析iOS面试
GNUstep KVC/KVO探索(二):KVO的内部实现

GNUstep KVC/KVO探索(二):KVO的内部实现

作者: 哦呵呵y | 来源:发表于2018-10-19 15:27 被阅读160次

    GNUstep KVC/KVO探索(一):KVC的内部实现
    GNUstep KVC/KVO探索(二):KVO的内部实现

    概述

    KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

    KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而NSNotificationCenter是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

    KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

    基础使用

    使用KVO分为三个步骤:

    通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。

    在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。

    当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。

    KVO的实现

    在分析KVO的内部实现之前,先来分析一下KVO的存储结构,主要用到了以下几个类:
    GSKVOInfo
    GSKVOPathInfo
    GSKVOObservation

    GSKVOInfo

    KVO是基于NSObject类别实现的非正式协议,所以所有继承于NSObject的类都可以使用KVO,其中可以通过- (void*) observationInfo方法获取对象相关联的所有KVO信息,而返回值就是GSKVOInfo,源码如下

    @interface  GSKVOInfo : NSObject
    {
      NSObject          *instance;  // Not retained.
      GSLazyRecursiveLock           *iLock;
      NSMapTable            *paths;
    }
    
    1. 它保存了一个对象的实例,重点 Not retained 由于没有持有,也不是weak,所以当释放之后,在调用会崩溃,需要在对象销毁前,移除所有观察者
    2. paths 用于保存keyPathGSKVOPathInfo 的映射:
    GSKVOPathInfo
    @interface  GSKVOPathInfo : NSObject
    {
    @public
      unsigned              recursion;
      unsigned              allOptions;
      NSMutableArray        *observations;
      NSMutableDictionary   *change;
    }
    

    它保存了一个keypath对应的所有观察者
    observations 保存了所有的观察者(GSKVOObservation 类型)
    allOptions 保存了观察者的options集合
    change 保存了KVO触发要传递的内容

    GSKVOObservation
    @interface  GSKVOObservation : NSObject
    {
    @public
      NSObject      *observer;      // Not retained (zeroing weak pointer)
      void          *context;
      int           options;
    }
    @end
    

    它保存了单个观察的所有信息

    1. observer保存观察者 注意这里也是 Not retained
    2. context options 都是添加观察者时传入的参数
      image.png
      上面几个类都是用来存储KVO信息,而KVO是如何实现属性变化通知的呢?
      KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
      要实现isa-swizzling技术,主要通过以下几个类实现:
      GSKVOReplacement
      GSKVOBase
      GSKVOSetter
    GSKVOReplacement
    @interface  GSKVOReplacement : NSObject
    {
      Class         original;       /* The original class */
      Class         replacement;    /* The replacement class */
      NSMutableSet  *keys;          /* The observed setter keys */
    }
    - (id) initWithClass: (Class)aClass;
    - (void) overrideSetterFor: (NSString*)aKey;
    - (Class) replacement;
    @end
    
    // 创建
    - (id) initWithClass: (Class)aClass
    {
      NSValue       *template;
      NSString      *superName;
      NSString      *name;
    
      original = aClass;
    
      /*
       * Create subclass of the original, and override some methods
       * with implementations from our abstract base class.
       */
      superName = NSStringFromClass(original);      // original == Temp
      name = [@"GSKVO" stringByAppendingString: superName];    // name = GSKVOTemp
      template = GSObjCMakeClass(name, superName, nil);   // template = GSKVOTemp
      GSObjCAddClasses([NSArray arrayWithObject: template]);
      replacement = NSClassFromString(name);
      GSObjCAddClassBehavior(replacement, baseClass);
    
      /* Create the set of setter methods overridden.
       */
      keys = [NSMutableSet new];
    
      return self;
    }
    
    
    1. 这个类保存了被观察对象原始类信息 original
    2. 创建一个原始类的子类 命名为GSKVO<原类名>,iOS系统使用命名规则为NSKVONotifying_原类名
    3. 拷贝GSKVOBase类中的方法到新类中
    4. 后续会通过object_setClass, 将被观察对象的isa指向这个新类,也就是isa-swizzling技术,而isa保存的是类的信息,也就是说被观察者对象就变成了新类的实例,这个新类用于实现KVO通知机制
    GSKVOBase

    这个类默认提供了几个方法,都是对NSObject方法的重写,而从上面得知,这些方法都要拷贝到新创建的替换类中。也就是被观察者会拥有这几个方法的实现

    - (void) dealloc
    {
      // Turn off KVO for self ... then call the real dealloc implementation.
      [self setObservationInfo: nil];
      object_setClass(self, [self class]);
      [self dealloc];
      GSNOSUPERDEALLOC;
    }
    

    对象释放后,移除KVO数据,将对象重新指向原始类

    - (Class) class
    {
      return class_getSuperclass(object_getClass(self));
    }
    

    此方法用来隐藏替换类信息,应用层获取类的信息,仍然是原始类的信息. 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

    - (Class) superclass
    {
      return class_getSuperclass(class_getSuperclass(object_getClass(self)));
    }
    

    此方法和class方法原理相同

    - (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);
        }
    }
    

    这个方法是属于KVC中的,重写这个方法,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey],而这两个方法是触发KVO通知的关键。
    所以说KVO是基于KVC的,而KVC正是KVO触发的入口。

    GSKVOBase
    @interface  GSKVOSetter : NSObject
    - (void) setter: (void*)val;
    - (void) setterChar: (unsigned char)val;
    - (void) setterDouble: (double)val;
    - (void) setterFloat: (float)val;
    - (void) setterInt: (unsigned int)val;
    - (void) setterLong: (unsigned long)val;
    #ifdef  _C_LNG_LNG
    - (void) setterLongLong: (unsigned long long)val;
    #endif
    - (void) setterShort: (unsigned short)val;
    - (void) setterRange: (NSRange)val;
    - (void) setterPoint: (NSPoint)val;
    - (void) setterSize: (NSSize)val;
    - (void) setterRect: (NSRect)rect;
    @end
    

    这个类和上面重写KVC方法原理相同,将来会替换被观察者keypathsetter方法实现。会在原始setter方法前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey]

    小结

    那么到这里,就对KVO的实现有个大致的了解,通过isa-swizzling技术, 替换被观察的类信息,并且hook被观察keyPath setter方法,在原始方法调用前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey],从而达到监听属性变化的功能


    源码实现

    接下来从源码中查看KVO的所有流程

    - (void) addObserver: (NSObject*)anObserver
          forKeyPath: (NSString*)aPath
             options: (NSKeyValueObservingOptions)options
             context: (void*)aContext
    {
      GSKVOInfo             *info;
      GSKVOReplacement      *r;
      NSKeyValueObservationForwarder *forwarder;
      NSRange               dot;
    
      setup();
      [kvoLock lock];
    
      // Use the original class
      r = replacementForClass([self class]);
    
      /*
       * Get the existing observation information, creating it (and changing
       * the receiver to start key-value-observing by switching its class)
       * if necessary.
       */
      info = (GSKVOInfo*)[self observationInfo];
      if (info == nil)
        {
          info = [[GSKVOInfo alloc] initWithInstance: self];
          [self setObservationInfo: info];
          object_setClass(self, [r replacement]);
        }
    
      /*
       * Now add the observer.
       */
      dot = [aPath rangeOfString:@"."];
      if (dot.location != NSNotFound)
        {
          forwarder = [[NSKeyValueObservationForwarder alloc]
            initWithKeyPath: aPath
               ofObject: self
             withTarget: anObserver
            context: aContext];
          [info addObserver: anObserver
                 forKeyPath: aPath
                    options: options
                    context: forwarder];
        }
      else
        {
          [r overrideSetterFor: aPath];
          [info addObserver: anObserver
                 forKeyPath: aPath
                    options: options
                    context: aContext];
        }
    
      [kvoLock unlock];
    }
    

    KVO的入口,添加观察者方法,主要做了以下几件事

    1. replacementForClass,创建替换类,并且加入到全局classTable中,方便以后使用
    2. 获取对象的监听者数据GSKVOInfo,如果没有就创建新的
    3. 接下来分为两种情况
      1️⃣ 如果利用了点语法(self.keyPath.keyPath), 会利用NSKeyValueObservationForwarder递归创建子对象监听,子对象在将监听到的变化转发到上层,以后再具体分析
      2️⃣ 默认情况(keyPath)直接监听对象的某个属性,则会调用overrideSetterFor方法,hook属性的setter方法,将setter方法的实现替换为相应参数类型的GSKVOSetter中的方法实现
    4. 然后调用[info addObserver: anObserver forKeyPath: aPath options: options context: aContext];方法,将新的监听保存起来。

    GSKVOInfo 中的添加方法

    - (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];
    }
    

    此方法主要是保存观察者信息

    1. 查询相应的GSKVOPathInfo --GSKVOObservation如果有就更新,如果没有就创建新的并保存
    2. 如果options中包含NSKeyValueObservingOptionInitial,则立马调用[anObserver observeValueForKeyPath: aPath ofObject: instance change: pathInfo->change context: aContext];发送消息给观察者

    If the NSKeyValueObservingOptionInitial option is set, we must send an immediate notification containing the existing value in the NSKeyValueChangeNewKey

    1. 其中获取当前值通过KVC中的[instance valueForKeyPath: aPath];获取

    willChangeValueForKey didChangeValueForKey

    这两个方法分别添加在setterKVC 赋值前后,用来保存属性值的变化,以及发送消息给观察者

    1. willChangeValueForKey
      主要记录属性值的 oldValue保存到pathInfo->change中,如果options包含NSKeyValueObservingOptionPrior,则会遍历所有观察者,立马发送消息给观察者
      NSKeyValueObservingOptionPrior 表示属性值修改前后都会收到通知
    2. didChangeValueForKey
      根据options保存属性的新旧值,遍历所有的观察者,发送消息
    移除观察者
    /*
     * removes the observer
     */
    - (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
    {
      GSKVOPathInfo *pathInfo;
    
      [iLock lock];
      pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
      if (pathInfo != nil)
        {
          unsigned  count = [pathInfo->observations count];
    
          pathInfo->allOptions = 0;
          while (count-- > 0)
            {
              GSKVOObservation      *o;
    
              o = [pathInfo->observations objectAtIndex: count];
              if (o->observer == anObserver || o->observer == nil)
                {
                  [pathInfo->observations removeObjectAtIndex: count];
                  if ([pathInfo->observations count] == 0)
                    {
                      NSMapRemove(paths, (void*)aPath);
                    }
                }
              else
                {
                  pathInfo->allOptions |= o->options;
                }
        }
        }
      [iLock unlock];
    }
    

    此方法主要用来移除相应keyPath的观察者,方法实现很简单,根据参数传入的anObserveraPath在前面介绍的数据结构中查询并移除

    总结

    到这里KVO的大致流程就分析完了

    1. 主要用了isa-swizzling,修改了观察者的类信息,并且hooksetter方法,当setter方法调用时发送消息给所有观察者
    2. 由上面源码可以看出对观察者、被观察者的引用都是Not Retain, 所以对象释放前一定要移除观察者。
    3. 消息的发送主要由[self willChangeValueForKey: key], [self didChangeValueForKey: key]触发,并且必须成对出现,automaticallyNotifiesObserversForKey方法用来控制,是否要主要添加上述的两个方法,默认返回值为YES,如果返回NO则不会自动添加,也就是说setter的调用以及KVC修改都不会触发通知
    4. + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
      此方法用来设置依赖关系,有时候需要某属性值随着同一对象的其他属性的改变而改变。可以通过事先将这样的依赖关系在类中注册,那么即便属性值间接地发生了改变,也会发送通知消息,被观察者类重写返回和key依赖的所有key集合
      内部实现也比较简单,将所有依赖关系存储在全局的dependentKeyTable中,然后hook了所有依赖的keysetter方法,当[self willChangeValueForKey: key], [self didChangeValueForKey: key]调用时会查找所有的依赖关系,然后发送消息
    5. KVO内部多次用到了KVC
      1️⃣ 重写 setValue:forKey
      2️⃣ 使用valueForKey --- valueForKeyPath获取属性的值,尤其是在使用点语法的时候,只有valueForKeyPath可以获得深层次的属性值。
      所以KVO是基于KVC而实现的。
    NSKeyValueObservationForwarder
    @interface NSKeyValueObservationForwarder : NSObject
    {
      id                                    target;
      NSKeyValueObservationForwarder        *child;
      void                                  *contextToForward;
      id                                    observedObjectForUpdate;
      NSString                              *keyForUpdate;
      id                                    observedObjectForForwarding;
      NSString                              *keyForForwarding;
      NSString                              *keyPathToForward;
    }
    

    最后,当使用key1.key2.key3作为keypath时,会利用NSKeyValueObservationForwarder来转发,会生成如下关系:

    被观察者 观察者 keyPath
    self forwarder1 key1
    key1 forwarder2 key2
    key2 forwarder3 key3

    其中观察者会依次相互引用

    - (void) observeValueForKeyPath: (NSString *)keyPath
                           ofObject: (id)anObject
                             change: (NSDictionary *)change
                            context: (void *)context
    {
      if (anObject == observedObjectForUpdate)
        {
          [self keyPathChanged: nil];
        }
      else
        {
          [target observeValueForKeyPath: keyPathToForward
                                ofObject: observedObjectForUpdate
                                  change: change
                                 context: contextToForward];
        }
    }
    

    在收到通知时会进行转发,所以当key3进行变化时,forwarder3 ---> forwarder2 ---> forwarder1 ---> 应用层注册的target

    相关文章

      网友评论

        本文标题:GNUstep KVC/KVO探索(二):KVO的内部实现

        本文链接:https://www.haomeiwen.com/subject/xvjszftx.html