认识KVO

2020-08-10  本文已影响0人  凌云01

问题

KVO的本质是什么?(iOS用什么方式实现对一个对象的KVO?)
如何手动触发KVO?
直接修改成员变量会触发KVO么?

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person1 = [[Person alloc] init];
    person1.age = 10;
    
    Person *person2 = [[Person alloc] init];
    person2.age = 20;
    
    // self 监听 person1的 age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
    
    person1.age = 11;
    
    [person1 removeObserver:self forKeyPath:@"age"];
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);

}

//控制台输出内容
监听到<Person: 0x600000005550>的age改变了{
    kind = 1;
    new = 11;
    old = 10;
}

通过上面代码可以看到,在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。

KVO的底层实现

我们知道复制操作就是调用了set方法,我们可以重写Person类的age的set方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。

- (void)setAge:(int)age {
   _age = age;
}

发现即使重写了setAge方法,person1对象和person2对象调用同样的set方法,但是我们发现person1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath方法。
我们知道setAge方法是放在Person类对象里面的,而instance对象的isa指针指向的就是Person类对象,我们分别打印看一下person1和person2的isa的值:


addObserver对person1对象的处理

通过上图我们发现,person1对象执行过addObserver操作之后,person1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,这个类对象是OC Runtime运行时动态定义的一个新类,这个新类是Person的子类,其superClass指向Person类对象。而person2对象没有任何改变。

首先我们知道,person2在调用setAge方法的时候,首先会通过person2对象中的isa指针找到Person类对象,然后在类对象中找到setAge方法。然后找到方法对应的实现。


未使用KVO监听的对象

那么person1对象在调用setAge方法的时候,肯定会根据person1的isa找到NSKVONotifying_Person,在NSKVONotifying_Person中找setAge的方法及实现。

使用了KVO监听的对象

经过查阅资料我们可以了解到
NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。

NSKVONotifyin_Person内部结构是怎样的?

- (void)printMethodNamesOfClass:(Class)class{
    unsigned int count;
    //获取方法数组
    Method *methodList = class_copyMethodList(class, &count);
    
    NSMutableString *methodNames = [NSMutableString string];
    //遍历所有方法
    for (int i = 0; i < count; i ++) {
        //获得方法
        Method method= methodList[i];
        //获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        //拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@","];
    }
    //释放
    free(methodList);
    //打印方法名
    NSLog(@"%@ %@", class, methodNames);
}


- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person1 = [[Person alloc] init];
    person1.age = 10;
    
    Person *person2 = [[Person alloc] init];
    person2.age = 20;
    
    // self 监听 person1的 age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
    
    person1.age = 11;
    
    [self printMethodNamesOfClass:object_getClass(person1)];
    [self printMethodNamesOfClass:object_getClass(person2)];

    [person1 removeObserver:self forKeyPath:@"age"];
    
}

//打印内容分别为:
NSKVONotifying_Person setAge:,class,dealloc,_isKVOA,
Person age,setAge:

通过上述代码我们发现NSKVONotifying_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA。
这里NSKVONotifying_Person重写class方法是为了隐藏NSKVONotifying_Person。不被外界所看到。我们在person1添加过KVO监听之后,分别打印person1和person2对象的class可以发现他们都返回Person。

NSLog(@"%@,%@",[person1 class],[person2 class]);
// 打印结果 Person,Person

如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到nsobject,而nsobect的class的实现大致为返回自己isa指向的类,返回person1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,但是apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用person1的class对象方法时,是Person类。这样person1给外界的感觉person1还是Person类,并不知道NSKVONotifying_Person子类的存在。

那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为

- (Class) class {
     // 得到类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}

最后我们来验证一下什么时候会调用observeValueForKeyPath:ofObject:change:context:方法
//当监听对象的属性值发生改变时,就会调用

- (void)setAge:(int)age
{
    NSLog(@"setAge:");
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}

//打印如下
willChangeValueForKey: - begin
willChangeValueForKey: - end
setAge:
didChangeValueForKey: - begin
监听到<Person: 0x60000000cf90>的age改变了{
    kind = 1;
    new = 11;
    old = 10;
}
didChangeValueForKey: - end

通过打印内容可以看到,确实在didChangeValueForKey方法内部已经调用了observer的observeValueForKeyPath:ofObject:change:context:方法。

问题

KVO的本质是什么?(iOS用什么方式实现对一个对象的KVO?)

如何手动触发KVO?
被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

//手动调用KVO
    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];

直接修改成员变量会触发KVO么?
不会,因为没有调用setter方法。因为KVO的本质是重写set方法,然后在set方法里依次调用willChangeValueForKey,原来的set方法,didChangeValueForKey,didChangeValueForKey内部会调用observer的observeValueForKeyPath: ofObject: change: context:方法


文中如果有不对的地方欢迎指出。

上一篇下一篇

猜你喜欢

热点阅读