iOS底层原理-KVO本质探究

本文深入探讨了iOS中KVO(Key-Value Observing)的工作原理,揭示了其背后的动态子类创建机制,以及如何通过KVC触发KVO。文章还讲解了KVO的注意事项,包括监听器的正确管理和依赖键的使用,最后介绍了手动触发KVO的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

iOS底层原理-KVO本质探究

先说结论,KVO 的本质:

在对一个已知类的某个属性进行 KVO 监听时,系统会在运行时动态创建一个已知类的子类 NSKVONotifying_某类名,并在子类实现 setter 方法,set方法实现内部会顺序调用willChangeValueForKey 方法、原来的 setter 方法实现、didChangeValueForKey 方法,而 didChangeValueForKey 方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context: 监听方法。

1. KVO 演示

- (void)testKVO {
    self.person.name = @"Origin Name";
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:nil];
    
    [self.person setValue:@"JK" forKey:@"name"];
    
}

输出:

XWInterviewDemos[38107:525654] KVO -- <XWPerson: 0x60000112fb00> 的 name 属性 发生的变化: {
    kind = 1;
    new = JK;
    old = "Origin Name";
}

2. 探寻 KVO 实现原理

重写 XWPersoninitialize 方法

@implementation XWPerson
+ (void)initialize {
    NSLog(@"%@ %s",self,__func__);
}
@end

会发现在运行上述代码时会调用两次,分别打印:

2018-10-11 12:55:41.686464+0800 XWInterviewDemos[38372:529858] XWPerson +[XWPerson initialize]
2018-10-11 12:55:45.002630+0800 XWInterviewDemos[38372:529858] NSKVONotifying_XWPerson +[XWPerson initialize]

我们知道,类的 initialize 方法会在类第一次接收到消息时调用,如果子类没有实现 +initialize,会调用父类的+initialize(所以父类的+initialize 可能会被调用多次),这意味着 XWPerson 作为 NSKVONotifying_XWPerson 的父类被调用两次。NSKVONotifying_XWPerson 即为 Apple 底层 对类进行 KVO 监听所动态生成的原类的子类。

还可在添加 KVO 前后打印当前 XWPersonisa 指针。
Xnip2018-10-11_15-20-44

用一幅图表示即:
Xnip2018-10-11_15-33-13

原图出自:SEEMYGO

3. KVO 使用注意点

  • 多次添加过的监听,都要挨个移除。所以这一点,在cell中使用时候要特别注意。因为cell多次运行, 监听可能就是多次添加
  • kvo触发是严格依赖kvc机制的。简单来说就是触发kvo必须是kvc方式给属性赋值。
    • _name = @"123"; 类似直接为成员变量赋值不会触发 KVO
  • dealloc 中进行 removeObserver 操作
  • 父类和子类有可能对同一个属性进行观察,我们知道如果对同一个属性的观察者移除两次会造成崩溃,所以我们每个类应该有唯一的 Context 进行区分。

4. KVO 的注册依赖键

问题描述:一个属性的值是依赖于其它一个或多个其它对象的属性.如果一个属性的值更改,那么派生属性的值也应该被标记为更改.

例如某类有三个属性

@property (nonatomic, copy) NSString *fullName;
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;

其中 fullName 值为 firstName + lastName

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}

firstNamelastName 被改变时,都要通知到那些观察 fullName 属性的对象.

一种解决办法是:在添加监听的对象类重写 keyPathsForValuesAffectingValueForKey: 指明 fullName 属性是依赖于lastNamefirstName属性的.

OC

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

Swift

 override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if key == "fullName" {
            return Set<String>(arrayLiteral: "firstName","lastName")
        } else {
            return super.keyPathsForValuesAffectingValue(forKey: key)
        }
    }

或者实现 方法实现相同的目的
OC

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

此 KVO 监听 fullName 属性时,当 lastNamefirstName属性变化时也会调用

5. KVO 手动触发

显式的调用 willChangeValueForKey: 和 didChangeValueForKey: 方法

如果想实现手动通知,我们需要借助一个额外的方法

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

这个方法默认返回YES,用来标记 Key 指定的属性是否支持 KVO,如果返回值为 NO,则需要我们手动更新。

@implementation XWPerson

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

@end

在需要监听 XWPerson 处.

[person willChangeValueForKey:@"name"];
person.name = @"2";
/// 此时在 任意需要触发kvo的地方调用如下代码即可手动触发 KVO
[person didChangeValueForKey:@"name"];
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值