KVO底层原理探索与实践

122 阅读4分钟

KVO 概述

KVO,全称为 Key-Value observing,中文名为键值观察,可以理解为监听模式,对象 A 监听对象 B 的属性 age,当属性 age 发生改变的时候,对象 A 收到通知,做相应的操作

KVO 使用基本步骤

  1. 注册观察者
  2. 实现 KVO 回调
  3. 移除观察者

注意:添加观察者与移除观察者必须成对出现,不然会发生不可控错误

KVO 常用方法

  1. 基本使用
@interface Person : NSObject

@property(nonatomic,copy)NSString * age;

@end

@interface ViewController ()

@property(nonatomic,strong)Person * p;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
     Person * p = [[Person alloc]init];
     _p = p;

     // 添加观察者
     [_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}

// 监听属性变化回调

/*
 *   keyPath     观察者的key
 *   object      观察者key所属的对象
 *   context     值改变的上下文
 *   change      改变的内容,新值or旧值
 */

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {

  NSLog(@"监听到改变:%@对象的属性:%@===发生改变:change:%@",object,keyPath,change);

}

// 修改属性的值,触发方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

   static int i = 0;
   i++;
   // 修改值
   self.p.age = [NSString  stringWithFormat:@"%d",i];

}

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

@end

  1. KVO 自动触发与手动触发

KVO 观察的开启和关闭有两种方式,自动和手动

  • 返回YES表示监听
  • 返回NO不监听
// key 表示监听属性
// 默认返回YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
   if([key isEqualToString:@"age"]) {
       NSLog(@"关闭了age自动触发");
      return NO;
   }
    return YES;
}

  • 自动监听关闭的时候,可以通过手动触发回调

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    static int i = 0;
    i++;

    // 1. willChangeValueForKey
    // 2. set赋值
    // 3. didChangeValueForKey
    [self.p willChangeValueForKey:@"age"];
    self.p.age = [NSString  stringWithFormat:@"%d",i];
    [self.p didChangeValueForKey:@"age"];

}

  1. 通过 KVO 观察一个对象里面的一个对象属性的里面的属性

@interface Tool : NSObject

@property(nonatomic,copy)NSString * isLogin;

@end


@interface Person : NSObject

//  Tool对象
@property(nonatomic,strong)Tool * toolObj;

@end


- (void)viewDidLoad {
    [super viewDidLoad];

    // 监听对象的对象的属性
    // toolObj 不为nil时候,改变toolObj属性值,会触发回调,为nil则不会收到回调
    [self.p addObserver:self forKeyPath:@"toolObj.isLogin" options:NSKeyValueObservingOptionNew context:nil];

    // 这种监听方式,改变toolObj属性值不会触发回调
    [self.p addObserver:self forKeyPath:@"toolObj" options:NSKeyValueObservingOptionNew context:nil];

}

  1. KVO 可以实现注册一个观察者,监听多个属性变化

    keyPathsForValuesAffectingValueForKey:(NSString *)key

  • 在 Person 对象里面再添加sexname属性
  • sex或者name属性改变的时候,监听age属性的观察者可以收到回调通知
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {

   NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"age"]) {
          NSArray *affectingKeys = @[@"sex", @"name"];
          keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

  1. KVO 监听可变数组变化

    通过常用的addObject的方式添加不会触发回调,必须通过 mutableArrayValueForKey 方式添加

@interface Person : NSObject

@property(nonatomic,strong)NSMutableArray * dataArray;

@end

- (void)viewDidLoad {
    [super viewDidLoad];

  // 监听数组
   [_p addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:nil];

}

// 修改值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 错误方式,不会触发监听回调
    [self.p.dataArray addObject:@"1"];

    // 正确方式,值改变收到回调
    [[self.p mutableArrayValueForKey:@"dataArray"] addObject:@"1"];

}

KVO 原理探索

  1. 注册观察者

@interface Person : NSObject
@property(nonatomic,copy)NSString * age;
@end

@implementation Person
@end



- (void)viewDidLoad {

    [super viewDidLoad];

    Person * p = [[Person alloc]init];
    _p = p;

    NSLog(@"添加观察者之前======实例p类名:%s",object_getClassName(_p));

   // 添加观察者
    [_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

    NSLog(@"添加观察者之后======实例p类名:%s",object_getClassName(_p));

}

// 观察者属性改变回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {

  NSLog(@"监听到改变====object:%@==keyPath:%@===change:%@",context,keyPath,change);

}

// 通过setAge的方法改变age的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static int i = 0;
    i++;
    self.p.age = [NSString  stringWithFormat:@"%d",i];
}



  1. 运行结果

  1. 根据结果可以看到在添加观察者之后 p 对象的指针isaPerson指向了NSKVONotifying_Person
  • 利用 runtime 打印NSKVONotifying_Person父类
 Class cls = class_getSuperclass(NSClassFromString(@"NSKVONotifying_Person"));

 NSLog(@"NSKVONotifying_Person父类:%@",NSStringFromClass(cls));

可以看到NSKVONotifying_PersonPerson的派生类

生成这个中间类是干什么的?

通过 runtime 打印出NSKVONotifying_Person类的所有方法

- (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")];


从打印结果看到有 4 个方法,setAge、class、dealloc、_isKVOA

打印出 Person 方法

可以看到两个对象方法地址不同,说明 NSKVONotifying_Person重写了父类PersonsetAge方法,同时重写了基类NSObjectclassdealloc_isKVOA方法

移除观察者之后,isa 会发生什么?

通过打印结果看到,移除观察者之后,isa指针由NSKVONotifying_Person又指向了Person,那么问题来了,中间类NSKVONotifying_Person还存在吗

通过runtime打印出内存中 Person 类情况

通过结果看到,移除观察者之后,中间类没有被销毁,还存在内存中,方便重用

总结

  • 添加观察者,监听 Person 对象的 age 属性之后,Person 对象的指针isa会指向派生类NSKVONotifying_Person
  • 当属性发生变化之后,NSKVONotifying_Person set 方法里面会调用 willChangeValueForKeydidChangeValueForKey方法触发监听的回调
  • 中间类重写了被观察属性的setter方法、class、dealloc_isKVO方法
  • 中间类在移除观察者之后,并不会立即被销毁
  • KVO 只对属性产生观察,不会观察成员变量,因为成员变量没有setter方法

拓展

  • 系统观察者流程繁琐,而且必须是添加与移除成对出现,可以再做一些逻辑优化
  • 通过自定义方式观察者,把添加与移除放在 map 里面匹配,避免不成对出现
  • 通过结合 Block 的回调,实现步骤简洁