KVO
什么是KVO
KVO全称Key Value Observing,其是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。观察者模式
由于KVO的实现机制,只针对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO
KVO可以监听单个属性的变化,也可以监听集合对象的变化。集合对象包含NSArray和NSSet。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVC监听的方法。
KVO的基本使用
主要分三个步骤
-
通过addObserver:forKeyPath:options:context: 方法注册观察者 -
- observer:观察者,监听属性变化的对象。该对象必须必须实现
observeValueForKeyPath:ofObject:change:context: 方法。 -
- keyPath:要观察的属性名称。要和属性声明的名称一致
-
- options:回调方法里收到被观察的属性的旧值或新值,枚举类型,系统为我们提供了4个方法
-
-
NSKeyValueObservingOptionOld :change中会包含key变化之前的值old -
-
NSKeyValueObservingOptionNew :change中会包含key变化之后的值new -
-
NSKeyValueObservingOptionInitial :change中不包含key的值,会在kvo注册时候立即发通知 -
-
NSKeyValueObservingOptionPrior :会在值发生改变前发出一次通知,改变后通知依然发出,也就是每个change会有两个通知。值变化之前发送通知的 change 中包含notificationIsPrior = 1; 值发生变化之后的的通知 change 不包含上面提到的notificationIsPrior ,可以跟 willChange 手动通知搭配使用 -
-
- 我们也可以中间以竖线来进行多种选择
NSKeyValueObservingOptionOld |。NSKeyValueObservingOptionNew 这样change既有new又有old -
观察对象发生改变,回调方法observeValueForKeyPath:ofObject:change:context: -
-
-
- change:字典类型,存放相关的值,根据options传入的枚举来返回新值旧值或者noticationlsPrior = 1
-
- context:注册观察者时候context传入的值
-
当观察者不需要监听时,可以调用removeObserver:forKeyPath: 方法将KVO移除,我们需要在观察者消失之前进行处理,否则就crash了  -
-
手动调用KVO
KVO没法实现对数组元素内部的监听,此时就需要我们手动调用KVO
KVO在属性发生改变时的调用时自动的,如果想要手动控制这个调用时机,或想要自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
- 如果想要手抖调用或者自己实现KVO需要重写下面的方法。该方法返回YES表示允许系统自动调用KVO,NO表示不允许系统自动调用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"name"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
- 需要重写setter方法
- (void)setName:(NSString *)name {
if (name != _name) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
不过一般情况下 手动触发KVO感觉没有什么必要 这样会调用两次KVO的响应事件 所以我们不使用这两种方法 直接在需要的地方加will和did 手动触发 
KVO的本质
KVO是基于runtime机制实现的
在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指针指向中间类。并且将class方法重写,返回原类的class。
NSLog(@"类对象 -%@", object_getClass(self.person));
NSLog(@"方法实现 -%p", [self.person methodForSelector:@selector(setName:)]);
NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.person)));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"类对象 -%@", object_getClass(self.person));
NSLog(@"方法实现 -%p", [self.person methodForSelector:@selector(setName:)]);
NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.person)));
 我们添加KVO前后
- person指向的类对象和元类对象以及对应监听的属性的set方法都发生了改变
- 添加KVO后,person中的isa指向了
-NSKVONotifying_Person 类对象 - 添加KVO后,
setName: 的实现调用是:Foundation中_NSSetLongLongValueAndNotify 方法
isa-swizzling(类指针交换) 就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。

NSKVONotifying_Person内部实现
- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类
- (void)setName:(int)name {
}
- (Class)class {
return [LDPerson class];
}
- (void)dealloc {
}
- (BOOL)_isKVOA {
return YES;
}
isa混写之后如何调用方法
- 调用监听的属性设置方法,例如
setAge: ,都会先调用NSKVONotify_Person 对应的属性设置方法 - 调用非监听属性设置方法,如
test ,会通过NSKVONotify_Person 的superclass来找到Person类对象,再调用起Person test方法
为什么重写class方法
- 如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person,就会将该类暴露出来
setter的实现不同
截图中我们可以看到set方法的实现在调用KVO后变成调用_NSSetIntValueAndNotify 这样一个C函数 我们不知道其本身是什么样 不过我们可以进行测试
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}

- 先调用will
- 然后调用原来的setAge
- 最后调用did这个方法,并且通知监听者属性值已变,然后监听者执行observe这个方法
KVO部分相关问题
- KVO的本质是什么?
- 利用runtime的API动态生成一个子类,并让实例对象的isa指向这个全新的子类
- 当修改实例变量对象的属性时候,在全新子类的set方法中会调用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey
- 调用原来的setter
- didChangeValueForKey:内部会触发监听器的监听方法
- 手动触发KVO
上面有写 - 直接修改成员变量会触发KVO么?
不会触发KVO
 通过赋值语句直接打印两个数组的地址是一样的,这是因为我们只用了strong修饰,相当于指针拷贝,所有操作都是对于指针来说的 我们给其中一个设置KVO,修改数组中的值,此时数组地址发生改变,因为KVO的缘故,还是replaceObjectAtIndex这个方法的缘故?
经过测试 mutableArrayValueForKey 这一部分应该是导致触发KVO监听的过程 这个方法返回了一个新的数组,导致了原数组地址的改变,触发了KVO的监听
KVC
什么是KVC
定义在NSKeyValueCoding.h中,是一个非正式的协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量
在NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKey 和setter方法setValue:forKey ,以及其衍生的keyPath方法,这两个方法是各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。
基础操作
KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。 在使用KVC时,直接将属性名当作key,并设置value,即可对属性进行赋值

多级访问
除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如对当前对象的address属性的street属性进行赋值。
KVC进行多级访问时,类似于属性调用一样用点语法进行访问即可 myAccount setValue:@"qwe" forKeyPath@"address.street"

传参nil
如果对非对象传递一个nil值,KVC会调用setNIlValueForKey方法 我们可以重写这个方法来避免
处理非对象
- setValue时,如果要赋值的对象是基本类型,需要将值封装成NSNumber或者NSValue类型
- valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或者NSValue
valueForKey可以自动将值封装成对象,但是setValue:forKey: 却不行。
我们必须手动讲值类型转换成NSNumber/NSValue类型才能进行传递 initWithBool:(BOOL)value
KVC获取值的过程
我们在KVO里面已经遇到这个问题了 使用ValueForKey这一部分导致了触发KVO监听的过程
现在来详细学习一下
setValue:forKey

- 程序回先通过setter方法对属性进行设置
- 如果没有找到set方法,KVC机制会检查+(Bool)accessInstanceVariablesDirectly(直接访问实例变量)方法有没有返回YES(默认返回YES)
-
- 如果重写方法成了NO,调用-setValueForUndefinedKey:(为未定义项设置值)抛出异常
-
- 返回YES就去找成员变量并直接赋值,按照_key,_isKey,key,iskey的顺讯找,没找到就抛出异常
嫖一张爱尔兰提贝的图 
valueForKey

- 先后顺序搜索getKey、key 、isKey、_getKey、_key五个方法,若某一个方法被实现,取到的即是方法返回的值,后面的方法不再运行。如果是BOOL或者int等类型,会将其包装成一个NSNumber对象
 - 如果五个方法都没有,还是会访问
accessInstanceVariablesDirectly 方法有没有返回YES(该方法默认返回YES) -
-
- 返回YES就去找成员变量并取值,取值顺序为_key、_isKey、key、isKey
KVC操作使用场景
动态的取值和设值
利用KVC动态的取值和设值是最基本的用途
多值操作
KVC可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
同样,也可以通过KVC进行批量赋值。在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
NSDictionary *dic = @{@"name" : @"book", @"age" : @"66", @"sex" : @"male"};
StudentModel *model = [[StudentModel alloc] init];
[model setValuesForKeysWithDictionary:dic];
NSLog(@"%@",model);
NSDictionary *modelDic = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
NSLog(@"modelDic : %@", modelDic);

如果 model 属性和 dic 不匹配,可以重写方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"sex"]) {
self.studentSex = (NSString *)value;
}
}
用KVC来访问和修改私有变量
KVC的本质是操作方法列表以及在内存中查找实例变量。 我们可以利用这个特性访问类的私有变量。
同样如果不想让外界使用KVC的方法访问类的成员变量,可以将accessInstanceVariablesDirectly 属性设置为NO
修改一些控件的内部属性
很多UI控件都是由内部UI控件组合而成的,但是Apple中没有提供访问这些控件的API,这样我们就无法正常地访问和修改这些空间的样式。而KVC在大多数情况下可以解决这个问题
|