KVC和KVO的底层原理

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

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

推荐阅读更多精彩内容