小码哥iOS学习笔记第四天: KVO的本质

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

一、KVO的使用

  • 新建工程, 定义Person类继承自NSObject, 并添加int类型的属性age
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

@implementation Person
@end
复制代码
  • ViewController中添加两个Person类型的属性person1person2, 并给person1添加监听age属性的观察者, 当点击屏幕时修改这两个对象的age属性值
@interface ViewController ()

@property (nonatomic, strong) Person *person1;

@property (nonatomic, strong) Person *person2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
}

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

/**
 当被观察的属性, 使用`set`方法赋值时, 触发观察者

 @param keyPath 被监听的属性
 @param object 添加监听的对象
 @param change 属性改变前后的值
 @param context 添加观察者时传入的参数
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@ - %@ - %@ - %@", object, keyPath, change, context);
}

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

@end
复制代码
  • 运行程序, 点击屏幕, 会有如下打印:
<Person: 0x60c000014b20> - age - {
    kind = 1;
    new = 21;
    old = 1;
} - 年龄
复制代码
  • 打印中只有person1的属性值发生改变信息, 而没有person2的属性值改变的信息

  • 这是因为给person1添加了观察者, 而person2没有添加

  • 对象添加KVO监听属性, 类似于下图

问: 为什么同样都是调用了setAge:方法, person1却能监听age的属性值改变?

二、添加KVO的对象的isa指针指向何处

  • 下图就是上面创建的工程, 我们在touchesBegan:withEvent:方法中
  • 打断点, 当点击控制器的view后, 查看person1person2isa指针指向何处

  • 根据结果可以知道

    • person1isa指向类对象NSKVONotifying_Person
    • person2isa指向类对象Person
  • 通过person1isa找到NSKVONotifying_Person后, 再次调用superclass, 可以看到NSKVONotifying_Person的父类是Person

  • 说明: 添加了观察者(KVO)的对象, 它的isa指针发生了改变, 指向了系统动态生成的子类NSKVONotifying_Person
  • 已经知道对象调用方法的过程:
    • 首先通过isa指针, 找到类对象
    • 在类对象中查找方法, 如果方法存在就会调用
  • 所以person1调用的setAge:方法, 是子类NSKVONotifying_Person中重写的setAge:方法
  • 这就是为什么, 明明person1person2都调用了setAge:方法, 而person1会有属性监听
1、未使用KVO监听的Person对象

2、使用KVO监听的Person对象

  • 上面是通过isa验证了person1指向了NSKVONotifying_Person类, 下面使用代码进行验证
  • 在给person1添加观察者的前后, 分别打印person1person2的类型
3、通过代码验证person1的类型是NSKVONotifying_Person
NSLog(@"%@ - %@",
      object_getClass(self.person1),
      object_getClass(self.person2));

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

NSLog(@"%@ - %@",
      object_getClass(self.person1),
      object_getClass(self.person2));
复制代码
  • 执行后打印如下:
Person - Person
NSKVONotifying_Person - Person
复制代码
  • 根据打印结果, 可以证明, 在给person1添加观察者之后, person1的类型是NSKVONotifying_Person

三、验证NSKVONotifying_PersonsetAge:的方法实现, 是_NSSetIntValueAndNotify函数

  • person1添加观察者的前后, 设置打印setAge方法地址的代码
NSLog(@"添加KVO之前 - %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:@"年龄"];

NSLog(@"添加KVO之后 - %p - %p",
      [self.person1 methodForSelector:@selector(setAge:)],
      [self.person2 methodForSelector:@selector(setAge:)]);
复制代码
  • 打印结果如下图:

  • 很明显, 在添加KVO的前后, person1调用的setAge:方法已经改变

  • 下面使用lldb打印一下setAge:方法

四、探索NSKVONotifying_Person类对象的isa, 指向何处

  • 在添加KVO前后, 添加如下代码, 打印person1的类对象和元类对象
NSLog(@"添加KVO之前 - %@ - %@",
      object_getClass(self.person1),
      object_getClass(object_getClass(self.person1)));

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

NSLog(@"添加KVO之前 - %@ - %@",
      object_getClass(self.person1),
      object_getClass(object_getClass(self.person1)));
复制代码
  • 打印结果如下

  • 由打印可知: NSKVONotifying_Person类对象的isa指向NSKVONotifying_Person的元类对象

五、通过逆向, 查看Fundation中的_NSSetIntValueAndNotify函数

  • 使用iFunBox查看越狱手机中的动态库文件

  • 我使用的iPhone6, 所以这里查看arm64架构下的动态库文件
  • dyld_shared_cache_arm64文件托至电脑(复制)

  • 通过终端, 使用dsc_extractordyld_shared_cache_arm64进行分解
# 终端命令
./dsc_extractor dyld_shared_cache_arm64 test
复制代码
  • 分解出的Fundation动态库如下

  • 接下来使用反编译工具 hopper, 对Fundation反编译
  • 使用hopper打开Fundation动态库文件

  • Nest

  • OK, 可以看到反编译成功

  • 搜索_NSSetIntValueAndNotify, 可以看到Fundation中确实有_NSSetIntValueAndNotify函数

  • 搜索ValueAndNotify, 可以看到有很多类似的方法

  • 根据搜索结果可以推断出, 对不同类型的属性添加观察, 就会调用对应属性类型的_NSSet*ValueAndNotify方法, *表示类型

  • 验证这个推断, 将Personage属性类型, 从int改为double

@interface Person : NSObject
@property (nonatomic, assign) double age;
@end
复制代码
  • 再次打印setAge:方法实现:

  • 根据结果, 验证推断正确

六、_NSSet*ValueAndNotify的内部实现

[self willChangeValueForKey:@"age"];
// 原来的setter实现
[self didChangeValueForKey:@"age"];
复制代码
  • 调用顺序:

    • 调用willChangeValueForKey:
    • 调用原来的setter实现
    • 调用didChangeValueForKey:
      • didChangeValueForKey:内部会调用observeValueForKeyPath:ofObject:change:context:
  • 通过代码验证, 在Person.m中手动实现willChangeValueForKeydidChangeValueForKey以及setAge:方法, 代码如下:

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

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

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}
@end
复制代码
  • 运行程序, 点击控制器的View, 修改person1age属性, 有如下打印:
// 1
willChangeValueForKey - begin
// 2
willChangeValueForKey - end
// 3
setAge:
// 4
didChangeValueForKey - begin
// 5
<Person: 0x60800001a510> - age - {
    kind = 1;
    new = 21;
    old = 1;
} - 年龄
// 6
didChangeValueForKey - end
复制代码
  • 根据打印: 可以证明以上的_NSSet*ValueAndNotify的内部实现

七、子类内部的方法

  • 使用KVO监听的Person对象的图片中, NSKVONotifying_Person的类对象中, 一共有两个指针isasuperclass, 四个方法setAge:, class, dealloc_isKVOA

  • 下面使用Runtime代码, 来验证NSKVONotifying_Person中确实存在这四个方法

  • 首先在ViewController中添加下面的方法:

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [array addObject:methodName];
    }
    NSLog(@"%@ - %@", cls, array);
}
复制代码
  • 同时删除Person.m中的所有方法
@implementation Person

@end
复制代码
  • 接着在给person1添加观察者的后面调用方法, 传入cls
self.person1 = [[Person alloc] init];
self.person1.age = 1;

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

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

[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
复制代码
  • 运行程序后, 有以下打印:
NSKVONotifying_Person - (
    "setAge:",
    class,
    dealloc,
    "_isKVOA"
)
Person - (
    "setAge:",
    age
)
复制代码
  • 可以看到NSKVONotifying_Person中有四个方法:
    • setAge:
    • class
    • dealloc
    • _isKVOA
1、推断NSKVONotifying_Personclass方法的实现
  • 首先在给person1添加观察者的后面添加打印[self.person1 class]的代码
self.person1 = [[Person alloc] init];
self.person1.age = 1;

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

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

NSLog(@"%@", [self.person1 class]);
复制代码
  • 执行后, 打印结果如下:
// 打印结果:
Person
复制代码
  • 打印结果是Person, 而person1的isa指向是NSKVONotifying_Person, 这说明在NSKVONotifying_Person中, 对class进行了重写
  • 现在推断NSKVONotifying_Person中的clss方法实现如下:
- (Class)class {
    return [Preson class];
}
复制代码

2、关于dealloc_isKVOA方法

  • 由于没办法看到NSKVONotifying_Person中具体的源码, 所以只能模糊推断
  • 因为NSKVONotifying_Person是为了观察age属性, 才创建出来的, 所以在dealloc中会进行一些结尾操作
  • _isKVOA方法, 则推断为:
- (BOOL)_isKVOA {
    return YES
}
复制代码

八、面试题

1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)
  • 利用RuntimeAPI动态生成一个子类, 并且让instance对象的isa指向这个全新的子类
  • 当修改instance对象的属性时, 会调用Fundation的_NSSet*ValueAndNotify函数
    • willChangeValueForKey:
    • 父类原来的setter方法
    • didChangeValueForKey:
      • 内部会触发监听器Observer的监听方法(observeValueForKeyPath:ofObject:change:context:)
2、如果直接修改对象的成员变量, 是否会触发监听器的(observeValueForKeyPath:ofObject:change:context:)方法?
  • Person类的_age暴露出来
@interface Person : NSObject
{
    @public
    int _age;
}
@property (nonatomic, assign) int age;
@end
复制代码
  • ViewController中的touchesBegan:withEvent:方法修改如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1->_age = 21;
}
复制代码
  • 运行程序后, 发现并没有触发observeValueForKeyPath:ofObject:change:context:方法
  • 所以, 直接修改对象的成员变量, 而不调用set方法, 将不会触发观察者的observeValueForKeyPath:ofObject:change:context:方法
3、如何手动触发KVO?
  • 已知实例对象被观察的属性, 在调用set方法进行修改时, 会触发_NSSet*ValueAndNotify函数
  • 并触发willChangeValueForKey:didChangeValueForKey:这两个方法, 所以我们可以手动添加这两个方法, 来触发KVO
  • 现在已知直接修改成员变量时, 不会触发KVO, 那么就在修改成员变量的前后添加这两个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.person1 willChangeValueForKey:@"age"];
    self.person1->_age = 21;
    [self.person1 didChangeValueForKey:@"age"];
}
复制代码
  • 运行程序, 点击ViewController的view, 有如下打印:
<Person: 0x60000001c6c0> - age - {
    kind = 1;
    new = 21;
    old = 1;
} - 年龄
复制代码
  • 所以, 通过调用willChangeValueForKey:didChangeValueForKey:方法, 就可以手动的调用KVO

注意:
willChangeValueForKey:didChangeValueForKey:, 两个方法必须同时出现, 如果只有一个, 将不会触发KVO

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1->_age = 21;
    [self.person1 didChangeValueForKey:@"age"];
}
复制代码
  • 运行程序, 点击屏幕后, 没有任何打印
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值