OC目标 – KVO

俗称“键值监听” ,用来监听某个特点值的改变

1. KVO基本运用

1.1 简略的KVO

  • 首要咱们新建一个iOS的App项目
  • 新建ZSXPerson
@interface ZSXPerson : NSObject
@property (nonatomic, assign) int age;
@end
@implementation ZSXPerson
@end
  • viewController 中创立ZSXPerson特点并初始化,然后增加KVO监听实例目标的age改变,接着通过 touchesBegan 点击屏幕的时分修正age值,来触发 KVO
#import "ViewController.h"
#import "ZSXPerson.h"
@interface ViewController ()
@property (nonatomic, strong) ZSXPerson *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    _person1 = [[ZSXPerson alloc] init];
    [_person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"点击了屏幕");
    _person1.age = 20;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"监听到 %@的%@的值发生改变:%@", object, keyPath, change);
}
- (void)dealloc {
    [_person1 removeObserver:self forKeyPath:@"age"];
}
@end
  • 运行项目,点击屏幕,查看控制台输出

OC目标 - KVO

  • 现在咱们已经运用 KVO 正常监听 person 的 age 值改变

2. 调查给目标增加kvo监听和没有增加kvo监听的差异

2.1 增加person2目标

咱们再增加一个person2特点并初始化,同样在点击屏幕的时分,给person2.age也赋值

OC目标 - KVO

此时点击屏幕,控制台的确只输出 person1 目标的监听改变

2.2 思考

  • 咱们对person1和person2仅仅做了同样了age赋值操作,kvo是怎样做到增加了监听的目标才触发observeValue呢?

3. KVO底层完成探求

3.1 增加KVO前后,setAge办法发生了什么改变?

在增加KVO前后,别离打印一下办法完成的地址

OC目标 - KVO
发现person1 的 setAge 办法在增加完 KVO 监听后改变了,person2 是没有改变的

3.2 增加KVO前后,setAge究竟走了什么办法

咱们打印一下办法

  • 直接打印办法地址看不到具体办法名,运用(IMP)强转一下

OC目标 - KVO
能够调查到

  • 未增加KVO监听的 setAge 办法是-[ZSXPerson setAge:]
  • 增加KVO监听后 setAge 办法是 Foundation_NSSetIntValueAndNotify`

3.2.1 其他数据类型

如果咱们还有一个double类型的特点

OC目标 - KVO
setHeight办法终究会是_NSSetDoubleValueAndNotify

3.3 查看isa

前面学过isa的指向,咱们知道目标办法是寄存类目标中的,既然两个目标的setAge目标办法不一样,那是不是他们isa指向的类目标也不一样呢

3.3.1 控制台打印

OC目标 - KVO
的确他们isa指向的类目标不是同一个,_person1在这边没有显现类目标称号,_person2能够看出来是ZSXPerson

3.4 运用runtime打印类目标

3.4.1通过runtime打印_person1的类目标:NSKVONotifying_ZSXPerson

OC目标 - KVO

3.4.2 打印_person1的类目标的superclass

一起还发现,NSKVONotifying_ZSXPerson的superclass就是ZSXPerson

OC目标 - KVO

3.5 结论

增加KVO后

  • 利用RuntimeAPI动态生成一个子类,而且让instance目标的isa指向这个全新的子类NSKVONotifying_ClassName
  • 当修正instance目标的特点时,会调用Foundation的_NSSetXXXValueAndNotify函数

3.6 _NSSetXXXValueAndNotify办法做了什么

_NSSetXXXValueAndNotifyFoundation结构的东西,由于无法拿到Foundation的源码,可是能够通过一些逆向的手法,得到_NSSetXXXValueAndNotify办法实际执行的伪代码

3.6.1 _NSSetXXXValueAndNotify

调用setAge办法时,_NSSetXXXValueAndNotify办法里边做的内容能够认为是这样:

  • [self willChangeValueForKey:@”age”];
  • [super setAge:age];
  • [self didChangeValueForKey:@”age”];
  • didChangeValueForKey 办法会调用监听器的(observeValueForKeyPath:ofObject:change:context:)办法

3.7 NSKVONotifying_ClassName类目标里边还有什么办法

咱们遍历打印一下NSKVONotifying_ClassName里边,看看它还有什么办法

- (void)printMethodNameOfClass:(Class)cls {
    unsigned int count;
    // 取得办法数组
    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 appendFormat:@"%@", methodName];
        [methodNames appendFormat:@", "];
    }
    // 开释
    free(methodList);
    // 打印办法名
    NSLog(@"%@ %@", cls, methodNames);
}

点击屏幕的时分,咱们调用一下打印

OC目标 - KVO

3.7.1 setAge:

会调用Foundation_NSSetIntValueAndNotify`

3.7.2 class:

你会发现,目标增加KVO监听后,isa只想了一个新的子类NSKVONotifying_ZSXPerson,可是咱们在运用[person1 class]获取类目标的时分,回来的依然是ZSXPerson

这是由于:

苹果底层设计时,为了屏蔽内部完成,让开发者运用过程中,不会忽然看到一个反常的东西,避免想入非非

咱们能够认为NSKVONotifying_ZSXPerson类重写了 class 办法如下

- (Class)class {
    return [ZSXPerson class];
}

3.7.2 dealloc:

能够认为里边就是做了一些跟KVO开释有关的收尾操作

  • (void)dealloc { // 收尾作业 }

3.7.2 _isKVOA:

回来是否是KVO的相关类

- (BOOL)_isKVOA {
    return YES;
}

4. 总结

4.1 未运用KVO监听的目标

OC目标 - KVO

4.2 运用了KVO监听的目标

OC目标 - KVO

4.3 增加KVO后

  • 利用RuntimeAPI动态生成一个子类,而且让instance目标的isa指向这个全新的子类NSKVONotifying_ClassName
  • 当修正instance目标的特点时,会调用Foundation的_NSSetXXXValueAndNotify函数
    • willChangeValueForKey:
    • 父类原来的setter
    • didChangeValueForKey: 内部会触发监听器(Oberser)的监听办法( observeValueForKeyPath:ofObject:change:context:)

扩展

手动触发KVO

  • 碰过这样一道面试题:运用下划线直接拜访成员变量的方式给变量赋值,会不会触发KVO监听?

答案是:不会。由于KVO重写的是set办法(setAge:)。直接给成员变量赋值不会走set办法,因此也不会触发KVO监听

  • 然后又会问,那能不能手动触发KVO监听?

手动触发

只要手动调用实例目标的willChangeValueForKey:didChangeValueForKey办法,就能触发

OC目标 - KVO

注意

需求别离调用willChangeValueForKey:didChangeValueForKey办法才会触发KVO

  • 内部完成在执行didChangeValueForKey办法的时分,会判别前面是否执行了willChangeValueForKey:办法,前面有调用过才会触发KVO 监听
    OC目标 - KVO

@oubijiexi