使用方法
注册Observer
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
options参数:
NSKeyValueObservingOptionNew:接收方法的change参数的中包含更改后的值
NSKeyValueObservingOptionOld:接收方法的change参数的中包含旧的值
NSKeyValueObservingOptionInitial:注册的时候发一次通知,改变后也发送一次通知
NSKeyValueObservingOptionPrior:属性改变之前发一次,改变之后再发一次
触发方法
- 直接调用setter方法,或者通过属性的点语法间接调用;
- 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
- 通过mutableArrayValueForKey:K方法获取到数组代理对象,并使用代理对象进行操作(注意:直接增删是不会触发的,当数组属性添加了一个观察者之后,通过mutableArrayValueForKey获取的代理数组实际类型为NSKeyValueNotifyingMutableArray)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
-
change:在注册时用options参数进行的配置,会包含不同的内容,其中kind关键字的值含义如下
kind = 1, 赋值 kind = 2, 插入kind= 3, 移除 kind = 4, 替换
延伸
I. kvo自定义结构体怎么取值?
答:当监听属性是结构体时,可定义一个结构体类型并把它的地址传进去getValue方法里
II. id是什么,和nsobject的区别?
答:id是动态数据类型,而NSObject *是静态数据类型,默认情况下所有的数据类型都是静态。id类型的实例在编译阶段不会做类型检查,会在运行时确定,而类NSObject的实例在编译期要做编译检查,保证指针指向是其NSObject类或其子类,当然,实例的具体类型也要在运行时才能确定,这也就是iOS三大特性之一的多态。
静态类型在编译时就知道变量的类型,编译时就知道变量的类型,在编译的时候就可以访问对象的属性和方法,如果访问了不属于静态类型的属性和方法,那么编译器就会报错,而动态数据类型在编译的时候并不知道变量的真实类型,只有在运行时的时候才知道它的真实类型,因此编译时候如果访问了不属于动态类型的属性和方法,编译器不会报错,导致运行时的错误,这也是动态数据类型的弊端。 -
实际上注册后,KVO默认会自动通知观察者,但其实也可以手动触发,首先在被观察的类中重写下面的方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ( [key isEqualToString:@"value"] ) {
return NO;
}
return YES;
}
在满足某些条件我们在触发调用接收方法,将条件代码包装下面任意一对方法中间。
- (will/did)ChangeValueForKey:
- (will/did)ChangeValueForKey:withSetMutation:usingObjects:
- (will/did)Change:valuesAtIndexes:forKey:
使用注意点
- 添加观察者几次,也要保证移除观察者几次
[m1 addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNew context:@""];
[m1 addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNew context:@""];
如果重复给一个对象多次添加相同的观察者,那么当属性发生改变时也会多次调用接收方法,同时在被观察者被销毁前也要移除同样的次数,否则在低版本系统下会崩溃。
原因在于iOS9之前NotificationCenter.default对self是unsafe_unretained引用,当self释放后,NotificationCenter.default持有的self并不会自动置为nil,而变成了一个野指针,这样再给self发送通知的话就会造成崩溃(给野指针发送消息, iOS9之后对于普通的添加观察者的方法不需要手动移除观察者self,因为iOS9之后NotificationCenter.default对self是weak引用,当self释放后,NotificationCenter.default持有的self会自动置为nil,而给一个nil发送推送的时候是不会发生崩溃的。
- 添加观察者和删除观察者以及各自的KeyPath要一一对应
[m1 removeObserver:self.m2 forKeyPath:@"value" context:nil];
[m1 addObserver:m3 forKeyPath:@"value" options:options context:@""];
[m1 removeObserver:m2 forKeyPath:@"value" context:nil];
[m1 addObserver:m2 forKeyPath:@"v" options:options context:@""];
[m1 removeObserver:m2 forKeyPath:@"value" context:nil];
以上几种情况程序都会崩溃。
实现原理
- 创建MYObject,然后添加一个value属性;
- 创建两个MYObject类的实例对象,然后其中为m1对象添加观察者,然后观察两个对象的实际所属类
self.m1 = [MYObject new];
self.m1.value = 1;
self.m2 = [MYObject new];
self.m2.value = 2;
NSLog(@"m1的类型:%@,m2的类型:%@",object_getClass(_m1),object_getClass(_m2));
[_m1 addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNewcontext:@""];
NSLog(@"m1的类型:%@,m1的父类:%@, m2的类型:%@",object_getClass(_m1),
class_getSuperclass(object_getClass(_m1)),object_getClass(_m2));
打印结果
从打印结果可以看到,m1对象未添加观察者前,m1和m2都是MYObject类的对象,添加之后,m1变成了NSKVONotifying_MYObject类的对象,并且它是MYObject的子类。
下面观察m1和m2的setValue:方法分别做了什么,通过methodForSelector获取方法的地址,再用IMP把地址转化成实际的方法名:
[_m1 methodForSelector:@selector(setValue:)] 0x10b826cb3
[_m2 methodForSelector:@selector(setValue:)] 0x10a6b9d30
方法名
可以看到NSKVONotifying_MYObject类的setter方法是Foundation框架中的_NSSetIntValueAndNotify函数,它的内部实现过程大致如下:
- (void)setValue:(int)value
{
_NSSetIntValueAndNotify();
}
void _NSSetIntValueAndNotify(){
[self willChangeValueForKey:@"value"];
[super setValue:value];
[self didChangeValueForKey:@"value"];
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
3.NSKVONotifying_MYObject类中的方法
利用runtime打印一下类中的方法
unsigned int outCount = 0;
Method *methods = class_copyMethodList(object_getClass(self.m1), &outCount);
for (int i = 0; i < outCount; ++i) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
NSLog(@"%@", name);
}
free(methods);
打印结果:
image.png
这四个方法的含义分别是:
setValue: 实现值改变时的通知效果
class:隐藏NSKVONotifying_MYObject类,直接返回他的父类。
- (Class)class
{
return class_getSuperclass(object_getClass(self));
}
dealloc:runtime在实例对象添加了KVO之后动态创建了类和一些对象,所以可能会在dealloc中回收这些资源。
_isKVOA: 是否使用了KVO。
- 注意点 - KVO对生成的中间类的格式是有要求的,默认都是以NSKVONotifying_<class>来命名,那如果我们不小心自己创建了一个一样名字的中间类,KVO就会无法使用。 image.png
网友评论