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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,018评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,046评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,215评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,303评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,181评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,171评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,577评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,260评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,546评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,630评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,390评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,260评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,633评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,920评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,210评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,569评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,767评论 2 335

推荐阅读更多精彩内容