和谐学习!不急不躁!!我是你们的老朋友小青龙~
前言
前面文章,我们探究了KVC的原理,本篇文章就KVO进行一番探究。虽然KVC和KVO长得很像,但是大家不能记混淆哦~
KVC
:Key-Value CodingKVO
:Key-value observing
老规矩,先上KVO的案例:
KVO的使用很简单:
-
注册(
addObserver:forKeyPath:options:context:
) -
回调代理方法(
observeValueForKeyPath:ofObject:change:context:
) -
移除观察者(
removeObserver:forKeyPath:context:
,这个需要自己移除,不然就会因为向一个被释放的对象发送消息而发送异常)
原理探究
虽然大家都是这么用的,但是当我们想看具体实现的时候,发现不能点进去看下一步:
别急,这时候我们可以打开苹果爸爸
提供的KVO官方文档:
图片上大概意思是:
-
KVO提供了一种机制,它允许当其它对象的指定属性发生改变时,这个对象被通知到。
-
可以是控制器观察数据模型的值变化;
-
可以是模型数据A观察模型数据B的变化(比如模型B是模型A其中一个属性);
-
也可以是模型数据对自己本身的数据变化做监听; 关于第2点,文档上还给出一个案例:
Account
作为Person
的其中一个属性,它有余额和利率的属性,Person对象可以监听Account对象的余额和利率
变化情况。
当然,关于账户信息的变更,可以通过定时去查询
来知道信息的变化情况。但是这样做效率不高
,而且不够及时
。
这时候KVO
的出现就像是账户边上占了一个人,24小时盯着账户的情况,一旦发生改变
就立马发送一个通知
告诉你“兄嘚儿,你的账户发生变动了,变动信息是XXX”。
最后,在你不需要监听的时候,你要手动取消监听。
KVO的使用流程和注意事项
上图讲述了KVO的实现步骤(注册、回调代理、移除
):
-
addObserver:forKeyPath:options:context:
-
observeValueForKeyPath:ofObject:change:context:
-
removeObserver:forKeyPath:
需要注意
:并非所有类对所有属性都符合KVO。通过遵循KVO Compliance
中描述的步骤,您可以确保自己的类符合KVO。通常,苹果提供的框架中的属性只有在有文档记录的情况下才符合KVO。
关于 KVO Compliance
-
该类必须符合属性的键值编码,如确保KVC符合性中所述。KVO支持与KVC相同的数据类型,包括Objective-C对象以及标量和结构支持中列出的标量和结构。
-
该类发出属性的KVO更改通知。
-
相关密钥已正确注册(请参阅
Registering Dependent Keys
)。
查看 Registering Dependent Keys
在许多情况下,一个属性的值取决于另一个对象中一个或多个其他属性的值。如果某个属性的值发生更改,则派生属性的值也应标记为更改。如何确保为这些依赖属性发布键值观察通知取决于关系的基数。
关于addObserver:forKeyPath:options:context:
注册观察者对象以接收与接收此消息的对象相关的密钥路径的KVO通知。
参数Options
NSKeyValueObservingOptionNew
//只包含改变后的状态信息NSKeyValueObservingOptionOld
//只包含改变前的状态信息NSKeyValueObservingOptionInitial
//这种模式下,会走两遍observeValueForKeyPath代理,且不同步NSKeyValueObservingOptionPrior
//这种模式下,会走两遍observeValueForKeyPath代理,且同步
一般情况下,我们会使用NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld
,这两个大家比较熟悉了,这里不做阐述。
接下来测试一下NSKeyValueObservingOptionInitial
:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [SSJPerson new];
self.person.weight = @"50kg";
// 注册观察者
[self.person addObserver:self forKeyPath:@"weight" options:(NSKeyValueObservingOptionInitial) context:SSJPersonWeightContext];
__block typeof(self)blockSelf = self;
// 2秒后改变内容
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"开始改变~");
blockSelf.person.weight = @"52kg";
});
NSLog(@"\n\n~~~~~~~ 间隔 ~~~~~~~\n");
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"打印change:%@",change);
NSLog(@"演示结束~");
}
运行:
第一次进断点:
第二次进断点:
接下来测试一下NSKeyValueObservingOptionPrior
:
第一次进断点:
第二次进断点:
说明:
-
NSKeyValueObservingOptionInitial模式下,会在注册完观察模式后
立刻执行一次
代理方法,返回被观察者person
最初的状态,当观察属性发生变化
的时候,会再调用一次
,并返回改变后的状态。 -
NSKeyValueObservingOptionInitial模式下,
当观察属性发生变化
的时候,才会调用两次代理方法,第一次返回旧数据,第二次返回新数据。
参数Context
context
:上下文标识符,传递给观察者的任意数据。
一种更安全、更可扩展的方法是使用上下文context
来确保您收到的通知是针对您的观察者而不是超类的。简单来说,如果对同一个消息接收者观察了不同的SSJPerson对象的不同属性值变化,那么在observeValueForKeyPath里就需要进行多次判断才能找到那个需要处理的消息,这时候可以通过设置不同的context
作为唯一标识符
,可以更高效的定位不同的消息。比如:
SSJPerson *personA;
SSJPerson *personB;
static void *SSJPersonAWeightContext = &SSJPersonWeightContext;
static void *SSJPersonBNameContext = &SSJPersonBNameContext;;
- (void)viewDidLoad {
[super viewDidLoad];
personA = [SSJPerson new];
personB = [SSJPerson new];
[personA addObserver:self forKeyPath:@"weight" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:SSJPersonAWeightContext];
[personB addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:SSJPersonBNameContext];
personA.weight = @"12";
personB.name = @"王五";
}
参数observer、参数keyPath
-
observer
:即要注册KVO通知的对象
。注册观察之后,observer必须实现
observeValueForKeyPath:ofObject:change:context:代理方法
。 -
keyPath
:要观察的属性的键值对路径
,且不能为nil
。
关于 observeValueForKeyPath:ofObject:change:context:
观察的对象的指定关键点路径(keyPath)上的值发生更改时,通知观察的对象。
参数
-
keyPath
:键值对路径。 -
object
:被观察的对象。 -
change
:字典类型,包含关于keyPath指定路径值的变化情况。 -
context
:注册观察者的时候传进来的内容。
关于 removeObserver:forKeyPath:
通过向观察对象发送removeObserver:forKeyPath:context:message,指定观察对象、键路径和上下文,可以删除键值观察者。
- 如果尚未注册为观察员,则请求以观察员身份删除会导致nsrange异常。可以放在try/catch块中以处理潜在异常。
案例:
添加try/catche处理后,成功捕获错误信息:
- 取消分配时,观察者不会自动删除自身。被观察对象继续发送通知,而不理会观察者的状态。但是,与任何其他消息一样,发送到已发布对象的更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己移除。
案例:
创建了一个SSJPerson单例作为被观察者,并且把当前控制器设为观察者,第一次返回上一层,控制器销毁了;但是由于单里的原因,再加上没有没有remove第一次的观察者,被观察者会继续给这个被销毁的控制器发消息(改变的时候才会发,也就是第二次进入控制层,进入test03方法的时候,这时候它会给两个控制器发消息)。这边花了一个图方便大家理解:
- 协议没有提供询问对象是观察者还是被观察者的方法。构造代码以避免与发布相关的错误。典型的模式是在观察者初始化期间(例如在init或viewDidLoad中)注册为观察者,并在解除分配期间(通常在dealloc中)取消注册,以确保正确配对和有序的添加和删除消息,并确保观察者在从内存中释放之前取消注册。
按照上文所说,remove一个不存在的观察模式,会报异常
,官方给的建议是使用try/cache
避免程序因为异常信息而崩落。问题是,错误终究还是存在,我们如何去避免这种错误呢?或者说,能否提前知道可不可以remove呢
??
- 写个NSObject分类,替换系统的
addObserver:forKeyPath:options:context:
方法,在新方法里将obser
和keypath
保存到自己的单例里。remove之前先从单例里获取对应的信息,根据读取信息判断是否可以remove,remove
之后单例不要忘记移除对应数据
。
观察模式的几种应用场景
当张三的上班时间发生变化时,需要被通知到。
- 控制器监听数据模型的变化:
- 模型之间的监听
KVO的实现详情
翻译如下:
使用称为`isa swizzling`的技术实现自动键值观察。
顾名思义,isa指针指向维护分派表的对象类。这个分派表本质上包含指向类实现的方法的指针以及其他数据。
当一个观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
决不能依赖isa指针来确定类成员身份。相反,您应该使用class方法来确定对象实例的类。
验证isa
知否真的被修改:
查看添加注册之后self.person的isa
指向:
事实证明,经过addObserver这一步的注册,self.person的isa
由原来的指向SSJPerson
改为指向NSKVONotifying_SSJPerson
这个中间类。
中间类NSKVONotifying_SSJPerson
跟原来的SSJPerson
类有什么关系呢?
我们知道,isKindOfClass
可以判断某个对象是否属于某个类,或者这个类的子类。
经过上面的打印,我们发现NSKVONotifying_SSJPerson
是SSJPerson
的子类。
removeObserver之后,「isa」的指向是否会重新指向「SSJPerson」呢?原来的NSKVONotifying_SSJPerson是否已经销毁了呢?
继续控制台打印:
这边提供一个方法,可以获取运行时指定类的所有子类:
//获取指定类的子类
+ (NSArray *)findSubClass:(Class)defaultClass
{
//注册类的总数
int count = objc_getClassList(NULL,0);
//创建一个数组,其中包含给定对象
NSMutableArray * array = [NSMutableArray arrayWithObject:defaultClass];
//获取所有已注册的类
Class *classes = (Class *)malloc(sizeof(Class) * count);
objc_getClassList(classes, count);
//遍历
for (int i = 0; i < count; i++) {
if (defaultClass == class_getSuperclass(classes[i])) {
[array addObject:classes[i]];
}
}
free(classes);
return array;
}
dealloc里remove观察模式之后再看看:
说明经过addObserver这一步产生的中间类,并不会因为观察模式的移除而销毁。
我们发现
-
经过
addObserver:forKeyPath:options:context:
这一步,会让被观察对象的isa指向一个动态生成的中间类(继承自原来的类); -
经过
removeObserver:forKeyPath:context:
这一步,会让被观察对象的isa指向原来的类,且中间类并不会被销毁。
关于中间类
是原来就有,还是后面动态生成的,可以通过lldb打印一下:
继续打印:
说明NSKVONotifying_SSJPerson
类是在addObserver:forKeyPath:options:context:
之后生成的。
NSKVONotifying_SSJPerson类有哪些属性
// 遍历所有的属性
- (NSArray *)getAllProperties:(id)instanceOjb{
u_int count = 0;
//传递count的地址
objc_property_t *properties = class_copyPropertyList([instanceOjb class], &count);
NSMutableArray *propertyArray = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; i++) {
//得到的propertyName为C语言的字符串
const char *propertyName = property_getName(properties[i]);
[propertyArray addObject:[NSString stringWithUTF8String:propertyName]];
// NSLog(@"%@",[NSString stringWithUTF8String:propertyName]);
}
free(properties);
return propertyArray;
}
打印SSJPerson
实例对象的属性:
打印的NSKVONotifying_SSJPerson
实例对象属性:
NSKVONotifying_SSJPerson类有哪些方法
这边提供了一个打印类所有实例方法的函数:
//遍历所有的方法
//遍历所有的方法
- (NSArray *)gainMethodList:(id)instanceOjb
{
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList([instanceOjb class], &methodCount);
NSMutableArray *methodArray = [NSMutableArray arrayWithCapacity:methodCount];
for (int i = 0; i < methodCount; i++) {
Method temp = methodList[i];
SEL name_A = method_getName(temp);
const char *name_sel = sel_getName(name_A);
[methodArray addObject:[NSString stringWithUTF8String:name_sel]];
}
free(methodList);
return methodArray;
}
这里对
打印SSJPerson
类实例方法:
打印NSKVONotifying_SSJPerson
实例方法
class_copyMethodList
获取的是到底是自己本身的实例方法,还是从父类继承的方法?
从API的注释可以看出,「class_copyMethodList」返回的是自己本身实现的方法。
由此可见,作为子类的NSKVONotifying_SSJPerson
实例对象,重写了
父类SSJPerson
的实例方法。
中间类的setter方法做了什么事情
到目前为止,我们知道注册观察模式之后,被观察对象的isa会指向新的中间类(原来类的子类),这个中间类重写了原来类的setter方法,那么这个setter方法干了什么事情呢?
对属性和成员变量设置观察模式:
运行结果:
由此可知,观察模式只对属性生效,对成员变量无效。
接下来通过lldb符号断点查看底层的函数走向:
通过堆栈信息可以看到,经过103行对name的赋值
,会进行一系列的函数调用:
-
_NSSetObjectValueAndNotify
-
_changeValueForKey:key:key:usingBlock:
-
_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:
-
setName: 注意,
对name的赋值
实际上是对中间类的赋值,因为那时候的isa已经不再指向原来的类,而经过这一系列的函数,最终还是会调用一次原来类的setName:
方法。
那么通知回调observeValueForKeyPath:ofObject:change:context:
是什么时候发起的呢?
下断点:
我们发现它是自NSKeyValueDidChange
之后发起的。
梳理下KVO整个流程
部分代码
- (void)viewDidLoad {
...
// 创建SSJPerson的实例变量
self.person = [SSJPerson new];
// 注册观察模式
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:SSJPersonNameContext];
// 赋值
self.person.name = @"王大大";
}
// 观察模式回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
/** 具体业务代码 */
}
- (void)dealloc{
...
[self.person removeObserver:self forKeyPath:@"name" context:SSJPersonBFriendsNameArrayContext];
}
- 调用
addObserver:forKeyPath:options:context:
方法,
-
底层动态生成「NSKVONotifying_SSJPerson」类(是SSJPerson的子类),并且person的
isa
指向了「NSKVONotifying_SSJPerson」, -
NSKVONotifying_SSJPerson重写了「SSJPerson」类的实例方法(比如setter、getter等方法);
- 对属性值name进行赋值(本质上是对NSKVONotifying_SSJPerson的name赋值),会调用以下函数
-
_NSSetObjectValueAndNotify
-
_changeValueForKey:key:key:usingBlock:
-
_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:
(这里会调用NSKeyValueDidChange
,然后发起「observeValueForKeyPath:ofObject:change:context:」回调) -
setName:
(这一步会对SSJPerson的name进行赋值)
- 调用
removeObserver:forKeyPath:context:
移除观察模式
-
person的isa重新指向SSJPerson
-
NSKVONotifying_SSJPerson不会销毁,依旧在内存中