KVO进阶——KVO实现探究

作者: 孢子菌 | 来源:发表于2016-10-13 16:09 被阅读974次

    本篇会对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方法是否重写这个问题了。

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

    例子和源码的资料

    相关文章

      网友评论

      • NeroXie:关于GCD的那个问题,在源代码中是有答案的,dispatch_barrier_async如果传入的是全局队列,在唤醒队列时会执行_dispatch_queue_wakeup_global函数,其执行效果同dispatch_async一致,而如果是自定义的队列的时候,_dispatch_continuation_pop中会执行dispatch_queue_invoke。它的作用简单的说就是在while循环中依次取出任务并发执行。当遇到DISPATCH_OBJ_BARRIER_BIT标记时,会修改标记值以保证后续while循环时直接goto out。
      • 热血足球2016:你好大大,我想请假一个问题,我uitextview 我写了一个子类继承他,我监听他的text在子类中,发现不走代理,监听不到
        [self.textView.superclass addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
        // [self.textView addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
        // 都没监听到
        }

        - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
        {
        if ([keyPath isEqualToString:@"text"]) {
        NSString *str = change[NSKeyValueChangeNewKey];
        NSLog(@"str = %@",str);
        self.countLabel.text = [NSString stringWithFormat:@"%02lu/100", str.length];
        }
        }
      • b59457960ac9:探索性很强,赞一个
      • 小乙的乙:看这篇文章有点晕
        孢子菌:@山是水的故事 😆
        山是水的故事:志宇???
        孢子菌:主要是子类化&动态替换方法,最后的类图可以多看看
      • 孢子菌:关于GCD dispatch_barrier_async 的问题,今天同事在文档中找到了答案。dispatch_barrier_async适用的场景是dispatch queue必须是用DISPATCH_QUEUE_CONCURRENT属性创建的queue,而使用global concurrent queues的时候,dispatch_barrier_async就和dispatch_async表现一样。多谢f.li~
      • 码农二哥:对于这个setter方法的实现,我其实是没大看懂的。[c instanceMethodForSelector: _cmd];这个取到的imp,应该是当前方法的函数指针(GSKVOSetter的setter),后面也是直接调用的该imp实现。没有找到这个setter是如何和原类方法中实际的setter联系起来的,之前通过sig方法签名也只取出了sel,原有实现并没有出现。
        - (void)doConstraintTest
        {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        }
        - (void)doNSProgressTest
        {
        }
        - (void)doExchangeMethodTest
        {
        Method m1 = class_getInstanceMethod(self.class, @selector(doConstraintTest));
        Method m2 = class_getInstanceMethod(self.class, @selector(doNSProgressTest));
        method_exchangeImplementations(m1, m2);
        [self doNSProgressTest];//你猜这里输出什么了什么还是什么也没有输出?这应该是一个道理。
        }
        孢子菌:@黑桃一 明白了,`[self doNSProgressTest]` 等价于`objc_msgsend(self, "doNSProgressTest")`,虽然实现变成了doConstraintTest,其中第二个参数就是_cmd,就是doNSProgressTest。也就是原方法`setName:`实现被替换成了`setter:`的之后,后者的_cmd依然是setName

      本文标题:KVO进阶——KVO实现探究

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