KVC & KVO 的实践和理解

1.KVC 部分

KVC全称是Key Value Coding,KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量,是苹果的黑魔法的一种;

  • 常用方法
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

具体更多可用方法可查看Foundation 框架 NSKeyValueCoding.h头文件;

1.1 KVC基本用法

在使用KVC时,直接将属性名当做key,并设置value,即可对属性进行赋值。

[kvcObj setValue:@"name1 value" forKey:@"name1"];
[kvcObj valueForKey:@"name1"];

如果调用者 设置的 value 值 不是一个对象,而是一个nil,则会执行- setNilValueForKey: 方法,-(void)setNilValueForKey: 方法是默认实现的,开发者可用通过重写对象的 -(void)setNilValueForKey: 方法来解决导致的程序异常;
需要注意的是只有 给非指针型对象的 成员变量赋值为 nil 的时候,才会触发崩溃;

  • 执行代码
    [kvcObj setValue:nil forKeyPath:@"name1"];
    [kvcObj setValue:nil forKeyPath:@"age"];

  • 异常情况:

2020-03-26 20:13:09.180672+0800 runtime[24707:6196226] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<KVCObject 0x6000000ed260> setNilValueForKey]: could not set nil as the value for the key age.'

重写 - setNilValueForKey: 方法后:

  • 添加代码
-(void)setNilValueForKey:(NSString *)key{
    NSLog(@"开发者通过KVC对【 %@ 】成员变量设置了 nil",key);
}
  • 运行结果
runtime[24810:6205573] 开发者通过KVC对【 age 】成员变量设置了 nil
1.2 处理异常

程序在调用KVC赋值的时候,会优先调用setkey方法(Key 表示属性名),如果没有找到setKey 方法,KVC会检查+ (BOOL)accessInstanceVariablesDirectly 是否返回YES,且该方法默认返回YES,如果开发者重写了 + (BOOL)accessInstanceVariablesDirectly 方法并返回 NO,系统则会直 执行 setValue:forUndefinedKey:方法,默认 setValue:forUndefinedKey: 方法没有实现,程序会异常导致崩溃;

  • 执行代码
    [kvcObj setValue:@" value" forKeyPath:@"undefineKey"];
  • 运行结果
runtime[25927:6253200] 开发者【 undefineKey 】未定义、且调用了KVC

添加了- setValue: forUndefinedKey: 方法后

  • 添加代码
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
      NSLog(@"开发者【 %@ 】未定义、且调用了KVC",key);
}
  • 运行结果
runtime[25927:6253200] 开发者【 undefineKey 】未定义、且调用了KVC
1.3 KVC 赋值查找顺序

(1)首先搜索setKey:方法。(key指成员变量名,首字母大写)

(2)上面的setter方法没找到,如果类方法 +(BOOL)accessInstanceVariablesDirectly返回YES。那么按_key_isKeykeyiskey的顺序搜索成员名。

(3)如果没有找到成员变量,调用- (void)setValue: forUnderfinedKey:

为了验证对KVC对象赋值顺序,是按照 set<Key>_<key>_is<Key>is<Key>的顺序设置成员变量,分别定义了1至4 步骤,设置进行验证;

  • 操作步骤
    1. 保留 _name2_isName2name2isName2成员变量 ,通过KVC给 name2 赋值,打印有关 name2 的成员变量值
    1. 注释 _name2,打印有关name2 的成员变量值
    1. 注释 _name2_isName2,打印有关name2 的成员变量值
    1. 注释 _name2_isName2name2 ,打印有关name2 的成员变量值
  • 执行代码
[kvcObj setValue:@"xxxx" forKey:@"name2"];
[kvcObj printName2];
  • 运行结果

1.情况1运行结果

runtime[26639:6315192] _name2 :xxxx
runtime[26639:6315192] _isName2 :(null)
runtime[26639:6315192] name2 :(null)
runtime[26639:6315192] isName2 :(null)

2.情况2运行结果 :

runtime[26689:6318649] _isName2 :xxxx
runtime[26689:6318649] name2 :(null)
runtime[26689:6318649] isName2 :(null)

3.情况3运行结果:

 runtime[26738:6321684] name2 :xxxx
 runtime[26738:6321684] isName2 :(null)

4.情况4运行结果:

runtime[27227:6340308] isName2 :xxxx

间接证明了如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作;

1.4 KVC valueForKey 的搜索方式

(1)首先按 getKeykeyisKey的顺序查找getter方法,找到直接调用。如果是BOOL、int等内建值类型,会做NSNumber的转换。

(2)上面的getter没找到,查找countOfKeyobjectInKeyAtindexKeyAtindexes格式的方法。如果countOfKey和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。

(3)还没找到,查找countOfKeyenumeratorOfKeymemberOfKey格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。
(4)还是没找到,且类方法+(BOOL)accessInstanceVariablesDirectly返回YES。那么按_key,_isKey,key,iskey的顺序搜索成员5名。

(5)再没找到,调用- (id)valueForUndefinedKey:

2.KVO 部分

KVO全称KeyValue Observing,是苹果提供的一套事件通知机制,允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。
使用KVO只需要两个步骤:

  • (1) 注册Observer;
  • (2) 接收通知;
  • (3) 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除; (建议在dealloc方法里移除)
2.1 KVO 的实现原理

当一个对象被观察时(Persion类),系统会在编译期间基于该类 实现一个新的子类(KvoPersion类),并对观察的属性 name (举例)重写其setter 方法;重写的setter 方法会在调用原setter方法之前和之后通知所有观察者 值的更改。最后通过isa 混写(isa-swizzling)把这个被观察者对象的isa 指针 指向类新创建的子类,被观察者就变成类系统生成的新子类(KvoPersion)对象;

  • 实现原理逻辑图如下帮助理解
KVO实现原理图.jpg.jpg

下面一些实例代码,是自己关于KVO 的一些猜想和实现;

打印监听对象的isa指针.png
2.1 第一种默认情况(不重写set 方法)

定义属性:(不重写ageset \ get 方法)
@property (nonatomic,assign) NSInteger age;

  • 执行代码
animal.age = 18;
//等同于
[animal setAge:18];
  • 运行结果
runtime[87775:4775968] keyPath:age
 change:{
    kind = 1;
    new = 18;
    old = 0;
}
runtime[87775:4775968] keyPath:age
 change:{
    kind = 1;
    new = 18;
    old = 18;
}
2.2 第2种情况(重写set方法、不赋值)

重写nameset方法,对成员变量_name 不赋值;
运行结果是可以触发KVO,_name 成员变量没有赋值,所有打印没有 新值(符合预期);

/* 重写被观察者属性的 name 属性*/
-(void)setName:(NSString *)name{
    NSLog(@"setName:");
    [self willChangeValueForKey:@"name"];
    [self didChangeValueForKey:@"name"];
}
  • 执行代码
    animal.name = @"peng";
  • 运行结果
runtime[87775:4775968] setName:
runtime[87775:4775968] keyPath:name
 change:{
    kind = 1;
    new = "<null>";
    old = "<null>";
}
2.3 第3种情况(重写set、并赋值)

重写set方法,并对 成员变量_address 赋值,可以预期触发了KVO,监听到新值的value;

  • 执行代码
    animal.address = @"shanghai";
  • 运行结果
runtime[87775:4775968] keyPath:address
 change:{
    kind = 1;
    new = shanghai;
    old = "<null>";
}
2.4 第4种情况(通过KVC 赋值)

对监听的对象通过KVC 赋值可以直接触发KVO的监听,其实了解KVC 工作原理的知道,也是调用setAddress方法;

  • 执行代码
    [animal setValue:@"KVCValue_shanghai" forKey:@"address"];

  • 运行结果:

2020-03-25 18:47:28.557311+0800 runtime[87775:4775968] setAddress:
2020-03-25 18:47:28.558028+0800 runtime[87775:4775968] keyPath:address
 change:{
    kind = 1;
    new = "KVCValue_shanghai";
    old = shanghai;
}    
2.5 第5种情况(主动触发、不调set方法、不赋值)
  • 添加代码
-(void)KVOtrigger{
    NSLog(@"KVOtrigger");
    [self willChangeValueForKey:@"name"];
    [self didChangeValueForKey:@"nama"];
}
  • 执行代码
    [animal KVOtrigger];,然后 并没有触发到KVO;
  • 运行结果
runtime[87775:4775968] KVOtrigger

如果有不清楚的地方,可以查看github源码 https://github.com/hunter858/runtime

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

推荐阅读更多精彩内容