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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容