KVO基本使用及面试题分析

基本概念

KVO:Key-Value Observing,俗称’键值监听’,用于监听某个对象属性值的改变。

基本使用

自动触发

创建Person对象,并创建属性name:

 Person *p = [[Person alloc]init];
  //添加观察者
  [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
复制代码

添加观察者后,当p的name属性的值发生变化时就会自动调用方法:

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

注:要在dealloc方法中移除监听

手动触发

使用场景:当监听的属性改变时不需要自动触发,当需要时再去触发。 解决方法是:先关闭自动触发,在Person.m中重写方法:

//默认YES(自动)
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
复制代码

然后在改变属性值的时候:

[_P willChangeValueForKey:@"name"];
 _P.name  = [NSString stringWithFormat:@"%d",a++];
 [_P didChangeValueForKey:@"name"];
复制代码

这样就完成了手动触发。

复杂情况

场景:当Person中的属性是自定义Student 类的实例对象stu,在Student类中有属性 age,如何通过观察p对象来监听age属性?

解决方法一

在添加观察者时:

[p addObserver:self forKeyPath:@"stu.age" options:NSKeyValueObservingOptionNew context:nil];

复制代码

弊端:当Student中有多个属性需要监听时,一个一个的添加监听者虽然没错但是太麻烦了!

解决方法二

在Person.m中重写该方法:

//返回一个容器

+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"stu"]) {

        NSArray *addKeyPath = @[@"_stu.no",@"_stu.age"];

        keyPaths = [keyPaths setByAddingObjectsFromArray:addKeyPath];

    }

    return keyPaths;

}
复制代码

这样只需要对stu监听即可。

注:一定要带下划线

内部实现原理

面试题答案:当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,使其指向通过Runtime动态创建的子类,该子类重写了set方法,内部实现会调用 willChangeValueForKey、父类的setter、didChangeValueForKey。在didChangeValueForKey方法中又会调用监听器的监听方法。

接下来一步步进行验证。 首先我们利用运行时方法分别打印添加监听前后对象的类:

 Person *p = [[Person alloc]init];
 NSLog(@"添加监听之前%@",object_getClass(p));
  //添加观察者
 [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
 NSLog(@"添加监听之后%@",object_getClass(p));
复制代码

打印结果:

KVODemo01[1749:181970] 添加监听之前Person
KVODemo01[1749:181970] 添加监听之后NSKVONotifying_Person
复制代码

由此可知再添加监听后,系统创建了一个新的类,p对象的isa指向这个新的类。

下一步我们可以打印添加监听前后的set方法的地址:

NSLog(@"添加监听之前%p",[p methodForSelector:@selector(setName:)]);
NSLog(@"添加监听之后%p",[p methodForSelector:@selector(setName:)]);
复制代码

打印结果:

添加监听之前0x10570c3a0    
添加监听之后0x105a56efe
复制代码

我们可以利用lldb分别看一下具体的方法实现:

p (IMP)0x10570c3a0
(IMP) $0 = 0x000000010570c3a0 (KVODemo01`-[Person setName:] at Person.h:12)

p (IMP) 0x105a56efe
(IMP) $2 = 0x0000000105a56efe (Foundation`_NSSetObjectValueAndNotify)
复制代码

由以上证明可知:添加了监听后,新创建的类重写了set方法,新创建的set方法内部调用了_NSSetObjectValueAndNotify.

最后我们验证 _NSSetObjectValueAndNotify中是否调用了上面提到了函数和他们的执行顺序: 在Person中重写:

在Person中重写:
-(void)willChangeValueForKey:(NSString *)key{
    NSLog(@"willChangeValueForKey-begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey-end");
}
-(void)didChangeValueForKey:(NSString *)key{
     NSLog(@"didChangeValueForKey-begin");
     [super didChangeValueForKey:key];
     NSLog(@"didChangeValueForKey-end");
    
}
复制代码

打印结果:

2018-03-20 16:06:30.610367+0800 KVODemo01[1892:206083] willChangeValueForKey-begin
2018-03-20 16:06:30.610559+0800 KVODemo01[1892:206083] willChangeValueForKey-end
2018-03-20 16:06:30.610661+0800 KVODemo01[1892:206083] didChangeValueForKey-begin
2018-03-20 16:06:30.610924+0800 KVODemo01[1892:206083] {
    kind = 1;
    new = 0;
}
2018-03-20 16:06:30.611022+0800 KVODemo01[1892:206083] didChangeValueForKey-end
复制代码

由此可知,调用Foundation的_NSSetObjectValueAndNotify的方法后内部依次调用了

willChangeValueForKey、
父类的setter、
didChangeValueForKey。
在didChangeValueForKey方法中又会调用监听器的监听方法
复制代码

结语

希望以上分享对读者有所帮助,更希望大神不吝赐教!

转载于:https://juejin.im/post/5ab1c0dc5188252c32198c29

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值