iOS-解析KVO的底层实现

之前讲过KVO的简单应用,这次来讲解下KVO监听属性值的变化到底是怎么实现的,在监听的时候苹果在背后都做了哪些事情。举个例子:

@interface Person : NSObject
{
    @public
    NSString * _nickname;
}
@property (nonatomic , copy) NSString *name;

在Person类中添加一个name属性和一个_nickname成员变量,两者区别在于添加属性name时,会生成相应的setter/getter方法以及一个名为_name的成员变量;添加 _nickname的时候是不会生成setter/getter方法,不明白的请看这里。为了在外部能够访问_nickname成员变量需要加上@public将其公开。

设置监听

static NSString * nameKey;
static NSString *nicknameKey;

Person *person = [[Person alloc] init];
_person = person;
    
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:&nameKey];
    
[_person addObserver:self forKeyPath:@"_nickname" options:NSKeyValueObservingOptionNew context:&nicknameKey];

这里分别对属性name和成员变量_nickname进行监听,观察两者被修改时是否都能被监听到?

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (context == &nameKey) {
        NSLog(@"name改变了");
    }else if (context == &nicknameKey){
        NSLog(@"nickname改变了");
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int i = 0;
    i ++ ;
    
    _person.name = [NSString stringWithFormat:@"name = %d",i];
    _person->_nickname = [NSString stringWithFormat:@"nickname = %d",i];
    
    NSLog(@"%@   %@",_person.name,_person->_nickname);    
}

当点击屏幕的时候发现name值的改变被监听到了,而_nickname值的改变没有被监听到,这是什么原因呢?这里就要说KVO的到底是怎么实现的。

KVO实现原理

在监听处打个断点,调试看看,这里面有什么猫腻
这里写图片描述

这个时候看你的控制台:

这里写图片描述

我们看到这时候的对象person类型还是Person,这个isa指针指向的是真实数据类型。然后我们向下走一步会看到:

这里写图片描述
咦!对象person的数据类型变成了 NSKVONotifying_Person ,这是什么情况??难道苹果默默的做了些不为人知的是事情??答案是肯定的。

首先当我们在添加监听的时候,在其内部会动态(通过objc_allocateClassPair函数)的创建一个继承自XXX类且名为NSKVONotifying_XXX的类,在这个类里面会去重写被观察属性的setter方法:

- (void)setName:(NSString *)name
{
    [self willChangeValueForKey:@"name"];// 老值
    [super setName:name];
    [self didChangeValueForKey:@"name"]; // 新值
}

重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针指向这个新创建的子类(通过object_setClass函数改变指针),对象就神奇的变成了新创建的子类的实例。
在这里插入图片描述
回到之前的问题,_nickname属性值变化不能够被监听的原因也就出来了,因为 _nickname没有setter方法,也就没办法重写setter方法,最终导致无法监听其值的变化。所以KVO观察属性的变化其实就是观察属性的setter方法

如果想要观察到成员变量的变化有两个办法:

1、通过KVC修改成员变量值

[_person setValue:@"John" forKeyPath:@"nickname"];

2、通过手动触发KVO

    [_person willChangeValueForKey:@"nickname"];
    _person -> _nickname = @"John";
    [_person didChangeValueForKey:@"nickname"];

手动触发KVO

通常情况下KVO是系统自动触发的,当属性的值发生变化时就会被观察到。如股不想被系统自动触发KVO该怎么做呢?也简单调用类方法automaticallyNotifiesObserversForKey:

// 默认返回 YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
   // 可以通过key判断哪些需要手动触发KVO,哪些需要自动触发KVO
    return NO;
}

该方法如果返回YES就是系统自动触发KVO,如果返回NO就是需要手动触发KVO,因为有个参数key,所以可以将不同的属性设置为不同的触发条件。自动触发很简单,上面已经说过了,那么当返回NO的时候如何手动触发KVO呢?可以调用下面两个方法:

  [_person willChangeValueForKey:@"name"];//name即将改变
  _person.name = @"玉子";
  [_person didChangeValueForKey:@"name"];//name已经改变

keyPathsForValuesAffectingValueForKey:

情景:在Person类中复合一个Teacher类,然后在Teacher类中添加若干属性,如level属性、age属性等。我们的需求是当Teacher类中的属性发生改变时能够被观察到,我们可以这样做:

[_person addObserver:self forKeyPath:@"teacher.level" options:NSKeyValueObservingOptionNew context:nil];
[_person addObserver:self forKeyPath:@"teacher.age" options:NSKeyValueObservingOptionNew context:nil];
// ....

添加若干个监听,这样就可以实现上述需求,但是这样写显得太low,而且代码重复。这时可以使用keyPathsForValuesAffectingValueForKey:方法:

// 直接监听teacher
[_person addObserver:self forKeyPath:@"teacher" options:NSKeyValueObservingOptionNew context:nil];
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"teacher"]) {
        // 添加依赖
        keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"_teacher.level",@"_teacher.age"]];
    }
    return keyPaths;
}

如果不使用keyPathsForValuesAffectingValueForKey:方法而去直接监听teacher属性,那么当teacher中的属性修改时,不会被监听到,因为teacher的地址没有改变,不会调用setter方法。

类似的情况还有监听NSMutableArray,当调用addObject:方法时不会被监听到,这时候可以使用mutableArrayValueForKey:具体方法可查看

KVO遇到的问题

KVO优点:

  1. 代码简洁、实现简单;
  2. 可以实现两个对象间的同步(mode和view的同步);
  3. 能够提供观察的属性的最新值以及先前值;
  4. keyPaths来观察属性,因此也可以观察嵌套对象。

缺点也很明显在使用KVO的时候,系统会在运行时做动态创建类、重写方法等工作,这是非常消耗内存的。

常见情景:

1、监听webViewestimatedProgress,估计进度变化,以此来给webView加个进度条:

[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];

2、监听tableViewcontentOffsetcontentSize等属性:

[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context
{
    if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) {
        [self doSomethingWhenContentOffsetChanges];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

这里有个注意点,当观察的属性不在本类中,而是在superClass或者更上层父类中,这个时候需要加这个if判断,不至于这次KVO事件的触发被中断。

KVO崩溃场景

被观察者在销毁前,要移除所有的观察者,iOS10以下会崩溃,iOS11以上不会崩溃。

  1. observe忘记写监听回调方法 observeValueForKeyPath;
  2. add和remove次数不匹配;
  3. 监听者和被监听者dealloc之前没有remove(其实也原因2,但是监听者和被监听者的生命周期不同)

参考文章:

iOS KVO崩溃全情景列举+解决方案分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值