一, KVO
KVO介绍
- KVO全称
KeyValueObserving
,俗称键值监听,是苹果提供的一套时事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接受事件。一般继承自NSObject的对象都默认支持KVO - KVO和NSNotificationCenter都是iOS观察者模式的一种实现。KVO对被监听对象无侵入性,不需要修改内部代码可以实现监听
实现原理
- KVO是通过
isa_swizzing
技术实现的 - 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改instance对象的属性时,会调用Foundation框架的_
NSSetXXXValueAndNotify
函数,该函数里面会先调用willChangeValueForkey
:然后调用父类的setter方法修改值,最后是didChangeForKey
:。didChangeValueForKey内部会触发监听器(Overseer) 的监听方法observeValueForKeyPath:ofObject:context:
- 并且将
class
方法重写,返回原类的Class
KVO的使用
1.通过addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接受keyPath属性的变化事件
- observer:观察者,监听属性变化的对象
- keyPath:要观察的属性名称。要和属性声明的名称一致
- options:回调方法中收到被观察者的属性的旧值或新值等 。对KVO机制进行配置,修改KVO通知的时机以及通知的内容
- context:传入任意类型的对象,在接受消息回调的代码中可以接受到这个对象,是KVO中的一致传值方式
2.在观察者中实现observeValueForKeyPath:ofObject:change:context
方法,当keyPath属性发生改变后,KVO会回调这个方法通知观察者
- keyPath:被观察对象的属性
- object:被观察的对象
- change:字典,存放相关的额值,根据options传入的枚举来返回新值旧值
- context:注册观察者的时候,context传递过来的值
3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:
方法将KVO移除
- 调用removeObserver需要在观察者消失之前,否则会导致Crash。
- 如果已经移除了监听,如果再次移除的时候,就会crash
对类对象进行验证
- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.[self setNameKVO];
}- (void)setNameKVO {self.person = [[Person alloc] init];self.person2 = [[Person alloc] init];NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object, keyPath, change, context);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {self.person.name = @"cccc";self.person2.name = @"aaaa";
}- (void) dealloc {[self.person removeObserver:self forKeyPath:@"name"];
}
点击屏幕,打印结果
2024-07-29 15:19:56.184170+0800 KVO[43833:2012363] 监听到<Person: 0x600001ba2580>的name属性值改变了 - {kind = 1;new = cccc;old = "<null>";
} - 1111
KVO是通过isa_swizzing
技术实现的。我们通过打断点,打印person和person2的isa指针。
(lldb) po self.person->isa
0x020060000383c843(lldb) po self.person2->isa
Person
通过打印我们知道两者的类对象并不相同,但person具体的类对象并没有打印出来。
导入runtime我们在注册观察者前后对两者的类进行打印
NSLog(@"person添加KVO监听之前 - %@ %@",object_getClass(self.person) , object_getClass(self.person2));NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之前 - %@ %@", object_getClass(self.person), object_getClass(self.person2));
打印结果
2024-07-29 15:19:52.524876+0800 KVO[43833:2012363] person添加KVO监听之前 - Person Person
2024-07-29 15:19:52.525004+0800 KVO[43833:2012363] person添加KVO监听之前 - NSKVONotifying_Person Person
添加KVO监听之后,person的类对象变为了NSKVONotifying_YZPerson
,这是苹果为我们生成的中间类。
对setter方法IMP进行验证
当改变name属性的时候,是调用setName:进行的,我们来看看setName:有什么变化
NSLog(@"person添加KVO监听之前 - %p %p",[self.person methodForSelector:@selector(setName:)], [self.person2 methodForSelector:@selector(setName:)]);NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之后 - %p %p",[self.person methodForSelector:@selector(setName:)], [self.person2 methodForSelector:@selector(setName:)]);
打印结果
2024-07-29 15:29:08.844353+0800 KVO[43989:2019265] person添加KVO监听之前 - 0x1043b5ab8 0x1043b5ab8
2024-07-29 15:29:08.844517+0800 KVO[43989:2019265] person添加KVO监听之后 - 0x180b60f08 0x1043b5ab8
添加监听后,self.person的setName的地址发生了改变
进一步打断点获取详细信息
2024-07-29 15:35:05.080958+0800 KVO[44095:2024193] person添加KVO监听之前 - 0x1041d1ab8 0x1041d1ab8
2024-07-29 15:35:05.081123+0800 KVO[44095:2024193] person添加KVO监听之后 - 0x180b60f08 0x1041d1ab8
(lldb) po (IMP) 0x1041d1ab8
(KVO`-[Person setName:] at Person.h:14)
(lldb) po (IMP) 0x180b60f08
(Foundation`_NSSetObjectValueAndNotify)
可以看到添加KVO监听之后,setName:
方法的IMP指向了Fondation框架下的_NSSetObjectValueAndNotify
内部调用流程
设置了kvo监听之后,内部调用有什么流程。我们在Person中添加如下代码
#import "YZPerson.h"@implementation YZPerson
- (void)setName:(NSString *)name{_name = name;
}- (void)willChangeValueForKey:(NSString *)key
{[super willChangeValueForKey:key];NSLog(@"willChangeValueForKey");
}- (void)didChangeValueForKey:(NSString *)key
{NSLog(@"didChangeValueForKey - begin");[super didChangeValueForKey:key];NSLog(@"didChangeValueForKey - end");
}
打印结果
2024-07-29 15:46:31.300565+0800 KVO[44318:2032881] willChangeValueForKey
2024-07-29 15:46:31.300689+0800 KVO[44318:2032881] didChangeValueForKey - begin
2024-07-29 15:46:31.301102+0800 KVO[44318:2032881] 监听到<Person: 0x600001662480>的name属性值改变了 - {kind = 1;new = cccc;old = "<null>";
} - 1111
2024-07-29 15:46:31.301192+0800 KVO[44318:2032881] didChangeValueForKey - end
发现在调用 [super didChangeValueForKey:key];的时候,监听到对象的改变,进而处理监听逻辑
窥探 NSKVONotifying_Person 的方法
- (void)printMethodNamesOfClass:(Class)cls
{unsigned int count;// 获得方法数组Method *methodList = class_copyMethodList(cls, &count);// 存储方法名NSMutableString *methodNames = [NSMutableString string];// 遍历所有的方法for (int i = 0; i < count; i++) {// 获得方法Method method = methodList[i];// 获得方法名NSString *methodName = NSStringFromSelector(method_getName(method));// 拼接方法名[methodNames appendString:methodName];[methodNames appendString:@", "];}// 释放free(methodList);// 打印方法名NSLog(@"%@ %@", cls, methodNames);
}-(void)setNameKVO{self.person = [[YZPerson alloc] init];// 注册观察者NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之后 - self.person的类是:%@ 里面的方法有:",object_getClass(self.person));[self printMethodNamesOfClass:object_getClass(self.person)];
}
打印结果
2024-07-29 16:11:14.569381+0800 KVO[44880:2053144] NSKVONotifying_Person setName:, class, dealloc, _isKVOA,
系统重写了新建的子类 NSKVONotifying_Person 的setName, class, dealloc,新增了 _isKVOA方法。
setName
在NSKVONotifying_Person ,系统会重写这个方法实现属性变化的通知
- (void)setName:(NSString *)name {[self willChangeValueForKey:@"name"];[super setName:name];[self didChangeValueForKey:@"name"];
}
作用:willChangeValueForKey:
和didChangeValueForKey:
会在属性变化之前和之后调用,触发KVO通知机制。观察者会在didChangeValueForKey:
调用时收到通知。
class
- (Class)class {return [YZPerson class];
}
重写class方法,使其返回原始类Person,而不是其真实动态生成的KVO子类。保持了对象的分装性和透明性
dealloc
- (void)dealloc {// 通常会调用移除所有观察者的代码[self removeObserver:self forKeyPath:@"name"];// 清理其他资源[super dealloc];
}
确保在对象销毁前,所有的KVO观察者都被正确的移除,防止因为访问已经释放的对象而导致崩溃
_isKVOA
- (BOOL)_isKVOA {return YES;
}
新增的私有方法,由于标识对象是否被KVO代理
手动调用KVO
KVO监听的关键 willChangeValueForKey
和 didChangeValueForKey
起了关键作用,一般来说只有监听属性发生变化的时候,才能触发监听,但是如果我们想自己手动调用KVO的话,只要自己手动调用这两个方法就可以了。
-(void)setNameKVO{self.person = [[YZPerson alloc] init];// 注册观察者NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之后 - self.person的类是:%@ 里面的方法有:",object_getClass(self.person));[self printMethodNamesOfClass:object_getClass(self.person)];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{// self.person.name = @"ccc";// 手动调用KVO[self.person willChangeValueForKey:@"name"];[self.person didChangeValueForKey:@"name"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{// 移除监听[self.person removeObserver:self forKeyPath:@"name"];
}
打印结果
2024-07-29 16:44:50.179131+0800 KVO[45535:2079896] 监听到<Person: 0x60000350cf80>的name属性值改变了 - {kind = 1;new = "<null>";old = "<null>";
} - 1111
用于new = null,也就是name的值没有改变,我们手动调用才触发的监听。
KVC
- KVC是
Key Value Coding
的简称。它是一种痛过字符串的名字(key)来访问类属性的机制。而不是通过Setter和Getter方法访问。KVC的方法定义在Foundation/NSKeyValueCoding
中。 - KVO和KVC都属于键值编程而底层实现机制都是isa_swizzing
常见的API有
-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
-(void)setValue:(id)value forKey:(NSString *)key;
-(id)valueForKeyPath:(NSString *)keyPath;
-(id)valueForKey:(NSString *)key;
key和keyPath的区别
key:
只能访问对象的当前层级属性
keyPath:
可以访问对象层次结构中的嵌套属性
多值操作
给定一组Key
,获得一组value
,以字典的形式返回。该方法为数组中的每个Key
调用valueForKey:
方法。
- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil
替换NSNull
对象
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
例子
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@property (nonatomic, strong) NSString *sex;@end
Person* person = [[Person alloc] init];NSDictionary* dictionary = @{@"name":@"fu",@"age":@66,@"sex":@"sex"};[person setValuesForKeysWithDictionary:dictionary];NSLog(@"name:%@",person.name);NSLog(@"age:%@",person.age);NSLog(@"sex:%@",person.sex);NSDictionary* dictionary1 = [person dictionaryWithValuesForKeys:@[@"name",@"age",@"sex"]];NSLog(@"Dictionary : %@", dictionary1);
输出
2024-07-29 21:24:40.266469+0800 KVC1.0[51192:2260944] model.name:fu
2024-07-29 21:24:40.266530+0800 KVC1.0[51192:2260944] model.age:66
2024-07-29 21:24:40.266572+0800 KVC1.0[51192:2260944] model.sex:sex
2024-07-29 21:24:40.266640+0800 KVC1.0[51192:2260944] tempModelDictionary : {age = 66;name = fu;sex = sex;
}
集合类型
FXPerson *person = [FXPerson new];// 赋值
ThreeFloats floats = {180.0, 180.0, 18.0};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"非对象类型%@", [person valueForKey:@"threeFloats"]);// 取值
ThreeFloats th;
NSValue *currentValue = [person valueForKey:@"threeFloats"];
[currentValue getValue:&th];
NSLog(@"非对象类型的值%f-%f-%f", th.x, th.y, th.z);
非对象处理
KVC支持基础数据类型和结构体,在使用KVC进行赋值或取值的时候,会自动在非对象值和对象值之间进行转换。
- 当进行取值如
valueForKey:
时,如果返回值非对象,会使用该值初始化一个NSNumber
(用于基础数据类型)或NSValue
(用于结构体)实例,然后返回该实例。 - 当进行赋值如
setValue:forKey:
时,如果key
的数据类型非对象,则会发送一条<type>Value
消息给value
对象以提取基础数据,然后赋值给key。
赋值setValue:forKey的原理
- 按照
setKey:
,_setKey:
的顺序查找方法,如果找到方法就传递参数,调用方法 - 如果没有找到。查看
accessInstanceVariablesDirectly
方法的返回值如果返回NO
,调用setValue:forUnderfinedKey
:并抛出异常 - 如果返回YES,按照
_key
,_iskey
,key
,iskey
的顺序查找成员变量。如果找到成员变量,就直接赋值 - 如果没找到,就调用
setValue:forUnderfinedKey:
并抛出异常
取值 valueForKey:的原理
1,按照getKey
、key
、isKey
、_key
的顺序查找方法,如果找到了,就直接调用
2,如果没找到,就查看accessInstanceVariablesDirectly
方法的返回值,如果返回NO
调用valueForUndefinedKey:
并抛出异常NSUnknownKeyException
3,YES 按照_key
、_isKey
、key
、isKey
的顺序查找成员变量
如果找到了成员变量,就直接取值。
4,如果没有查找到成员变量就调用valueForUndefinedKey:
并抛出异常NSUnknownKeyException