深入理解KVO

2018-09-15  本文已影响56人  03a336dff0ec

iOS | KVO | Objective-C

KVO的本质是什么,如何手动触发KVO?


1.什么是KVO

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变

添加监听:

typedef enum NSKeyValueObservingOptions : NSUInteger {
    // 新值(包含于回调方法change字典中)
    NSKeyValueObservingOptionNew = 0x01,
    // 旧值(包含于回调方法change字典中)
    NSKeyValueObservingOptionOld = 0x02,
    // 观察最初的值(在注册观察服务时会调用一次触发方法)
    NSKeyValueObservingOptionInitial = 0x04,
    // 分别在值修改前后触发方法(即一次修改有两次触发)
    NSKeyValueObservingOptionPrior = 0x08
} NSKeyValueObservingOptions;

/**
监听属性方法,方法调用者为被观察对象

@param observer 观察者/订阅者
@param keyPath  要观察的属性 
@param options  监听变化条件
@param context  上下文,将会传递到监听回调函数中
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

监听回调:

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

移除监听:

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

KVO使用大家都比较熟悉,Demo应该就没有写的必要了,下面我们直接来探索下本质

2.KVO的本质

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.person1 = [[Person alloc] init];
    self.person1.age = 10;

    self.person2 = [[Person alloc] init];
    self.person2.age = 15;

    // 输出 >> 监听之前:Person, Person
    NSLog(@"监听之前:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
    // 输出 >> 监听之前 setter 方法:0x10d2fb550, 0x10d2fb550
    NSLog(@"监听之前 setter 方法:%p, %p",
    [self.person1 methodForSelector:@selector(setAge:)],
    [self.person2 methodForSelector:@selector(setAge:)]);

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

    // 输出 >> 监听之后:NSKVONotifying_Person, Person
    NSLog(@"监听之前:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
    // 输出 >> 监听之后 setter 方法:0x10d643bf4, 0x10d2fb550
    NSLog(@"监听之后 setter 方法:%p, %p",
    [self.person1 methodForSelector:@selector(setAge:)],
    [self.person2 methodForSelector:@selector(setAge:)]);

    // (lldb)po ([NSKVONotifying_Person class]).superclass Person
    // (lldb) p (IMP)0x10d2fb550 (IMP) $0 = 0x000000010d2fb550 (KVO与KVC`-[Person setAge:] at Person.h:13)
    // (lldb) p (IMP)0x10d643bf4 (IMP) $1 = 0x000000010d643bf4 (Foundation`_NSSetLongLongValueAndNotify)
}

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到:%@ 的:%@ 属性值改变了,change:%@ - context:%@", object, keyPath, change, context);
}

通过断点调试,我们发现 person2 的类对象没有发生变化,person1 的类对象变成了 NSKVONotifying_Person,而且是Person的子类。

使用 Runtime 打印 NSKVONotifying_Person 方法列表:

    unsigned int methCount = 0;
    NSMutableArray *methodArr = [NSMutableArray array];
    Method *meths = class_copyMethodList([NSClassFromString(@"NSKVONotifying_Person") class], &methCount);
    for(int i = 0; i < methCount; i++) {
        Method meth = meths[i];
        SEL sel = method_getName(meth);
        const char *name = sel_getName(sel);
        [methodArr addObject:[NSString stringWithUTF8String:name]];
    }
    if (methodArr.count) NSLog(@"%@", methodArr);
    free(meths);

总结:
1> 添加监听时,Runtime动态生成 NSKVONotifying_Person类,并且使 person1 的 isa 指针指向新的类
2> 重写 setAge: ,person1 调用 setter 方法时会从 NSKVONotifying_Person 开始查找,在自己的类对象中能够找到,所以会调用自己的 setAge:方法( 会调用Foundation的_NSSetValueAndNotify函数)
3> _NSSet
ValueAndNotify 调用流程:willChangeValueForKey -> [super setAge:] (Person 的 setter 方法) -> didChangeValueForKey(同时触发 observeValueForKeyPath 监听回调方法,订阅者接收)
4> class 方法:重写 class 方法的目的是什么呢?(lldb) po self.person1.class 输出为:Person,原来,苹果粑粑是想要隐藏NSKVONotifying_Person,让开发者无感,使用时与未添加监听时无异
5> dealloc方法:释放 KVO 新产生的资源
6> _isKVOA方法:标记这个新类 KVO 机制新建的

willChangeValueForKey/didChangeValueForKey还有疑惑的同学可以在 Person.m 中对这两个方法进行重写,再进行调试以变理解。

上一篇下一篇

猜你喜欢

热点阅读