KVO探究

967 阅读5分钟

KVO(Key-value observing)即:键值观察,当注册的键值发生改变时,会给予反馈回调

KVO使用介绍

常用的使用方法如下所示:

//给person对象的name属性添加监听
[self.student addObserver:self
                   forKeyPath:@"name"
                      options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
                      context:NULL];
//系统监听的固定回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context{
    NSLog(change, context);
}

addObserver

addObserver方法给被监听者添加观察者对象,其中有四个参数,如下所示

	//由该对象观察指定对象(例如上面的student对象)
- (void)addObserver:(NSObject *)observer

	//观察的指定对象(student)的指定属性名称
	forKeyPath:(NSString *)keyPath
    
    //键值观察的枚举类型
    //NSKeyValueObservingOptionNew 把更改之前的值提供给处理方法
    //NSKeyValueObservingOptionOld 把更改之后的值提供给处理方法
    //NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
    //NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。
    options:(NSKeyValueObservingOptions)options
    
    这个context平时用的比较少,但不代表他不重要,例如:当两个同类对象(两个student)同时被该对象观察时,可以通过该参数区分
    context:(nullable void *)context

通过上面也可以了解到,一个对象可以有多个被观察者,同时一个被观察者也可以观察多个对象

observeValueForKeyPath

observeValueForKeyPath为触发监听的回调,具体为给属性赋值,即调用set方法时

//监听的键值
- (void)observeValueForKeyPath:(NSString *)keyPath
					  //监听的对象
                      ofObject:(id)object 
                      	//回调的字典参数,新老数据都在里面
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                        //传入的上下文备用参数context,void*类型,在回调会被带回,可以通过__bridge 转化成object类型
                       context:(void *)context{

automaticallyNotifiesObserversForKey

这个方法是在被观察者类中实现的,是对键值监听进行控制的,相当于一个开关,默认为YES。

返回为YES时,开放对应键值监听;当返回为NO时,则对应键值关闭监听,关闭时意味着addObserver后也不会触发监听回调

实现如下所示:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO; //此时不观察为name的属性
    }
    return YES;
}

willChangeValueForKey、didChangeValueForKey

当automaticallyNotifiesObserversForKey方法返回为NO时,无法触发回调,可是有些键值仍然需要监听,因此可以重写set方法,只需要在键值赋值的前后分别加上willChangeValueForKey、didChangeValueForKey方法,那么该键值属性更改时,仍然可以触发监听回调

实现如下所示:

- (void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

removeObserver

当我们使用完毕监听,在观察者释放时必须要及时移除监听,实现如下

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}

如果没有移除观察者,则会发生不可预料的问题:

例如:当被观察者为观察者的属性(或者观察者持有被观察者)时,此时及时没有释放也没有问题,对象都被释放了,不会出现问题;当被观察不是被当前观察对象持有(假设为单例)时,此时如果不主动移除,该监听的被观察者对象消失时,由于被观察者仍然存在,仍然会回调该方法,则可能导致野指针调用造成崩溃等,此时如果该对象由于某些原因没有正常释放,那么仍然后台回调,则可能出现不该有的回调

因此,观察者的移除则非常的有比较

引申

而比较出名的FBKVOController,则是根据被观察者与观察者的多对多的关系,合理利用被观察者对象及其属性的生命周期,实现被观察者销毁自动移除监听功能

系统KVO实现原理探究

假设我们定义的类为LSPerson,那么当某一次忘记移除观察者时会崩溃,此时崩溃到了一个叫NSKVONotifying_LSPerson的类,这个类就是我们添加观察者的时候,系统给我们处理的类

下面我们验证一下NSKVONotifying_LSPerson与LSPerson之间的关系,看看系统做了什么

给person添加监听,查看所属类,打印为self.person的class

self.person = [[LSPerson alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

NSLog("%@ -- %@", [self.person class], object_getClass(self.person)); //查看所属类

下面分别在添加观察之间分别打印LSPerson、NSKVONotifying_LSPerson类的信息,然后在观察之后打印NSKVONotifying_LSPerson的信息

unsigned int count = 0;
//获取所有的方法列表,遍历查看方法的名称和实现地址是否发生改变
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
    Method method = methodList[i];
    SEL sel = method_getName(method);
    IMP imp = class_getMethodImplementation(cls, sel);
    NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}

//	注册类的总数
int count = objc_getClassList(NULL, 0);
// 	创建一个结果数组, 其中包含指定对象和父类
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
//	保存所有的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
遍历所有的类查看父类是否是指定类LSPerson,并打印
for (int i = 0; i<count; i++) {
    if (cls == class_getSuperclass(classes[i])) {
        [mArray addObject:classes[i]];
    }
}
NSLog(@"classes = %@", mArray);

通过上面测试,我们了解到了:

NSKVONotifying_LSPerson为LSPerson的子类,该类的所属类指向了父类NSKVONotifying_LSPerson

NSKVONotifying_LSPerson重写了set的imp方法

重写了NSKVONotifying_LSPerson的class方法,返回为LSPerson

最后

可以模仿系统的一些逻辑来实现自定义KVO,会发现根本不需要担心移除观察者的问题

1.创建子类,将该对象的isa指向新创建的子类

2.重写class,指向父类(即当前对象的class)

3.重写指定键值的set的imp方法,实现监听回调

4.释放时,该对象指向当前类(即释放类的父类),避免不必要的麻烦