KVO的相关知识(笔记)

2022-04-13  本文已影响0人  我家冰箱养企鹅

使用方法

注册Observer

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath 
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

options参数:
NSKeyValueObservingOptionNew:接收方法的change参数的中包含更改后的值
NSKeyValueObservingOptionOld:接收方法的change参数的中包含旧的值
NSKeyValueObservingOptionInitial:注册的时候发一次通知,改变后也发送一次通知
NSKeyValueObservingOptionPrior:属性改变之前发一次,改变之后再发一次

触发方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
  1. change:在注册时用options参数进行的配置,会包含不同的内容,其中kind关键字的值含义如下
    kind = 1, 赋值 kind = 2, 插入kind= 3, 移除 kind = 4, 替换
    延伸
    I. kvo自定义结构体怎么取值?
    答:当监听属性是结构体时,可定义一个结构体类型并把它的地址传进去getValue方法里
    II. id是什么,和nsobject的区别?
    答:id是动态数据类型,而NSObject *是静态数据类型,默认情况下所有的数据类型都是静态。id类型的实例在编译阶段不会做类型检查,会在运行时确定,而类NSObject的实例在编译期要做编译检查,保证指针指向是其NSObject类或其子类,当然,实例的具体类型也要在运行时才能确定,这也就是iOS三大特性之一的多态。
    静态类型在编译时就知道变量的类型,编译时就知道变量的类型,在编译的时候就可以访问对象的属性和方法,如果访问了不属于静态类型的属性和方法,那么编译器就会报错,而动态数据类型在编译的时候并不知道变量的真实类型,只有在运行时的时候才知道它的真实类型,因此编译时候如果访问了不属于动态类型的属性和方法,编译器不会报错,导致运行时的错误,这也是动态数据类型的弊端。

  2. 实际上注册后,KVO默认会自动通知观察者,但其实也可以手动触发,首先在被观察的类中重写下面的方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ( [key isEqualToString:@"value"] ) {
        return NO;
    }
    return YES;
}

在满足某些条件我们在触发调用接收方法,将条件代码包装下面任意一对方法中间。

使用注意点

  1. 添加观察者几次,也要保证移除观察者几次
[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发送推送的时候是不会发生崩溃的。

  1. 添加观察者和删除观察者以及各自的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];

以上几种情况程序都会崩溃。

实现原理

  1. 创建MYObject,然后添加一个value属性;
  2. 创建两个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。

  1. 注意点 - KVO对生成的中间类的格式是有要求的,默认都是以NSKVONotifying_<class>来命名,那如果我们不小心自己创建了一个一样名字的中间类,KVO就会无法使用。 image.png
上一篇 下一篇

猜你喜欢

热点阅读