KVC和KVO的底层原理

作者: HelloAda | 来源:发表于2017-04-19 18:11 被阅读626次
    KVC和KVO在实际的运用中是很常见的。所以了解它的底层实现原理是非常不错的一件事。

    KVC(NSKeyValueCoding)

    KVC就是通过key值,来获取对象的属性进行操作,而不是通过我们明确的存取方式来获取,是一个非正式的Protocol。KVO就是基于KVC来实现的。

    KVC的一般使用:

    @interface Person : NSObject
    {
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
    }
    - (void)testName;
    
    @end
    @implementation Person
    
    - (NSString *)getName {
        NSLog(@"%s",__func__);
        return @"D";
    }
    
    - (NSString *)name {
        NSLog(@"%s",__func__);
        return @"D";
    }
    
    - (NSString *)isName {
        NSLog(@"%s",__func__);
        return @"D";
    }
    
    - (void)setName:(NSString *)name {
        NSLog(@"%s",__func__);
    }
    //- (NSInteger)countOfName {
    //    return 2;
    //}
    //
    //- (id)objectInNameAtIndex:(NSInteger)index {
    //    return @"arrayItem";
    //}
    
    - (void)testName {
        NSLog(@"_name = %@",_name);
        NSLog(@"name = %@",name);
        NSLog(@"isName = %@",isName);
        NSLog(@"_isName = %@",_isName);
    }
    
    - (id)valueForUndefinedKey:(NSString *)key {
        NSLog(@"取值没有找到这个key %@",key);
        return nil;
    }
    
    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key {
        NSLog(@"设值没有找到这个key %@",key);
    }
    
    + (BOOL)accessInstanceVariablesDirectly {
        return YES;
    }
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *person = [[Person alloc] init];
        [person valueForKey:@"name"];
        [person setValue:@"ADA" forKey:@"name"];
        [person testName];
    }
    
    

    运行后,set和get方法都会被执行,但是这与点语法还是有区别的。

    KVC有自己的执行机制

    在调用 setValue: forKey: 的时,程序优先调用 setName: 方法,如果没有找到 setName: 方法 KVC会检查这个类的 + (BOOL)accessInstanceVariablesDirectly 类方法看是否返回YES(默认YES),返回YES则会继续查找该类有没有名为_name的成员变量,如果还是没有找到则会继续查找_isName成员变量,还是没有则依次查找name,isName。上述的成员变量都没找到则执行setValue:forUndefinedKey: 抛出异常,如果不想程序崩溃应该重写该方法。假如这个类重写了+ (BOOL)accessInstanceVariablesDirectly 返回的是NO,则程序没有找到setName:方法之后,会直接执行setValue:forUndefinedKey: 抛出异常。

    在调用valueForKey:的时,会依次按照getName,name,isName的顺序进行调用。如果这3个方法没有找到,那么KVC会按照countOfName,objectInNameAtIndex来查找。如果查找到这两个方法就会返回一个数组。如果还没找到则调用+ (BOOL)accessInstanceVariablesDirectly 看是否返回YES,返回YES则依次按照_name,_isName,name,isName顺序查找成员变量名,还是没找到就调用valueForUndefinedKey:;返回NO直接调用valueForUndefinedKey:

    KVC的一些注意

    KVC在设置时可能会设置错误的Key值导致程序崩溃,需要重写valueForUndefinedKey:和setValue:forUndefinedKey:。还有一种是在设置中不小心传递了nil,这时候需要重写setNilValueForKey:。

    可能还有一些内容我没有提到,读者可自行注释上面所展示的代码来验证查找的顺序,会比较好理解。

    KVO(Key-Value Observing)

    KVO是OC设计模式中的一种,简单的说就是添加一个被观察对象A的属性,当被观察对象A的属性发生更改时,观察对象会获得通知,并作出相应的处理。NSObject类都实现了KVO ,解决了观察对象和被观察对象的解耦。

    KVO的一般使用

    @interface Person : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    @interface ViewController ()
    {
        Person *person;
    }
    @end
    
    @implementation Person
    
    //+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    //    return NO;
    //}
    @end
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        person = [[Person alloc] init];
    //  第三个参数代表新值
        [person addObserver:self 
                 forKeyPath:@"name" 
                    options:NSKeyValueObservingOptionNew 
                    context:nil];
    }
    
    - (IBAction)change:(id)sender {
    //    [person willChangeValueForKey:@"name"];
        person.name = @"ADA";
    //    [person didChangeValueForKey:@"name"];
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"keyPath = %@",keyPath);
        NSLog(@"object = %@",object);
        NSLog(@"change = %@",change);
    }
    
    - (void)dealloc {
        [person removeObserver:self forKeyPath:@"name" context:nil];
    }
    

    这个是常见的KVO。
    其实这个是自动实现的KVO还有手动实现的KVO
    将上诉注释掉的代码打开即可实现。

    KVO的底层是通过isa-swizzling实现的。官方文档中第一段有提到

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

    那么这个isa-swizzling是什么呢?

    大家可能对Method-Swizzling会比较熟悉,它的实现其实是一个替换函数实现指针的过程。

    method-swizzling
    具体实现的代码:
    + (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector isClassMethod:(BOOL)isClassMethod {
        Class class = [self class];
        Method originalMethod;
        Method swizzledMethod;
        
        if (isClassMethod) {
            originalMethod = class_getClassMethod(class, originalSelector);
            swizzledMethod = class_getClassMethod(class, swizzledSelector);
        }else {
            originalMethod = class_getInstanceMethod(class, originalSelector);
            swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        }
        if (!originalMethod) {
            NSLog(@"original is nil (%@)",originalMethod);
        }
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    

    那么isa-swizzling顾名思义就是替换isa的过程。

    那isa又是什么呢?

    oc是面向对象的语言,每一个对象都是一个类的实例。
    每个对象都有一个名为isa的指针,指向该对象的类。每个类中又描述了它的实例的特点,比如成员变量列表,成员函数列表。每一个对象都可以接收消息,而对象能够接收的消息列表都保存在它所对应的类中。NSObject就是一个包含isa指针的结构体

    NSObject的定义头文件

    从Class的定义中,我们也可以看出Class也是一个包含isa指针的结构体。每一个类实际上也是一个对象,每一个类也有一个名为isa的指针。

    Class的定义头文件

    既然每一个类也是一个对象,那它必然是另一个类的实例。这个类就是元类(meta)。元类也是一个对象。元类的isa指针都指向一个根元类.根元类本身的isa指针指向自己。


    class-diagram.jpg

    这一块可能有点绕。
    个人理解的isa就是一个Class 类型的指针. 每个实例对象都有一个isa的指针,指向该对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。同样的,元类也是类,它也是对象。元类也有isa指针,最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

    isa-swizzling就是在运行时动态地修改 isa 指针的值,达到替换对象整个行为的目的。

    既然是替换了类,那么在添加了KVO之后这个类究竟做了什么改变。
    我们可以通过object_getClass()来打印出isa指针。

        NSLog(@"%@",object_getClass(person));
        [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        NSLog(@"%@",object_getClass(person));
    
    

    运行后可以在控制台看到:

    • 2017-04-19 16:49:55.277 KVODemo[1759:1701820] Person
    • 2017-04-19 16:49:55.278 KVODemo[1759:1701820] NSKVONotifying_Person

    也就是说pesron对象的isa指针已经指向了NSKVONotifying_Person类了。

    那这个NSKVONotifying_Person类究竟是什么呢?

    在网上查阅后发现,这个NSKVONotifying_Person是Person的一个子类。

    我们可以通过class_getSuperclass来验证。

    @implementation Person
    - (void)print{
        NSLog(@"isa:%@, supper class:%@", NSStringFromClass(object_getClass(self)), class_getSuperclass(object_getClass(self)));
    }
    @end
    

    然后再添加KVO之前和之后分别调用这个方法,可以在控制台看到:

    • 2017-04-19 17:43:33.311 KVODemo[1899:1927921] isa:Person, supper class:NSObject
    • 2017-04-19 17:43:33.312 KVODemo[1899:1927921] isa:NSKVONotifying_Person, supper class:Person

    所以可以知道NSKVONotifying_Person是Person的子类。

    然后还有一点就是系统是自动实现监听类的属性,那么set方法就有可能被重写了,因为消息机制是通过isa查找的,如果子类中没有对应的方法,就会在父类中查找,但是我们在Person中并没有写willChangeValueForKey:和didChangeValueForKey:这两个方法。所以肯定也是在子类中实现的。

    在print方法里 在加入一条打印

        NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
    

    运行后控制台显示:

    • name setter function pointer:0x10c265740
    • name setter function pointer:0x10c368c60

    证明set方法确实是被重写了。

    到这里基本可以确定KVO的实现是:

    添加观察后:
    系统实现了一个子类,然后将被观察的类对象的isa指针指向这个子类。再重写了setter方法。并在当中添加了willChangeValueForKey:和didChangeValueForKey:。
    移除观察就是将isa指针指向原来的类对象中。

    那么isa-swizzling做的处理应该是这样的:


    isa-swizzling.png

    大概就这样子,如果有什么不对的地方,欢迎大家提出来。共同进步。
    觉得对你有帮助给个喜欢。

    相关文章

      网友评论

        本文标题:KVC和KVO的底层原理

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