KVO

243 阅读5分钟

简单使用

KVO的全称是“Key-Value Observing”,俗称“健值监听”,可以用于监听某个对象属性值的改变,本质就是增加一个观察者,属性值改变的时候调用方法,告诉观察者。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    self.person1.height = 11;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    self.person2.height = 22;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    [self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 20;
    self.person2.age = 20;
    
    self.person1.height = 30;
    self.person2.height = 30;
}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
    [self.person1 removeObserver:self forKeyPath:@"height"];
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

注意:KVO最后要在dealloc里移除。

KVO本质

KVO本质是利用runtime运行时生成了一个MJPerson的子类:NSKVONotifying_MJPerson类 这个子类复写了setAge实现,setAage实现调用了Fondation框架的 _NSSetXXValueAndNotify方法(_NSSetIntValueAndNotify、_NSSetCharValueAndNotify、_NSSetObjectValueAndNotify、_NSSetDoubleValueAndNotify)。 MJPerson的instance对象的isa指针指向NSKVONotifying_MJPerson的class对象,当我们调用MJPerson的setter方法时,就会通过isa指针找到NSKgVONotifying_MJPerson的setAge:方法。

_NSSetXXValueAndNotify又调用了以下几个方法:

[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
  • willChangeValueForKey、didChangeValueForKey.NSKVONotifying_MJPerson类没有这两个方法,是基类NSObject的方法。
  • [super setAge:age]; 调用父类的setter方法,也就是MJPerson的setAge:
  • didChangeValueForKey又调用了 [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];

伪代码如下:

#import "NSKVONotifying_MJPerson.h"

@implementation NSKVONotifying_MJPerson

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 伪代码
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

@end

怎样验证

  • 证明确实生成了NSKVONotifying_MJPerson类。1.我们自己创建个NSKVONotifying_MJPerson后KVO就会失败。 2.打印 self.person1.isa指针。
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>

@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end

// 反编译工具 - Hopper

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    
//    NSLog(@"person1添加KVO监听之前 - %@ %@",
//          object_getClass(self.person1),
//          object_getClass(self.person2));
    NSLog(@"person1添加KVO监听之前 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
//    NSLog(@"person1添加KVO监听之后 - %@ %@",
//          object_getClass(self.person1),
//          object_getClass(self.person2));  //object_getClass(self.person1)打印结果是NSKVONotifying_MJPerson,是通过isa指针找到类对象。

    //methodForSelector方法打印方法的具体实现地址
    NSLog(@"person1添加KVO监听之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

    
    NSLog(@"类对象 - %@ %@",
          object_getClass(self.person1),  // self.person1.isa
          object_getClass(self.person2)); // self.person2.isa

    NSLog(@"元类对象 - %@ %@",
          object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
          object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // NSKVONotifying_MJPerson是使用Runtime动态创建的一个类,是MJPerson的子类
    // self.person1.isa == NSKVONotifying_MJPerson
    [self.person1 setAge:21];
    
    // self.person2.isa = MJPerson
//    [self.person2 setAge:22];
}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

@end

  • KVO后打印object_getClass(self.person1)发现是NSKVONotifying_MJPerson类对象。(说明object_getClass是通过isa指针找的)

  • NSLog(@"元类对象 - %@ %@", object_getClass(object_getClass(self.person1)),可以发现NSKVONotifying_MJPerson的isa指针指向的是NSKVONotifying_MJPerson的元类对象,而不是MJPerson的元类对象。

  • NSLog(@"person1添加KVO监听之后 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], 这个方法打印方法的实现地址。然后通过p (IMP)地址值发现NSKVONotifying_MJPerson的setAge:实现是_NSXXXSetIntValueAndNotify.

总结:NSKVONotifying_MJPerson继承自MJPerson.MJPerson的instance对象的isa指针指向NSKVONotifying_MJPerson的类对象,NSKVONotifying_MJPerson类对象的isa指针指向自己的元类对象,superClass指针指向父类也就是MJPerson的类对象。
  • 验证_NSXXXSetIntValueAndNotify调用的方法
#import "MJPerson.h"

@implementation MJPerson

- (void)setAge:(int)age
{
    _age = age;
    
    NSLog(@"setAge:");
}

//- (int)age
//{
//    return _age;
//}

- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

@end

说明:我们覆写了willChangeValueForKey又调用了[super willChangeValueForKey].相当于没写这个函数。

通过打印结果可知确实如上所说。

窥探Foundation

可以用逆向方法,Hooper从手机共享动态库里提取出Foundation动态库,再用Hooper看。

或者 用nm.nm用于llvm symbol table dumper 导出符号表 nm Foundation | grep ValueAndNotify

子类NSKVONotifying_MJPerson的内部方法

@implementation ViewController

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    

    // 获得方法数组 runtime方法
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    [self printMethodNamesOfClass:object_getClass(self.person1)];
    [self printMethodNamesOfClass:object_getClass(self.person2)];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self.person1 setAge:21];
    
    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];
}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// observeValueForKeyPath:ofObject:change:context:
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

@end

  • class_copyMethodList(<#Class _Nullable __unsafe_unretained cls#>, unsigned int * _Nullable outCount) 这是个runtime方法,第二个参数放的是指针的值,所以传入&count

  • Method *methodList = class_copyMethodList(cls, &count);C语言知识,可以用指针指向数组。

  • free(methodList);前面create或者copy了都要释放。

总结:通过打印得,NSKVONotifying_MJPerson有 setAge:, class, dealloc, _isKVOA方法

伪代码如下:

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
    return [MJPerson class];
}

- (void)dealloc
{
    // 收尾工作
}

- (BOOL)_isKVOA
{
    return YES;
}


为什么会有class方法呢? 调[MJPerson class]发现不是对象isa指针指向的NSKVONotifying_MJPerson的类对象。因为NSKVONotifying_MJPerson对使用者是不可见的,所以要改写class方法。这也符合面对对象思想。

面试题

1.iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数。 1.willchangeValueForKey: 2.父类原来的setter 3.didChangeValueForKey:
  • 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)

2.如何手动触发KVO ?
手动调用willchangeValueForKey:和didChangeValueForKey:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self.person1 setAge:21];
    
    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];
}

既然调用didChangeValueForKey会去调用通知观察者,为什么还要调用willChangeValueForKey?必须要调用,有可能didChangeValueForKey里面会判断是否调用willChangeValueForKey

  1. 直接修改成员变量会触发KVO么?
    不会触发。