KVO详解

作者: NeroXie | 来源:发表于2018-08-13 15:03 被阅读67次

    在iOS开发中,我们可以通过KVO机制来监听某个对象的某个属性的变化。

    KVO实现步骤

    KVO的实现分为三步:

    1.- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

    2.- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

    3.- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

    KVO实现机制

    KVO的实现依赖于RunTime,在Apple的文档中有提到过KVO的实现:

    Automatic key-value observing is implemented using a technique called isa-swizzling.

    The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

    When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

    You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    Apple的文档提到KVO是使用了isa-swizzling的技术。当观察者注册对象的属性时,观察对象的isa指针被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。另外还提到我们不应该依赖isa指针来确定类成员资格,而是使用类方法来确定对象实例的类。

    isa-swizzling

    先用一段代码验证一下KVO的实现是不是进行了isa-swizzling
    首页实现一个Person类:

    @interface Person : NSObject
        
    @property (nonatomic, copy) NSString *name;
    
    - (void)printInfo;
    
    @end
    
    @implementation Person
    
    - (void)printInfo {
        NSLog(@"isa:%@, supper class:%@", NSStringFromClass(object_getClass(self)), class_getSuperclass(object_getClass(self)));
        NSLog(@"self:%@, [self superclass]:%@", self, [self superclass]);
        NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
        NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printInfo)));
    }
    
    @end
    

    ViewController中使用KVO监听Person对象的相关属性:

    #pragma mark - Lifceycle
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [[Person alloc] init];
        NSLog(@"Before KVO --------------------------------------------------------------------------");
        [self.person printInfo];
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        NSLog(@"After KVO --------------------------------------------------------------------------");
        [self.person printInfo];
        [self.person removeObserver:self forKeyPath:@"name"];
        NSLog(@"Remove KVO --------------------------------------------------------------------------");
        [self.person printInfo];
    }
    
    #pragma mark - KVO
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    }
    

    打印一下结果:


    isa-swizzling.jpg

    添加KVO之后,isa已经替换成了NSKVONotifying_Person,而根据class_getSuperclass得到的结果竟然是Person, 然后name是使我们KVO需要观察的属性,它的setter函数指针变了。

    KVO的实现原理:

    • add observer

      1. 通过runtime生成一个以NSKVONotifying_+类名的形式来命名的派生类。
      2. 将被观察的对象的isa指针指向这个派生类。
      3. 重写setter方法,重写setter方法的本质是在赋值语句前后加上相应的通知。
    • remove observer

      将其的isa指针指向原来的类对象中

    上面提到必须是重写setter方法,如果是直接对属性进行赋值的话,是不会触发KVO的。其实到这里我们有个疑问,setter到底是怎么被重写的。

    重写Setter方法

    虽然Apple并没有开源KVO的代码,但是我们可以通过验证的方式进行推导。

    Person.m文件中添加以下代码:

    - (void)willChangeValueForKey:(NSString *)key {
        NSLog(@"%s", __func__);
        [super willChangeValueForKey: key];
    }
    
    - (void)didChangeValueForKey:(NSString *)key {
        NSLog(@"%s", __func__);
        [super didChangeValueForKey:key];
    }
    

    打印结果:


    rewriteSetter.jpg

    从上面的结果我们可以知道KVO在重写setter方法的时候,调用willChangeValueForKeydidChangeValueForKey通知观察者。**

    KeyPath

    KeyPath使我们用来监听的属性,它的实质是什么?先看一段代码:

    //Person.h
    @interface Person : NSObject
    
    @property (nonatomic, copy) NSString *nick;
    
    @end
    
    //Person.m
    @synthesize nick = realNick;
    
    - (void)setNick:(NSString *)nick {
        realNick = nick;
    }
    
    - (NSString *)nick {
        return realNick;
    }
    
    //ViewController
    - (void)_testKeyPath {
        self.person = [[Person alloc] init];
        [self.person addObserver:self
                      forKeyPath:@"nick"//"realNick"
                         options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                         context:nil];
        self.person.nick = @"Nero";
        [self.person removeObserver:self forKeyPath:@"nick"];
    }
    

    实际结果是,使用nick能够监听到对应变化,而使用真正的实例变量realNick时,无法监听到值。KeyPath指向的并不是真正的实例变量,而是对于setter方法的关联,KVO会使用Keypath作为后缀去寻找原类的setter方法的方法签名,和实际存取对象和属性名称没有关系。所以这也是为什么我们重命名了setter方法之后,没有办法再去使用KVOKVC了,需要手动调用一次willChangeValue方法。

    手动触发一个KVO

    代码如下:

    //Person
    @interface Person : NSObject {
    @public NSInteger age;
    }
    
    @end
    
    //ViewController
    - (void)_testImplementKVOManually {
        self.person = [[Person alloc] init];
        [self.person addObserver:self
                      forKeyPath:@"age"
                         options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                         context:nil];
        [self.person willChangeValueForKey:@"age"];//获取旧值
        self.person->age = 26;
        [self.person didChangeValueForKey:@"age"];//获取新值
        [self.person removeObserver:self forKeyPath:@"age"];
    }
    

    KVO的跨线程监听

    我们知道使用Notification时,跨线程发送通知是无法被接受到的,但是KVO是可以跨线程监听的。

    - (void)_testKVOinGCD {
        dispatch_queue_t queue = dispatch_queue_create("test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
        self.person = [Person new];
        dispatch_async(queue, ^{
            NSLog(@"%@", [NSDate date]);
            self.person.name = @"Nero";
        });
        
        dispatch_async(queue, ^{
            NSLog(@"%@", [NSDate date]);
            sleep(1);
            [self.person addObserver:self
                          forKeyPath:@"name"
                             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                             context:nil];
            NSLog(@"%@", [NSDate date]);
        });
        
        dispatch_barrier_async(queue, ^{
            NSLog(@"%@", [NSDate date]);
            self.person.name = @"NeroXie";
        });
    }
    

    可以看到在两个不同的线程里创建的Observer和Target,观察变化也是能够生效的。
    这里使用了dispatch_barrier_async是确保第三个task在前两个task运行后再执行,而且使用的队列必须是自定义的并发队列,如果使用全局队列,栅栏就不想起作用了,因为dispatch_barrier_async相当于dispatch_asysc

    相关文章

      网友评论

        本文标题:KVO详解

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