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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

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