Loading

iOS 底层原理 | KVO

一、KVO 概述

KVO,(Key-Value Observing),即键值监听,是一种机制,允许注册成为其他对象的观察者,当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。

二、KVO 基本使用

必须执行以下步骤,才能使对象接收 KVO 兼容属性通知的键值:

  • 将观察者注册到观察对象上 使用这个方法:addObserver:forKeyPath:options:context:
  • 实现 observeValueForKeyPath:ofObject:change:context: 来接收观察者内部值的变化的通知消息。
  • 在观察者从内存释放之前,调用 removeObserver:forKeyPath: 来移除观察者。
  • 1. 注册观察者
- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;
  • 2. 注册观察者
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;
  • 3. 注销观察者
- (void)removeObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath;

1. 基本使用

首先有一个 Person 类,只有一个 name 属性,代码如下:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Person


@end

ViewController.m 中:

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _person = [Person alloc];
    // 注册 self 也就是 controller 为自己的观察者
    [_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    _person.name = @"哈哈";
}

// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@ - %@ - %@",keyPath,object,change);
}

// 移除观察者
- (void)dealloc{
    [_person removeObserver:self forKeyPath:@"name"];
}

@end

点击屏幕,控制台输出:

2021-01-18 15:59:15.571749+0800 KVO[3322:245856] name - <Person: 0x6000008c0350> - {
    kind = 1;
    new = "\U54c8\U54c8";
    old = "<null>";
}

NSKeyValueChange 值:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//设值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替换
};

2. 手动触发 observer 回调

我们改变了 name 的值,就自动触发了 observer 回调,但是有时候我们并不一定每一次都通知,比如满足某一个条件的时候,才想着通知一下。
我们先在 Person 中添加一个方法,关闭自动触发:

// 对所有的都关闭
//+ (BOOL)automaticallyNotifiesObserversOfName {
//    return NO;
//}

// 可以对某个指定key 关闭
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if([key isEqualToString:@"name"]){
        NSLog(@"关闭了自动触发");
        return NO;
    }
    return YES;
}

person.name 赋值的地方添加上手动触发的代码:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // name 的值即将改变
    [_person willChangeValueForKey:@"name"];
    _person.name = @"哈哈";
    // name的值改变完成
    [_person didChangeValueForKey:@"name"];
}

3. 观察属性.属性的变化

Person 类中有 Dog 属性,Dog 类中有 age 属性,观察 age 的变化如下:

[_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];

4. 观察多个属性变化

Person 类中再添加两个属性:firstNamelastName,它们两个是 name 的组成,当给 name 添加监听时,当 firstNamelastName 任意一个变化,都要收到通知:

  • Person 类中加入类方法 + keyPathsForValuesAffectingValueForKey,返回一个容器。
  • 注册观察 Personname 属性,这样就可以在 firstNamelastName 变化时,也收到相应的回调。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingName {
    NSSet *keyPaths = [NSSet setWithArray:@[@"firstName",@"lastName"]];
    return keyPaths;
}

4. 观察可变数组变化

Person 添加一个可变数组属性,当向这个可变数组添加数据时,是不会调用 setter 方法的,也不会触发 KVO 的回调,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _person = [Person alloc];
    // sons是一个可变数组
    _person.sons = [@[] mutableCopy];
    // 观察 sons 的变化
    [_person addObserver:self forKeyPath:@"sons" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // sons添加元素
    [_person.sons addObject:@"lili"];
}

直接通过 [_person.sons addObject:@"lili"]; 无法触发 KVO 回调,针对于可变数组的集合类型,需要通过 mutableArrayValueForKey 方法将元素添加到可变数组中,才能触发 KVO 回调,将添加元素的代码改为如下代码即可:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[_person mutableArrayValueForKey:@"sons"] addObject:@"lili"];
}

三、KVO 底层探索

KVO 使用 isa-swizzling 技术实现自动键值观察。

  • 该isa指针,指向对象的类,它保持一个调度表。该分派表实质上包含指向该类实现的方法的指针以及其他数据。
  • 当为对象的属性注册观察者时,将修改观察对象的 isa 指针,指向中间类而不是真实类。结果,isa 指针的值不一定反映实例的实际类。
  • 您永远不要依靠 isa 指针来确定类成员。相反,您应该使用该 [ class] 方法确定对象实例的类。

1. 验证 KVO 只对属性观察

  1. 随意创建一个 Project ,创建一个类,类拥有一个属性,一个成员变量。
  2. 实例化一个类的对象,并添加属性和成员变量的观察者都为 ViewController
  3. 添加 touchBegin 方法,做到点击屏幕,就让属性和实例变量都发生变化。
  4. KVO 观察的是拥有 setter 方法的变量,可以是成员变量,但是必须使用 KVC 进行赋值,属性则是可以直接赋值进行观察的。
    (注:具体验证自行实现)

2. 验证 KVO 中间类

根据官方文档描述,在注册 KVO 观察者后,观察对象的 isa 指针会发生改变,指向了一个中间类。

注册前后类名对比:

获取NSKVONotifying_Person类的父类,可以看到是 PersonNSKVONotifying_PersonPerson 的子类:

我们也可以通过以下代码获取类信息:

- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[I];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

///// 调用
[self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];

得:

2021-01-18 20:47:41.791402+0800 KVO[4396:407755] setNickName:-0x10918c54b
2021-01-18 20:47:41.791516+0800 KVO[4396:407755] class-0x10918afd5
2021-01-18 20:47:41.791612+0800 KVO[4396:407755] dealloc-0x10918ad3a
2021-01-18 20:47:41.791706+0800 KVO[4396:407755] _isKVOA-0x10918ad32

NSKVONotifying_Person 类拥有着4个方法:

  • setXxx : 重写被观察的属性的 set 方法。
  • class : 重写自己的 class 方法。
  • dealloc : 重写自己的 dealloc 方法。
  • _isKVOA : 判断是不是 KVO 生成的中间类。

在进行了 KVO 观察之后的对象,它的 isa 再指向的就是 NSKVONotifying_xxx 这个类,做的改变都会找到 NSKVONotifying_xxxset 方法来对属性进行更改。

NSKVONotifying_xxx 这个中间子类会重写父类的 setterdeallocclass 方法。并且当观察者注销后,中间类不会被销毁,而是缓存起来,有需要的时候直接调用,减少了下次再添加观察者的时候的开支。

参考文档

  1. 《Key-Value Observing Programming Guide》 苹果官方文档

  2. 《[iOS] KVO底层原理》 code_ce

posted @ 2021-02-14 03:04  QiuZH's  阅读(200)  评论(0编辑  收藏  举报