阅读 181

KVO(NSKeyValueObserving)

  • (一) KVO定义

KVO:NSKeyValueObserving,键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。

  • (二) KVO三部曲

1)注册观察者

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior context:@"context"];
参数一:observer:观察者,也就是KVO通知的订阅者。必须实现 observeValueForKeyPath
参数二:keyPath:描述将要观察的属性,相对于被观察者。
参数三:options:KVO的一些属性配置;有四个选项。
    NSKeyValueObservingOptionNew:change字典包括改变后的值
    NSKeyValueObservingOptionOld:change字典包括改变前的值
    NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
    NSKeyValueObservingOptionPrior:值改变前也要通知change字典包括(notificationIsPrior = 1 和旧值);
参数四:context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。
复制代码

2)观察者通知

每当key发生变化时,都会触发这个毁掉。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"keyPath:%@\nobject:%@\nchange:%@\ncontext:%@",keyPath,object,change,context);
}
复制代码

3)注销观察者

- (void)dealloc
{
    [self removeObserver:self.person forKeyPath:@"name"];
}
复制代码
  • (三) KVO底层实现

键值观察编程指南

自动键值观察是使用称为isa-swizzling的技术实现的。
该isa指针,顾名思义,指向对象的类,它保持一个调度表。该分派表实质上包含指向该类实现的方法的指针以及其他数据。
在为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,isa指针的值不一定反映实例的实际类。
您永远不要依靠isa指针来确定类成员。相反,您应该使用该class方法确定对象实例的类。
复制代码

首先我们先来验证一下,是否存在中间类:

NSLog(@"监听之前类:%s - 内存地址:%p",object_getClassName(self.person),[self.person methodForSelector:@selector(setName:)]);
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"context"];
NSLog(@"监听之后类:%s - 内存地址:%p",object_getClassName(self.person),[self.person methodForSelector:@selector(setName:)]);
复制代码

输出:

监听之前类:MMKVOPerson - 内存地址:0x103afc8e0
监听之后类:NSKVONotifying_MMKVOPerson - 内存地址:0x7fff25721c7a
复制代码

由输出可以看出会存在一个NSKVONotifying_+原类名的新类。并重写了setter方法。

1.我们在为一个对象添加观察者的时候,系统会为这个类派生出一个名为NSKVONotifying_+原类名 的类。

2.在这个类中会重写它的setter方法。并调用

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
复制代码

来观察赋值前后的属性的值。然后触发回调通知。

  • (四) 禁止使用系统的KVO

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
复制代码

注意:dealloc中的移除观察者要删掉,因为观察者没有注册上,会报because it is not registered as an observer.这个错误。

  • (五) 自定义KVO

//setName方法
void setterMethodIMP(id self, SEL _cmd, NSString *name){
    
    //1.取出观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
    //2.取出keyPath
    id keyPath = objc_getAssociatedObject(self, (__bridge const void *)@"keyPath");
    //3.通知观察者
    //[observer observeValueForKeyPath:(NSString *)keyPath ofObject:self change:@{(NSString *)keyPath:name} context:nil];
    objc_msgSend(observer,@selector(observeValueForKeyPath:ofObject:change:context:),(NSString *)keyPath,self,@{(NSString *)keyPath:name},nil);
}
- (void)MM_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    
    NSLog(@"自定义KVO");
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"MMKVONotifying_%@",oldName];
    
    //创建一个名为MMKVONotifying_MMKVOPerson的类
    Class newClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    
    //设置对象的isa指针 将self -> MMKVONotifying_MMKVOPerson
    object_setClass(self, newClass);
    
    //重写 setName 方法
    //获取方法名称
    NSString *methodName = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
    SEL sel = NSSelectorFromString(methodName);
    
    //添加一个自己的setter方法实现
    class_addMethod(newClass, sel, (IMP)setterMethodIMP, "v@:@");
    
    //添加关联着
    objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_ASSIGN);
    objc_setAssociatedObject(self, (__bridge const void *)@"keyPath", keyPath, OBJC_ASSOCIATION_COPY);

}
- (void)dealloc
{
    //释放观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
    observer = nil;
}
复制代码

输出:

keyPath:name
object:<MMKVONotifying_MMKVOPerson: 0x60000106bbd0>
change:{
    name = "\U9a6c\U5c0f\U6482*";
}
context:(null)
复制代码

1.动态的创建一个MMKVONotifying_MMKVOPerson的类

2.将当前类的isa指向新类

3.将setterSEL指向自己实现的IMP(setterMethodIMP),方法中调用objc_msgSend通知我们观察者

4.为observerkeyPath添加关联

5.重写dealloc方法,注销观察者

文章分类
iOS
文章标签