KVO进阶——KVO实现探究

本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。

一、使用上的疑问

1.keyPath是什么

当我们使用@property时候,keyPath是指的是我们的属性名,实例变量或者是存取方法?
👇 对一个属性值使用@synthesize重新定义了存储变量

#import "Person.h"

@interface Student : Person 
@property (nonatomic, strong) NSString* mark;
@end

@implementation Student
@synthesize mark = abc;

- (void)setMark:(NSString *)newMark {
    abc = newMark;
}
- (NSString *)mark {
    
    return abc;
}

main() {
    Student *stu = [[Student alloc] init];
    stu.mark = @"65";
    StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu];
    [stuObserver addObserverForKeyPath:@"mark"];   // 重命名get方法
    stu.mark = @"85";
}

实际结果是,能够监听到mark值的变化,反之,我将mark替换成真正的实例变量abc时,无法获取状态。
现在想想其实答案早就存在了,我们不做显示的@synthesize的指定时,其实等价于@synthesize mark = _mark;,由此看来keyPath实际指的并不是真正存储你数据的变量。

2.KVO是否能够继承

我是否能够监听我父类里的属性,哪怕他并没有暴露出来?通过某些手段得(猜)到了keyPath,然后去监听它甚至是KVC修改他的值。

子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?
子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO能否观察到?
子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?

// Person类
@interface Person : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong, readonly) NSString *fullName;
- (void)setNewInnerName:(NSString *)str;
@end

@interface Person ()
@property (nonatomic,strong) NSString *innerName;
@end
@implementation Person

- (void)setNewInnerName:(NSString *)str {
      self.innerName = str;// 通过get、set访问  触发KVO
//    [self setValue:str forKey:@"innerName"];// KVC方式,其实调用的也是setter方法 触发KVO
//    _innerName = str;// 直接访问成员变量,不触发KVO
}

// Student类
@interface Student : Person
@end

@implementation Student

- (void)setFirstName:(NSString *)firstName {
    NSLog(@"重写的setFirstName方法");
}
@end


// 执行文件
main() {
    Person *p = [[Person alloc] init];
    p.firstName = @"zhao";
    p.lastName = @"zhiyu";

    PersonKvoObserver *personKvoObserver = [[PersonKvoObserver alloc] initWithPerson:p];
    [personKvoObserver addObserverForKeyPath:@"fullName"];  // 属性关联
    [personKvoObserver addObserverForKeyPath:@"innerName"]; // 内部属性

    p.firstName = @"zhao1";
    [p setNewInnerName:@"newInnerNmame"];// 没有暴露的属性的get、set方法被调用时,也会发送通知

    // 子类的属性监听
    Student *stu = [[Student alloc] init];
    stu.firstName = @"stu";
    stu.lastName = @"dent";

    StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu];
    [stuObserver addObserverForKeyPath:@"fullName"];// 子类继承属性依旧被监听
    [stuObserver addObserverForKeyPath:@"firstName"];   // 重写方法,不加super,依旧会监听kvo
    [stuObserver addObserverForKeyPath:@"innerName"]; 
    
    stu.firstName = @"stu1";
    stu.lastName = @"dent1";
    [stu setNewInnerName:@"newInnerNmame"];// 没有暴露的属性的get、set方法被调用时,也会发送通知
}

通过上面的例子,我们能看出几点:
①通过KVO,能观察父类的属性值。
②只要知道了keyPath,不管有没有暴露方法,依旧可以通过KVO方式观察值的变化,而且同属性一样,可以被继承。
③子类重写父类的set方法,也并不会影响KVO的观察。

从这儿开始就有点好奇了,这个KVO是否通过子类化的方法实现?那如何让子类的继承属性也能被监听到?了解到KVO依赖setter方法的重写,那我子类重写的setter方法之后,为什么子类继承属性的监听依然生效?

3.跨线程的监听

我们知道使用Notification时,跨线程发送通知是无法被接受到的,那么现在看看KVO在多线程中的表现。

 //  在两个线程定义目标和观察者
    dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
//    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    __block Student *stu1 = nil;
    dispatch_async(concurrentQueue, ^{
        // 对象属性
        stu1 = [[Student alloc] init];
        NSLog(@"Student %@",[NSDate new]);
        stu1.lastName = @"yyyyyyy";
    });
    
    __block StudentKvoObserver *stuObserver1;
    dispatch_async(concurrentQueue, ^{
        sleep(2);

        stuObserver1 = [[StudentKvoObserver alloc] initWithStudent:stu1];
        [stuObserver1 addObserverForKeyPath:@"fullName"];// 子类继承属性依旧被监听
        NSLog(@" StudentKvoObserver %@",[NSDate new]);

    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"dispatch_barrier_async %@",[NSDate new]);
        NSLog(@"zzzzzz start%@",[NSDate new]);
        stu1.lastName = @"zzzzzz";
        NSLog(@"zzzzzz end%@",[NSDate new]);
    });

输出结果

2016-10-11 10:46:53.319 KVCLearn[3364:331572] Student 2016-10-11 02:46:53 +0000
2016-10-11 10:46:55.324 KVCLearn[3364:331578] StudentKvoObserver 2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.325 KVCLearn[3364:331578] dispatch_barrier_async 2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.325 KVCLearn[3364:331578] zzzzzz start2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.326 KVCLearn[3364:331578] fullName
<Student: 0x7fb2cbd8ca50>
{
kind = 1;
new = "(null)zzzzzz";
old = "(null)yyyyyyy";
}
<StudentKvoObserver: 0x7fb2cbc08d60>
2016-10-11 10:46:55.326 KVCLearn[3364:331578] zzzzzz end2016-10-11 02:46:55 +0000

可以看到在两个不同的线程里创建的Observer和Target,观察变化也是能够生效的。
这里有一个关于GCD的问题,这里我使用了dispatch_barrier_async,分发到自定义的并发队列上,这时barrier是正常工作的,保证了第三个task在前两个执行完之后执行。但是当我直接使用系统全局的并发队列时,barrier不起作用,不能保证他们的执行顺序。这里希望有高人看见了能解答下。

二、实现探究

1.API接口

Foundation里关于KVO的部分都定义在NSKeyValueObserving.h中,KVO通过以下三个NSObject分类实现。

  • NSObject(NSKeyValueObserving)
  • NSObject(NSKeyValueObserverRegistration)
  • NSObject(NSKeyValueObservingCustomization)

这里会从NSObject (NSKeyValueObserverRegistration) 的 - addObserver:forKeyPath:options:context: 为入口,去一步步分析如何整个KVO的实现方式。

2.先说结论

实现方式:
一个对象在被调用addObserver方法时,会动态创建一个KVO前缀的原类的子类,用来重写所有的setter方法,并且该子类的- (Class) class- (Class) superclass方法会被重写,返回父类(原始类)的Class。最后会将当前对象的类改为这个KVO前缀的子类。

比较绕,让我们来看个例子。比如说类Person的实例person调用了addObserver方法时,addObserver方法内部给你创建了一个KVOPerson类,KVOPerson的所有的setter方法会被重写,它的class和superClass方法会被改写成返回Person和NSObject,之后使用object_setClass将KVOPerson设置成person的class。

当我们调用person的setName方法时,实际是调用的一个KVOPerson实例的setName方法,但由于重写了class,在外部看不出来其中的差别。在setter方法中,我们在实际值被改变的前后回调用- (void)willChangeValueForKey:(NSString *)key;- (void)didChangeValueForKey:(NSString *)key;方法,通知观察者值的变化。

3.代码

源码是来自GNUSetup里的Foundation,据说和apple的实现类似,只是相关API的版本会比较老一些。我们先从addObserver方法开始。

@implementation NSObject (NSKeyValueObserverRegistration)

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  ....

  // 1.使用当前类创建GSKVOReplacement对象 
  r = replacementForClass([self class]);

  ....

  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)
    {
      info = [[GSKVOInfo alloc] initWithInstance: self];
      [self setObservationInfo: info];
      //2.重新设置class
      object_setClass(self, [r replacement]);
    }

    ....

   //3.重写replace的setter方法
   [r overrideSetterFor: aPath];
   //4.注册当前类和观察者到全局表中
   [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
}

忽略了一些分支,可以看到主要为上面四个步骤。我们可以一个一个拆开来看。

replacementForClass
// 单例生成一个GSKVOReplacement对象,保证一个类只有一个KVO子类
static GSKVOReplacement *
replacementForClass(Class c)
{
  GSKVOReplacement *r;

  setup();
  [kvoLock lock];
  r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
  if (r == nil)
    {
      r = [[GSKVOReplacement alloc] initWithClass: c];
      NSMapInsert(classTable, (void*)c, (void*)r);
    }
  [kvoLock unlock];
  return r;
}

- (id) initWithClass: (Class)aClass
{
  NSValue       *template;
  NSString      *superName;
  NSString      *name;

  original = aClass;

  superName = NSStringFromClass(original);
  name = [@"GSKVO" stringByAppendingString: superName];// 添加前缀
  template = GSObjCMakeClass(name, superName, nil);// 通过objc_allocateClassPair得到class指针
  GSObjCAddClasses([NSArray arrayWithObject: template]);// objc_registerClassPair注册class
  replacement = NSClassFromString(name);// 前面动态生成且注册了GSKVO子类,然后就可以通过该方法得到
// 添加模板类的一些方法,包括重写class和superClass让对象类型不暴露,
// setValue:forkey在数据改变前后加上willChange和didChange方法 
  GSObjCAddClassBehavior(replacement, baseClass);

  /* Create the set of setter methods overridden.
   */
  keys = [NSMutableSet new];

  return self;
}
object_setClass(self, [r replacement]);
// replace就是新生成的KVOXXX的class

@interface  GSKVOReplacement : NSObject
{
  Class         original;       /* The original class */
  Class         replacement;    /* The replacement class */
  NSMutableSet  *keys;          /* The observed setter keys */
}


replacement = NSClassFromString(name);// 在initWithClass方法中赋值
overrideSetterFor
重写setter方法,在值改变前后添加上willChange&didChange
- (void) overrideSetterFor: (NSString*)aKey
{
  if ([keys member: aKey] == nil)
    {
      NSMethodSignature *sig;// 当前key值对应setter的方法签名
      SEL       sel;// 当前key值对应setter的方法名selector
      IMP       imp;// 当前key值对应setter的函数指针IMP

      const char    *type;
      NSString          *a[2];
      unsigned          i;
      BOOL              found = NO;
      
      // 得到setXxxx:和_setXxxx:方法名
      a[0] = [NSString stringWithFormat: @"set%@%@:", tmp, suffix];
      a[1] = [NSString stringWithFormat: @"_set%@%@:", tmp, suffix];

      for (i = 0; i < 2; i++)
        {
          /*
             得到方法签名
           */
          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:)];// 返回setterChar:函数的函数指针IMP
                break;
              // 对象、类、指针
              case _C_ID:
              case _C_CLASS:
              case _C_PTR:
                imp = [[GSKVOSetter class]
                  instanceMethodForSelector: @selector(setter:)];// 返回setter:函数的函数指针IMP,后面有详解
                break;
                break;

              ....
                
              default:
                imp = 0;
                break;
            }

          if (imp != 0)
            {
          if (class_addMethod(replacement, sel, imp, [sig methodType]))// 将原sel和新imp加到replacement类中去
        {
                  found = YES;
        }
          else
        {
          NSLog(@"Failed to add setter method for %s to %s",
            sel_getName(sel), class_getName(original));
        }
            }
        }
      if (found == YES)
        {
          [keys addObject: aKey];
        }
    }
}

这个步骤是将keypath对应的setter方法重写找出来,把原有的SEL函数名和重写后的实现IMP加入到子类中去。这样做,新生成的子类就有和原父类一样表现了,再加上之前的class替换,在KVO的对外接口上已经没有差别。这里也解释了我一开始的问题,keypath到底指的是什么,其实是setter方法,或者说方法名的后缀。因为我们用@property生成了默认的set方法是满足规范的,所以会将keypath和property关联起来。

// setter方法的实现细节
@implementation GSKVOSetter
- (void) setter: (void*)val
{
  NSString  *key;
  Class     c = [self class];
  void      (*imp)(id,SEL,void*);

  imp = (void (*)(id,SEL,void*))[c instanceMethodForSelector: _cmd];

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

对于这个setter方法的实现,我其实是没大看懂的。[c instanceMethodForSelector: _cmd];这个取到的imp,应该是当前方法的函数指针(GSKVOSetter的setter),后面也是直接调用的该imp实现。没有找到这个setter是如何和原类方法中实际的setter联系起来的,之前通过sig方法签名也只取出了sel,原有实现并没有出现。希望有大牛看到这个能给我解答一下。

-(void) addObserver: forKeyPath: options: context:

这个部分就是观察者的注册了。通过以下类图可以很方便得看到,所有的类的KVO观察都是通过infoTable管理的。以被观察对象实例作key,GSKVOInfo对象为value的形式保存在infoTable表里,每个被观察者实例会对应多个keypath,每个keypath会对应多个observer对象。顺带提一下,关于Notification的实现也类似,也是全局表维护通知的注册监听者和通知名。
GSKVOInfo的结构可以看出来,一个keyPath可以对应有多个观察者。其中观察对象的实例和option打包成GSKVOObservation对象保存在一起。

KVO实现类图.jpg

三、总结

看完了KVO的实现部分,我们再回过头来看开头提到的几个问题。

keyPath是什么
首先keyPath,是对于setter方法的关联,会使用keypath作为后缀去寻找原类的setter方法的方法签名,和实际存取对象和property名称没有关系。所以这也是为什么我们重命名了setter方法之后,没有办法再去使用KVO或KVC了,需要手动调用一次willChangeValue方法。

子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?
因为继承的关系Father <- Son <- KVOSon,当我监听一个父类属性的keyPath的时候,Son实例同样可以通过消息查找找到父类的setter方法,再将该方法加入到KVOSon类当中去。

子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO能否观察到?
由于在overrideSetterFor中,我们是直接通过sel去得到方法签名signature,所以和暴不暴露没啥关系。

子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?
在上一条中知道,其实子类监听父类属性,并不依赖继承,而是通过ISA指针在消息转发的时候能够获取到父类方法就足够。所以当我们重写父类setter方法,相当于在子类定义了该setter函数,在我们去用sel找方法签名时,直接在子类中就拿到了,甚至都不需要去到父类里。所以理解了KVO监听父类属性和继承没有直接联系这一点,就不再纠结set方法是否重写这个问题了。

最后线程安全的部分,没有做深入的研究,在这篇就不多做表述了。在我贴的源码中都去掉了很多枝叶,其中就包括加锁的部分。有兴趣的朋友可以去下面贴的源码地址去看完整版,其中对线程安全的考虑,递归锁、惰性递归锁使用,也是很值得学习的。

例子和源码的资料

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

推荐阅读更多精彩内容

  • 本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。 一、使用上的疑问 1.key...
    奋拓达阅读 499评论 0 2
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,011评论 0 26
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 守素小师妹 【武林地下小报】受琅琊阁卖榜的影响,家长们普遍认为老牌武林名校无法适应今天武林的发展。武林高考结束后,...
    茜羽阅读 917评论 28 24
  • 借我一道刺眼的光 暴露一抹沉寂的伤 借我一拂温柔的风 干化一掬落寞的泪 丛山回响 借我一生的漂泊 借我一盏飘零的灯...
    321小肥仔阅读 178评论 0 0