系统底层源码分析(1)——KVO

在日常开发中经常会用到KVO,而RxSwift框架也有KVO,在了解RxSwift框架的KVO之前,我们先来了解一下系统KVO的底层原理。

(在这之前说个题外话,以前面试的时候曾经被问到:“KVO的底层原理是什么?”,当时我说:“不知道,我猜可能在setter方法的底层源码里加了个回调。”,当时觉得自己好菜🤦♂️,虽然说了总比不说好,可是说了个好傻的答案。)

  • 但是大家都知道,苹果的源码看不了,那怎么办呢?还有一个靠谱的方法就是看GNUstep,因为它的源码和苹果的很像。

早在 1985 年, Steve Jobs 离开苹果电脑 (Apple) 后成立了 NeXT 公司, 并于 1988 年推出了 NeXT 电脑, 使用 NeXTStep 为作业系统. 在当时, NeXTStep 是相当先进的系统. 以 Unix (BSD) 为基础, 使用 PostScript 提供高品质的使用者图形介面, 并以 Objective-C 语言提供完整的物件导向环境.

尽管 NeXT 在软体上的优异, 其硬体销售成绩不佳, 不久之后, NeXT 便转型为软体公司. 1994 年, NeXT 与升阳 (Sun Microsystem) 合作推出 OpenStep 界面, 目标为跨平台的物件导向程式开发环境. NeXT 接着推出实作 OpenStep 介面的 OPENSTEP 系统, 可在 Mach, Microsoft Windows NT, Sun Solaris 及 HP/UX 上执行. 1996 年, 苹果电脑买下 NeXT, 做为苹果电脑下一代作业系统的基础, OPENSTEP 系统便演进成为 MacOS X 的 Cocoa 环境.

在 1995 年, 自由软体基金会 (Free Software Fundation) 开始了 GNUstep 计划, 目的在实作 OpenStep 介面, 以提供 Linux/BSD 系统一个完整的程式发展环境. 但由于 OpenStep 界面过于庞大, 开发人力不足, 及许多技术在当时尚未成熟 (如 Display PostScript), 所以直到目前为止, GNUstep 才算是一个完整的程式开发环境.

我们从基本的开始,写OC的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"KVO OC");
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}
  • (一)添加观察者
  1. 我们直接从添加观察者开始,在GNUstep源码全局搜索并找到相应方法:
@implementation NSObject (NSKeyValueObserverRegistration)

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  GSKVOInfo             *info;
  GSKVOReplacement      *r;
  NSKeyValueObservationForwarder *forwarder;
  NSRange               dot;

  setup();//获取 baseClass: GSKVOBase
  [kvoLock lock];

  r = replacementForClass([self class]);//创建子类并注册

  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)//判断是否交换过
    {
      info = [[GSKVOInfo alloc] initWithInstance: self];//创建GSKVOInfo
      [self setObservationInfo: info];//设置GSKVOInfo
      object_setClass(self, [r replacement]);//切换isa
    }

  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];//替换原来的setter方法
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
    }

  [kvoLock unlock];
}
  1. 首先调用setup();,这里面会获取到一个GSKVOBase类:
static inline void
setup()
{
  if (nil == kvoLock)
    {
      [gnustep_global_lock lock];
      if (nil == kvoLock)
    {
      ...
      baseClass = NSClassFromString(@"GSKVOBase");
    }
      [gnustep_global_lock unlock];
    }
}
  1. 然后通过replacementForClass([self class]);创建子类并注册:
static GSKVOReplacement *
replacementForClass(Class c)
{
   ...
  r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);//从列表获取
  if (r == nil)//没有就创建
    {
      r = [[GSKVOReplacement alloc] initWithClass: c];
      NSMapInsert(classTable, (void*)c, (void*)r);
    }
  [kvoLock unlock];
  return r;
}
@implementation GSKVOReplacement

- (id) initWithClass: (Class)aClass
{
  ...
  original = aClass;

  superName = NSStringFromClass(original);
  name = [@"GSKVO" stringByAppendingString: superName];
  template = GSObjCMakeClass(name, superName, nil); //创建子类
  GSObjCAddClasses([NSArray arrayWithObject: template]);//注册
  replacement = NSClassFromString(name);
  GSObjCAddClassBehavior(replacement, baseClass);//添加GSKVObase的重写方法

  keys = [NSMutableSet new];

  return self;
}
  1. 这里先是调用GSObjCMakeClass(name, superName, nil);创建子类:
NSValue *
GSObjCMakeClass(NSString *name, NSString *superName, NSDictionary *iVars)
{
  ...
  classSuperClass = NSClassFromString(superName);
  ...
  classNameCString = [name UTF8String];
  //创建新类
  newClass = objc_allocateClassPair(classSuperClass, classNameCString, 0);
  if ([iVars count] > 0)
    {
      ...
      //添加变量
      if (NO
        == class_addIvar(newClass, iVarName, iVarSize, iVarAlign, iVarType))
        { ... }
    }
    }

  return [NSValue valueWithPointer: newClass];
}
  1. 然后通过GSObjCAddClasses([NSArray arrayWithObject: template]);进行注册:
void
GSObjCAddClasses(NSArray *classes)
{
  NSUInteger    numClasses = [classes count];
  NSUInteger    i;

  for (i = 0; i < numClasses; i++)
    {
      objc_registerClassPair((Class)[[classes objectAtIndex: i] pointerValue]);//注册
    }
}
  1. 接下来回到第3点,下一步就是通过GSObjCAddClassBehavior(replacement, baseClass);添加GSKVObase的方法:
void
GSObjCAddClassBehavior(Class receiver, Class behavior)
{
  ...
  methods = class_copyMethodList(behavior, &count);//GSKVOBase方法列表
  
  if (methods == NULL) 
  { ... }
  else
    {
      GSObjCAddMethods (receiver, methods, NO);//添加方法
      free(methods);
    }
    ...
}
void
GSObjCAddMethods(Class cls, Method *list, BOOL replace)
{
  ...
  while ((m = list[index++]) != NULL)
    {
      SEL       n = method_getName(m);
      IMP       i = method_getImplementation(m);
      const char    *t = method_getTypeEncoding(m);
      //添加方法
      if (YES == class_addMethod(cls, n, i, t))
    { ... }
      else if (YES == replace)
    { ... } 
      else
    { ... }
    }
}

这里可以先看一下GSKVObase的方法:

@implementation GSKVOBase

- (void) dealloc { ... }
- (Class) class { ... }
- (void) setValue: (id)anObject forKey: (NSString*)aKey { ... }
- (void) takeStoredValue: (id)anObject forKey: (NSString*)aKey { ... }
- (void) takeValue: (id)anObject forKey: (NSString*)aKey { ... }
- (void) takeValue: (id)anObject forKeyPath: (NSString*)aKey { ... }
- (Class) superclass { ... }

@end
    1. 子类的准备完成后回到第1点,通过object_setClass(self, [r replacement]);切换isa指向子类。
    1. 接着会创建GSKVOInfoGSKVOInfo会保存很多信息,所以后面调用了[info addObserver: anObserver forKeyPath: aPath options: options context: forwarder];,由原观察者进行迁移到GSKVOInfo观察者迁移),然后在里面做各种处理(context,options等)。
  1. 在后面还有一步,通过[r overrideSetterFor: aPath];替换原来的settter方法(会根据对应类型替换成GSKVOSettersetter):
@implementation GSKVOReplacement

- (void) overrideSetterFor: (NSString*)aKey
{
  if ([keys member: aKey] == nil)
    {
      ...
      suffix = [aKey substringFromIndex: 1];//从第2个字母开始截取
      u = uni_toupper([aKey characterAtIndex: 0]);//取首字母->大写
      tmp = [[NSString alloc] initWithCharacters: &u length: 1];//char->String
      a[0] = [NSString stringWithFormat: @"set%@%@:", tmp, suffix]//拼接成相应setter方法
      a[1] = [NSString stringWithFormat: @"_set%@%@:", tmp, suffix];
      [tmp release];
      for (i = 0; i < 2; i++)
        {
          /*
           * Replace original setter with our own version which does KVO
           * notifications.
           */
          sel = NSSelectorFromString(a[i]);//方法选择器
         ...
          sig = [original instanceMethodSignatureForSelector: sel];//监听对象的方法
          ...
          type = [sig getArgumentTypeAtIndex: 2];
          switch (*type)
            {
              case _C_CHR:
              case _C_UCHR:
                imp = [[GSKVOSetter class]
                  instanceMethodForSelector: @selector(setterChar:)];//其他类型操作一样
                break;
              case _C_SHT:
              case _C_USHT: ...
              case _C_INT:
              case _C_UINT: ...
              case _C_LNG:
              case _C_ULNG: ...
              case _C_FLT: ...
              case _C_DBL: ...
              case _C_BOOL: ...
              case _C_ID:
              case _C_CLASS:
              case _C_PTR:
                imp = [[GSKVOSetter class]
                  instanceMethodForSelector: @selector(setter:)];
                break;
              case _C_STRUCT_B: ...
              default: ...
            }

          if (imp != 0)
            {
          //添加方法;属性setter方法选择器关联上面的imp指针
          if (class_addMethod(replacement, sel, imp, [sig methodType]))
        { ... }
          else
        { ... }
            }
        }
      ...
}
  • 属性setter方法选择器关联上面的imp指针,执行时对应类型属性调用上面的方法。
  • (二)监听响应
  1. 一切准备好后,当值改变时,将会走setter方法,但是由于之前切换了isa,所以会来到这里:
@implementation GSKVOSetter

- (void) setterChar: (unsigned char)val
{
  NSString  *key;
  Class     c = [self class];
  void      (*imp)(id,SEL,unsigned char);
 //_cmd是SEL
  imp = (void (*)(id,SEL,unsigned char))[c instanceMethodForSelector: _cmd];//获取监听对象属性setter的原imp指针

  key = newKey(_cmd);
  if ([c automaticallyNotifiesObserversForKey: key] == YES)
    {
      // pre setting code here
      [self willChangeValueForKey: key];
      (*imp)(self, _cmd, val);//执行原setter方法
      // post setting code here
      [self didChangeValueForKey: key];
    }
  else
    {
      (*imp)(self, _cmd, val);
    }
  RELEASE(key);
}

- (void) setterDouble: (double)val{ ... }
...//还有其他类型

或者用setValue:forKey:/setValue:forKeyPath:赋值时:

@implementation GSKVOBase

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
  ...
  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];//原来多了这个
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}
  1. 它们都会调用willChangeValueForKey
@implementation NSObject (NSKeyValueObserverNotification)

- (void) willChangeValueForKey: (NSString*)aKey
{
  ...
  if (pathInfo != nil)
    {
      if (pathInfo->recursion++ == 0)
        {
          ...
          if (old != nil)
            { ... }
          else if (pathInfo->allOptions & NSKeyValueObservingOptionOld)
            { ... }
          ...
          //通知响应
          [pathInfo notifyForKey: aKey ofInstance: [info instance] prior: YES];
        }
      [info unlock];
    }

  [self willChangeValueForDependentsOfKey: aKey];
}
@implementation GSKVOPathInfo

- (void) notifyForKey: (NSString *)aKey ofInstance: (id)instance prior: (BOOL)f
{
  ...
while (count-- > 0)
    {
      GSKVOObservation  *o = [observations objectAtIndex: count];

      if (f == YES)
        { ... }
      else
        {
          if (o->options & NSKeyValueObservingOptionNew)
            {
              [change setObject: newValue
                         forKey: NSKeyValueChangeNewKey];//保存新值
            }
        }

      if (o->options & NSKeyValueObservingOptionOld)
        {
          [change setObject: oldValue
                     forKey: NSKeyValueChangeOldKey];//保存旧值
        }

      [o->observer observeValueForKeyPath: aKey
                                 ofObject: instance
                                   change: change
                                  context: o->context];//回调
    }
  ...
}

保存新旧值后,通过[o->observer observeValueForKeyPath: aKey ofObject: instance change: change context: o->context];进行回调出去。

原来KVO就是在setter方法的底层源码里加了回调。

(突然好开心,没想到当年说的好傻的答案起码方向是对的)

  • (三)移除观察者
  1. 移除观察者就是反着来:
@implementation NSObject (NSKeyValueObserverRegistration)

- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
  ...
  [info removeObserver: anObserver forKeyPath: aPath];//
  if ([info isUnobserved] == YES)
    {
      object_setClass(self, [self class]);//把isa换回来
      IF_NO_GC(AUTORELEASE(info);)
      [self setObservationInfo: nil];//
    }
  ...
}
  • 总结

KVO的原理就是,通过在底层创建新的子类,切换监听对象的isa指向子类,交换属性setter时执行的imp指针,属性赋值时,执行新的set系列方法,然后会调用willChangeValueForKey走向监听回调。

  • 补充

其实Swift的源码有一部分开源了,所以我们也可以看看相关的:

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

推荐阅读更多精彩内容