KVO的使用及原理

概述

KVO 全称KeyValueObserving键值监听,是苹果提供的一套事件通讯机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。一般继承自NSObject的对象都默认支持KVO

对象的属性是否发生变化肯定会调用其setter方法,而KVO的本质是监听对象有没有调用被监听属性的setter方法

常用方法

使用KVO分三个步骤:

  • 添加观察者(注册)。
  • 观察者中实现回调。
  • 移除观察者(删除)。
/*
注册监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
监听的属性路径为keyPath支持点语法的嵌套
监听类型为options支持按位或来监听多个事件类型
监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
添加监听器只会保留监听器对象的地址,不会增加引用,也不会在对象释放后置空,因此需要自己持有监听对象的强引用,该参数也会在回调函数中回传
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/*
删除监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
监听上下文context,应与addObserver方法的context匹配
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

/*
与上一个方法相同,只是少了context参数
推荐使用上一个方法,该方法由于没有传递context可能会产生异常结果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/*
监听器对象的监听回调方法
keyPath即为监听的属性路径
object为被监听的对象
change保存被监听的值产生的变化
context为监听上下文,由add方法回传
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

简单实现

我们创建一个 Person类,然后在类中添加一个name属性和sex属性。

@interface Person : NSObject
@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSString* sex;
@end
 
@implementation Person

-(instancetype)init{
    self = [super init];
    if (self) {
        _name = @"liangtong";
        _sex = @"M";
    }
    return self;
}

然后我们观察这个Person实例对象的name属性

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    _person = [[Person alloc] init];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [_person addObserver:self forKeyPath:@"name" options:options context:@"123"];
    _person.name = @"joker";
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

-(void)dealloc{
    [_person removeObserver:self forKeyPath:@"name"];
    _person = nil;
}

执行程序,然后log信息如下

2019-01-26 15:53:08.545313+0800 runtime_kvo[5666:1724236] 监听到isa : NSKVONotifying_Person 
 superclass : Person 
 setName IMP : 0x1096ea63a 
 setSex IMP: 0x1093914d0的name属性值改变了 - {
    kind = 1;
    new = joker;
    old = liangtong;
} - 123

注册观察者后,当我们通过.给对象属性进行赋值时,最终会通知观察者具体的改变。例子中,我们会在监听回调中得到keypath 为 name,context为123的object变更

实现原理

为了能够看到更多的细节,我们重写Person类的description方法。

/****
 * 重写description,展示更多信息
 ***/
-(NSString*)description{
    
    NSString* className = NSStringFromClass(object_getClass(self));
    NSString* superclass = NSStringFromClass(class_getSuperclass(object_getClass(self)));

    IMP setNameIMP = [self methodForSelector:@selector(setName:)];
    IMP setSexIMP = [self methodForSelector:@selector(setSex:)];
    
    NSString* desc = [NSString stringWithFormat:@"isa : %@ \n  \
                      superclass : %@ \n \
                      setName IMP : %p \n \
                      setSex IMP: %p",
                      className,superclass,setNameIMP,setSexIMP];
    return desc;
}

在注册观察者前后分别打印_person实例对象的信息,如下

2019-01-26 15:59:23.528750+0800 runtime_kvo[5734:1745208] Before Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x100dca430 
                       setSex IMP: 0x100dca490
2019-01-26 15:59:23.529090+0800 runtime_kvo[5734:1745208] After Observe---------------->
 isa : NSKVONotifying_Person 
                        superclass : Person 
                       setName IMP : 0x10112363a 
                       setSex IMP: 0x100dca490
2019-01-26 15:59:23.529278+0800 runtime_kvo[5734:1745208] 监听到isa : NSKVONotifying_Person 
                        superclass : Person 
                       setName IMP : 0x10112363a 
                       setSex IMP: 0x100dca490的name属性值改变了 - {
    kind = 1;
    new = joker;
    old = liangtong;
} - 123

我们可以看到,注册KVO监听后,_person对象的isa指针由Person类变成了NSKVONotifying_Person类。而superclassNSObject变成了Person类setName:的方法实现发生了变更(由0x100dca430变成了0x10112363a)而setSex:的未发生变更。

我们大致可以猜到KVO是通过isa-swizzling技术实现的

  • 在运行期间根据原类创建一个中间类(NSKVONotifying_xxx),这个中间类是原类的子类。
  • 动态修改了对象的isa指向中间类。
  • 中间类重写了被监听属性的setter方法,没有监听的属性setter方法则不会被重写。
    • 重写属性的setter方法在修改之前会调用willChangeValueForKey:方法。
    • 重写属性的setter方法在修改之后会调用didChangeValueForKey:方法。
    • 通过添加断点,我们可以看到在修改之后,会调用NSKeyValueNotifyObserver
    • 最终会调用到observeValueForKeyPath:ofObject:change:context:方法中。
  • 重写delloc方法,销毁新生成的NSKVONotifying_Person类
断点.png

猜测与验证

通过以上,我们猜测如果阻止系统自动调用属性的willChangeValueForKey:didChangeValueForKey:方法,可能会阻止KVO的事件传递。于是我们在Person类中重写以下方法

/**
 * 当key未name时候,不自动触发相关的setter
 **/
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

继续运行刚才的代码,结果如下

2019-01-26 16:28:54.434002+0800 runtime_kvo[6071:1853052] Before Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x104ea4400 
                       setSex IMP: 0x104ea4460
2019-01-26 16:28:54.434228+0800 runtime_kvo[6071:1853052] After Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x104ea4400 
                       setSex IMP: 0x104ea4460

果真未自动触发KVO!!

那么问题来了,我们可以通过手动出发KVO吗?如果我们手动调用被阻止的两个方法,可以出发KVO吗?为了测试我们的猜想,我们给Person类的sex属性的setter中,添加相关代码。

-(void)setSex:(NSString *)sex{
    _sex = sex;
    //通过手动调用setter,测试能否触发KVO
    [self willChangeValueForKey:@"name"];
    _name = @"Hello Ketty";
    [self didChangeValueForKey:@"name"];
}

修改下运行的代码,之前是通过name的setter进行操作,现在我们换成调用sex的setter,如下

    _person.sex = @"F";

结果如下:

2019-01-26 16:35:28.746294+0800 runtime_kvo[6174:1872068] Before Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x10b9c2400 
                       setSex IMP: 0x10b9c2320
2019-01-26 16:35:28.746496+0800 runtime_kvo[6174:1872068] After Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x10b9c2400 
                       setSex IMP: 0x10b9c2320
2019-01-26 16:35:28.746700+0800 runtime_kvo[6174:1872068] 监听到isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x10b9c2400 
                       setSex IMP: 0x10b9c2320的name属性值改变了 - {
    kind = 1;
    new = "Hello Ketty";
    old = liangtong;
} - 123

sexsetter方法中,我们手动调用willChangeValueForKey:didChangeValueForKey:方法对name进行设置,成功触发了KVO操作!

总结

经过以上代码,我们大致了解了KVO的本质。

  • 1、isa-swizzling,利用RuntimeApi动态生成一个子类(NSKVONotifying_xxx),并让instance对象的isa指向这个全新的子类。
  • 2、当修改对象的被监听属性时候,会依次调用子类(NSKVONotifying_xxx)的以下方法
    • willChangeValueForKey:
    • 父类原来的setter
    • didChangeValueForKey:
    • 最终触发observeValueForKeyPath:ofObject:change:context:

当我们重写automaticallyNotifiesObserversForKey:方法,对name的相关自动调用willChangeValueForKey:didChangeValueForKey:`方法返回NO时,KVO未触发,表明直接修改成员变量的值不会触发KVO。

经过以上猜测部分,我们也知道了如何手动触发KVO。手动调用以下两个方法:

  • 手动调用 willChangeValueForKey:
  • 手动调用didChangeValueForKey:

KVO通知依赖以上两个方法,在属性变更之前通过调用 willChangeValueForKey:记录旧值;而属性发生改变之后通过调用didChangeValueForKey:保存新值。继而 observeValueForKey:ofObject:change:context:也会被调用。

KVO的监听移除

  • 添加与移除成对出现

  • 不移除会造成内存泄漏

  • 多次移除会造成崩溃

    •    @try {
            [object removeObserver:self forKeyPath:@"keyPath"];
         }
         @catch (NSException * __unused exception) {}
      
    • 系统为了实现KVO,为NSObject添加了一个名为NSKeyValueObserverRegistration的Category,KVO的add和remove的实现都在里面。在移除的时候,系统会判断当前KVO的key是否已经被移除,如果已经被移除,则主动抛出一个NSException的异常

Demo

https://github.com/liangtongdev/Demo-runtime_kvo

参照

KVO原理分析及使用进阶:https://www.jianshu.com/p/badf5cac0130
KVO :https://github.com/SunshineBrother/JHBlog/blob/master/iOS知识点/iOS底层/3、KVO.md
iOS-KVO 实现原理:https://www.jianshu.com/p/0e75d99c3480

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • KVC/KVO 概念 KVC : 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准...
    满脸胡茬的小码农阅读 1,984评论 2 8
  • 关于KVC KVC是什么? Key-Value Coding,即键值编码。它是一种不通过存取方法,而通过属性名称字...
    Wang66阅读 12,654评论 4 38
  • 问题 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVO ? 首先需要了解KVO...
    hjltony阅读 588评论 0 2
  • 该文章属于刘小壮原创,转载请注明:刘小壮[https://www.jianshu.com/u/2de707c93d...
    刘小壮阅读 48,648评论 35 227
  • 面试问题: · iOS用什么方式实现对一个对象的KVO? · 如何手动触发KVO? 我们通过以下几个点来寻找这两个...
    高思阳阅读 252评论 0 1