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
,_isKey
,key
,iskey
的顺序搜索成员名。
(3)如果没有找到成员变量,调用- (void)setValue: forUnderfinedKey:
为了验证对KVC对象赋值顺序,是按照 set<Key>
、_<key>
、 _is<Key>
、is<Key>
的顺序设置成员变量,分别定义了1至4 步骤,设置进行验证;
- 操作步骤
- 保留
_name2
、_isName2
、name2
、isName2
成员变量 ,通过KVC给 name2 赋值,打印有关 name2 的成员变量值
- 注释
_name2
,打印有关name2 的成员变量值
- 注释
_name2
和_isName2
,打印有关name2 的成员变量值
- 注释
_name2
、_isName2
和name2
,打印有关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)首先按 getKey
,key
,isKey
的顺序查找getter
方法,找到直接调用。如果是BOOL、int
等内建值类型,会做NSNumber
的转换。
(2)上面的getter没找到,查找countOfKey
、objectInKeyAtindex
、KeyAtindexes
格式的方法。如果countOfKey
和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。
(3)还没找到,查找countOfKey
、enumeratorOfKey
、memberOfKey
格式的方法。如果这三个方法都找到,那么就返回一个可以响应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 的一些猜想和实现;
2.1 第一种默认情况(不重写set 方法)
定义属性:(不重写age
的set \ 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方法、不赋值)
重写name
的set
方法,对成员变量_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