文章目录
- KVC
- 1. KVC赋值原理 setValue:forKey:
- 2. KVC取值原理 valueForKey:
- 3. 注意
- 4. KVC的批量存值和取值
- KVO 使用
- 1. KVO的介绍
- 2. KVO监听的步骤
- 注册监听
- 监听实现
- 移除监听
- 例子
- 3. KVO的传值
- 4. KVO注意
- 5. KVO的使用场景
- KVO原理
- 1. KVO的本质是改变了setter方法的调用
- 2. _NSSetXXXValueAndNotify的内部实现原理
- 3. _NSSetObjectValueAndNotify
- 4. 手动调用KVO
- 5. KVO的实现的注意
- 6. KVO类指针交换
- KVO总结
- 1. KVO的本质
- 2. KVO实现过程总结
- KVO & KVC 问题总结
- 3.1 isa混写之后如何调用方法?
- 3.2 为什么在生成的子类内部重写class方法
- 3.3 直接修改成员变量的值,会不会触发KVO?
- 3.4 KVC修改属性会触发KVO吗?
- 3.5 KVO怎么监听数组的元素变化?
KVC
1. KVC赋值原理 setValue:forKey:
- 首先会按照
setKey
、_setKey
的顺序查找方法,找到方法,直接调用方法并赋值; - 未找到方法,则调用
+ (BOOL)accessInstanceVariablesDirectly(
是否可以直接访问成员变量,默认返回YES); - 若
accessInstanceVariablesDirectly
方法返回YES
,则按照_key
、_isKey
、key
、isKey
的顺序查找成员变量,找到直接赋值,找不到则抛出NSUnknowKeyExpection
异常; - 若
accessInstanceVariablesDirectly
方法返回NO
,那么就会调用setValue:forUndefinedKey:
并抛出NSUnknowKeyExpection
异常;
2. KVC取值原理 valueForKey:
- 首先会按照
getKey
、key
、isKey
、_key
的顺序查找方法,找到直接调用取值 - 若未找到,则查看
+ (BOOL)accessInstanceVariablesDirectly
的返回值,若返回NO
,则直接抛出NSUnknowKeyExpection
异常; - 若返回的
YES
,则按照_key
、_isKey
、key
、isKey
的顺序查找成员变量,找到则取值; - 找不到则调用
valueForUndefinedKey
:抛出NSUnknowKeyExpection
异常;
3. 注意
key
的值必须正确,如果拼写错误,会出现异常。- 当
key
的值是没有定义的,valueForUndefinedKey
:这个方法会被调用,如果你自己写了这个方法,key
的值出错就会调用到这里来。 - 因为类可以反复嵌套,所以有个
keyPath
的概念,keyPath
就是用.
号来把一个一个key链接起来,这样就可以根据这个路径访问下去。 NSArray
/NSSet
等都支持KVC
。- 可以通过
KVC
访问自定义类型的私有成员。 - 如果对非对象传递一个
nil
值,KVC
会调用setNIlValueForKey
方法,我们可以重写这个方法来避免传递nil出现的错误,对象并不会调用这个方法,而是会直接报错。 - 处理非对象,
setValue
时,如果要赋值的对象是基本类型,需要将值封装成NSNumber
或者NSValue
类型,valueForKey
时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber
或者NSValue
。valueForKey
可以自动将值封装成对象,但是setValue:forKey:
却不行。我们必须手动讲值类型转换成NSNumber/NSValue
类型才能进行传递initWithBool:(BOOL)value。
4. KVC的批量存值和取值
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *sex;
@property (nonatomic, strong) NSString *age;
@end
// KVC批量赋值
- (void)kvc_setKeys {
NSDictionary *kvc_dict = @{@"name": @"clearlove", @"sex": @"male", @"pr_age": @"21"};
Person *pr = [[Person alloc] init];
[pr setValuesForKeysWithDictionary:kvc_dict];
NSLog(@"%@", pr.age);
}
// KVC批量取值
- (void)kvc_getValues {
Person *pr = [[Person alloc] init];
[pr setValue:@"mekio" forKey:@"name"];
[pr setValue:@"male" forKey:@"sex"];
[pr setValue:@"120" forKey:@"pr_age"];
NSDictionary *pr_dict = [pr dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
NSLog(@"%@", pr_dict);
}
如果有取值或者赋值的时候有key和属性不对应,重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法
(上面代码是已经重写的部分
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"pr_age"]) {
self.age = (NSString *)value;
}
}
KVO 使用
1. KVO的介绍
KVO
的全称是Key Value Observing
: 键值监听,可以用于监听某个对象属性值的改变;
KVO
时苹果提供的一套事件通知机制, KVO
和NSNotificationCenter
都是iOS观察者模式的实现。
区别: NSNotificaionCenter
可以存在一对多,而KVO则是 一对一的 关系。
2. KVO监听的步骤
注册监听
通过[addObserver:forKeyPath:options:context:]
方法注册KVO,这样可以接收到keyPath属性的变化事件;
observer
:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。
keyPath
:要观察的属性名称。要和属性声明的名称一致。
options
:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
context
:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。
监听实现
通过方法[observeValueForKeyPath:ofObject:change:context:]
实现KVO的监听;
keyPath
:被观察对象的属性
object
:被观察的对象
change
:字典,存放相关的值,根据options传入的枚举来返回新值旧值
context
:注册观察者的时候,context传递过来的值
移除监听
在不需要监听的时候,通过方法[removeObserver:forKeyPath:]
,移除监听;
例子
- 在view实现一个button监听person的name变化。
// kvo监听
- (void)kvo_pr {
NSDictionary *kvc_dict = @{@"name": @"clearlove", @"sex": @"male", @"pr_age": @"21"};
self.pr_1 = [[Person alloc] init];
[self.pr_1 setValuesForKeysWithDictionary:kvc_dict];
[self.pr_1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"new_name == %@", [change valueForKey:@"new"]);
}
- (void)dealloc {
[self.pr_1 removeObserver:self forKeyPath:@"name"];
}
- 如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用相关对象的监听事件,如果返回NO则表示不可以调用相关对象的监听事件。
- 如果name不可以被监听,在persn的实现重写
(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法
// 禁止监听某个属性
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
NSLog(@"forbid Nostifity pr.name");
return NO;
}
return [super automaticallyNotifiesObserversForKey: key];
}
3. KVO的传值
- 可以通过方法
context
传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。 - 通过KVO在
Model
和Controller
之间进行通信。
4. KVO注意
- 调用
[removeObserver:forKeyPath:]
需要在观察者消失之前,否则会导致Crash。 - 在调用
addObserver
方法后,KVO
并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。 - 观察者需要实现
observeValueForKeyPath:ofObject:change:context:
方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。 - KVO的
addObserver
和removeObserver
需要是成对的,如果重复remove
则会导致NSRangeException
类型的Crash,如果忘记remove
则会在观察者释放后再次接收到KVO
回调时Crash。
5. KVO的使用场景
- 对于时刻变化的对象,例如
colletionView
的items
,总是动态的变化,这个时候可以使用KVO
监听对象。 - 在
AVFounditon
中获取AVPlayer
的播放进度,播放状态,也需要使用KVO来观察。
KVO原理
1. KVO的本质是改变了setter方法的调用
- 利用Runtime API动态生成一个子类
NSKVONotifying_XXX
,并且让instance
对象的isa指向这个全新的子类,NSKVONotifying_XXX
的superclass
指针指向原来的类; - 当修改
instance
对象的属性时,会调用Foundation
的_NSSetXXXValueAndNotify
函数;
2. _NSSetXXXValueAndNotify的内部实现原理
- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类
- (void)setClassName:(NSString *)className {
}
- (Class)class {
- 这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
return [testClass class];
}
- (void)dealloc {
// 收尾工作
}
- (BOOL)_isKVOA {
- 添加一个名为_isKVOA的实例变量**,用于标识该对象是否支持KVO机制。
return YES;
}
重写Class方法 :这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
添加一个名为_isKVOA的实例变量,用于标识该对象是否支持KVO机制。
3. _NSSetObjectValueAndNotify
在具体实现过程中,系统会动态生成一个继承自原始类的中间类_NSSetXXXValueAndNotify
,并且在该类的初始化方法中,调用了一个叫做_NSSetObjectValueAndNotify
()的函数,用于实现属性改变的通知。
_NSSetObjectValueAndNotify
()函数的实现过程如下:
a) 首先会调用 willChangeValueForKey
b) 调用原来的setter实现然后给属性赋值
c) 最后调用 didChangeValueForKey
d) 最后调用 observer
的 observeValueForKeyPath
去告诉监听器属性值发生了改变 .
4. 手动调用KVO
- 修改类方法
automaticallyNotifiesObserversForKey
的返回值; - 调用KVO主要依靠两个方法,在属性发生改变之前调用
willChangeValueForKey
方法,在发生改变之后调用didChangeValueForKey
方法即可;
5. KVO的实现的注意
- 当观对象移除所有的监听后,会将观察对象的isa指向原来的类。
- 当观察对象的监听全部移除后,动态生成的类不会注销,而是留在下次观察的时候在用,避免反复创建中间子类。
6. KVO类指针交换
- isa-swizzling(类指针交换)就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。
KVO总结
1. KVO的本质
- 利用
RuntimeAPI
动态生成一个子类,并且让instance
对象的isa
指向这个全新的子类 - 当修改
instance
对象的属性时,会调用Foundation
的_NSSetXXXValueAndNotify
函数 - 接着调用父类原来的setter方法修改属性的值。
- 最后调用
didChangeValueForKey
,其内部会触发监听器(Oberser)的监听方法(observerValueForKeyPath:ofObject:change:context:);
2. KVO实现过程总结
- 调用
addObserver:forKeyPath:options:context:context
调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class
,并且将该对象的isa指针指向这个新的类。 - 在该子类内部实现4个方法-被观察属性的
set
方法、class
方法、isKVO
、delloc
。 - 最关键的是set方法中,先调用
willChangeValueForKey
,再调用原来的setter方法给成员变量赋值,最后调用didChangeValueForKey
。
willChangeValueForKey和didChangeValueForKey
需要成对出现才能生效,在didChangeValueForKey
中会去调用观察者的observeValueForKeyPath: ofObject:
方法。
- 重写
class
方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass
判断的时候出错。 isKVO
方法作为能否实现KVO
功能的一个标识。delloc
里面还原isa
指针
KVO & KVC 问题总结
3.1 isa混写之后如何调用方法?
- 调用监听的属性设置方法,例如
setAge
:,都会先调用NSKVONotify_Person
对应的属性设置方法。 - 调用非监听属性设置方法,如
test
,会通过NSKVONotify_Person
的superclass
来找到Person
类对象,再调用起[Person test]
方法。
3.2 为什么在生成的子类内部重写class方法
如果没有重写class方法,当对象调用class
方法的时候,会在自己的方法缓存列表、方法列表、父类缓存、方法列表一直向上去查找该方法,因为class
方法是NSObject
中的方法,如果不重写最终可能会返回NSKVONotifying_Person
,就会将该类暴露出来。
3.3 直接修改成员变量的值,会不会触发KVO?
不会触发KVO,KVO的本质是替换了setter方法的实现,所以只有通过set方法修改才会触发KVO。
3.4 KVC修改属性会触发KVO吗?
会的 ,尽管setvalue:forkey:
方法不一定会触发instance
实例对象的setter
:方法,但是setvalue:forkey:
在更改成员变量值的时候,会手动调用willchangevalueforkey
、didchangevalueforkey
,会触发监听器的回调方法。
3.5 KVO怎么监听数组的元素变化?
- KVO本来只能监听数组长度的变化,不能监听内部对象的变化,我们可以手动KVC修改数组内部的元素达到目的。
我们可以通过KVC来对数组进行添加元素的操作,这样就可以监听到了。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。