问题:
1、KVO的使用?实现原理?(为什么要创建子类来实现)
2、KVC的使用?实现原理?(KVC拿到key以后,是如何赋值的?知不知道集合操作符,能不能访问私有属性,能不能直接访问_ivar)
3、问setter、getter,比如@property关键字,如果setter、getter是在什么时候调用都不知道,那更别谈kvc了。
附3篇参考文章
文章1
、如何优雅地使用 KVO
写了一个Demo简单介绍了KVO的使用,并引出 Facebook 开源的 KVOController框架
注:
使用FBKVOController有一个坑,自己监听自己的属性时会形成循环引用。如:[self.kvoController observe:self ......] (self->_objectInfosMap->self)。 如果换成 [self.KVOControllerNonRetaining obseve:self ......]不会循环引用,但是又会造成在self这个对象释放->引发kvoControlloer释放时,kvoController取内部的_objectInfosMap取到的观察者为nil, 没法取消kvo观察,导致崩溃。
文章2
、Objective-C中的KVC和KVO
玉令天下的博客,写的蛮不错的。
文章3
、iOS底层原理总结 - 探寻KVO本质
文中多处的实验验证还是挺不错的。
KVO的两个方法介绍:
1. 添加监听
通过以下方法添加一个监听者:
-(void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
我们重点关注一下这个方法的4个参数:
observer:
就是要添加的监听者对象,当监听的属性发生改变时就会去通知该对象,
该对象必须实现 -observeValueForKeyPath:ofObject:change:context: 方法,
要不然程序会抛出异常。
keyPath:
就是要被监听的属性,这里和KVC的规则一样。但是这个值不能传nil,要不然会报错。
通常我们在用的时候会传一个与属性同名的字符串,但是这样可能会因为拼写错误,导致监听不成功,
一个推荐的做法是,用这种方式NSStringFromSelector(@selector(propertyName)),
其实就是是将属性的getter方法转换成了字符串,这样做的好处就是,如果你写错了属性名,
xcode会用警告提醒你。
options:
是一些配置选项,用来指明通知发出的时机和通知响应方法
-observeValueForKeyPath:ofObject:change:context:
的change字典中包含哪些值,它的取值有4个,定义在NSKeyValueObservingOptions中,
可以用|符号连接,如下:
1. NSKeyValueObservingOptionNew:
指明接受通知方法参数中的change字典中应该包含改变后的新值。
2. NSKeyValueObservingOptionOld:
指明接受通知方法参数中的change字典中应该包含改变前的旧值。
3. NSKeyValueObservingOptionInitial:
当指定了这个选项时,在addObserver:forKeyPath:options:context:消息被发出去后,
甚至不用等待这个消息返回,监听者对象会马上收到一个通知。
这种通知只会发送一次,你可以利用这种“一次性“的通知来确定要监听属性的初始值。
4. NSKeyValueObservingOptionPrior:当指定了这个选项时,在被监听的属性被改变前,
监听者对象就会收到一个通知(一般的通知发出时机都是在属性改变后,
虽然change字典中包含了新值和旧值,但是通知还是在属性改变后才发出),
这个通知会包含一个NSKeyValueChangeNotificationIsPriorKeykey,
其对应的值为一个NSNumber类型的YES。
context:
添加监听方法的最后一个参数,是一个可选的参数,可以传任何数据,
这个参数最后会被传到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。
如果你要用context来区分不同的通知,一个推荐的做法是声明一个静态变量,其保持它自己的地址,
这个变量没有什么意义,但是却能起到区分的作用,如下:
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
2. 接受通知
前面说过了,每一个监听者对象都必须实现下面这个方法来接收通知:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
keyPath,object,context和监听方法中指定的一样,
关于change参数,它是一个字典,有五个常量作为它的键:
NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
分析下:
NSKeyValueChangeKindKey:
指明了变更的类型,值为“NSKeyValueChange”枚举中的某一个,类型为NSNumber。
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
一般情况下返回的都是1也就是第一个NSKeyValueChangeSetting,
但是如果你监听的属性是一个集合对象的话,当这个集合中的元素被插入,删除,替换时,
就会分别返回NSKeyValueChangeInsertion,NSKeyValueChangeRemoval和NSKeyValueChangeReplacement。
NSKeyValueChangeNewKey:
被监听属性改变后新值的key,当监听属性为一个集合对象,
且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)。
NSKeyValueChangeOldKey:
被监听属性改变前旧值的key,当监听属性为一个集合对象,
且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)
NSKeyValueChangeIndexesKey:
如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion,
NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,
这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。
NSKeyValueChangeNotificationIsPriorKey:
如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,
change字典中就会带有这个key,值为NSNumber类型的YES.
最后,完整的change字典大概就类似这样:
NSDictionary *change = @{
NSKeyValueChangeKindKey : NSKeyValueChange(枚举值),
NSKeyValueChangeNewKey : newValue,
NSKeyValueChangeOldKey : oldValue,
NSKeyValueChangeIndexesKey : @[NSIndexSet, NSIndexSet],
NSKeyValueChangeNotificationIsPriorKey : @1,
};
3、当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。
需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
注意点
1、KVO的addObserver和removeObserver需要是成对的,
如果重复remove则会导致NSRangeException类型的Crash,
保险的做法是,把remove操作放在try/catch中。
如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
2、苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,
这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
4. KVO内部实现原理
1、KVO是通过isa-swizzling(黑魔法)技术实现的
2、当某个类的属性对象第一次被观察(addObserver)时,系统就会在运行期动态地创建该类的一个派生类,
在这个派生类中重写基类中任何被观察属性的setter 方法。
派生类在被重写的setter方法内实现真正的通知机制
3、如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
4、每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,
那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
5、键值观察通知依赖于NSObject 的两个方法:
willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前,
willChangeValueForKey:一定会被调用,这就会记录旧的值。
而当改变发生后,didChangeValueForKey:会被调用,
继而 observeValueForKey:ofObject:change:context: 也会被调用。
6、KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,
从而达到隐藏生成的派生类
5. _NSsetIntValueAndNotify
NSKVONotifyin_Person中的setage方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。
完。
网友评论