KVO的原理探索

/ 1

概述

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

基本使用

使用 KVO 监听 person 对象的 age 属性改变:

@interface JFPerson : NSObject
@property (nonatomic, assign) int age;
@end

@implementation JFPerson
@end

@interface ViewController ()
@property (nonatomic, strong) JFPerson *person;
@end

@implementation ViewController

- (void)dealloc
{
    // 移除KVO监听
    [self.person removeObserver:self forKeyPath:@"age"];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[JFPerson alloc] init];
    self.person.age = 10;

    [self setupPersonAgeObserving];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // .语法修改属性值,相当于执行了setter方法 [self.person setAge:20];
    self.person.age = 20;
}

// 监听person对象的age属性
- (void)setupPersonAgeObserving
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}

// 监听到属性改变后,就会执行
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性改变:%@", object, keyPath, change);

    NSLog(@"new=%@", change[NSKeyValueChangeNewKey]);
    NSLog(@"old=%@", change[NSKeyValueChangeOldKey]);
}

@end

修改 person 对象的 age 属性,打印日志:

监听到<JFPerson: 0x60000042c7e0>的age属性改变:{
    kind = 1;
    new = 20;
    old = 10;
}
new=20
old=10

KVO实现原理

创建两个对象,只给 person1 添加 KVO 监听:

self.person1 = [[JFPerson alloc] init];
self.person2 = [[JFPerson alloc] init];

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

然后通过 lldb 打印 isa,发现添加 KVO 监听后的对象的 类对象 改变了:

(lldb) p self.person1->isa
(Class) $0 = NSKVONotifying_JFPerson
(lldb) p self.person2->isa
(Class) $1 = JFPerson

其实 NSKVONotifying_JFPersonRuntime 动态创建的一个继承自 JFPerson 的子类。

NSKVONotifying_JFPerson 类中,重写了 setAge: 方法,下面是大概伪代码:

@implementation NSKVONotifying_JFPerson

- (void)setAge:(int)age
{
    // 调用Foundation框架中的函数
    _NSSetIntValueAndNotify();
}

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

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

@end

手动触发KVO

只要手动调用 willChangeValueForKeydidChangeValueForKey 方法,就可以触发 KVO

[self.person willChangeValueForKey:@"age"];
self.person->_age = 10;
[self.person didChangeValueForKey:@"age"];

KVO验证

只对 person1 添加 KVO 监听,打印 setAge: 函数的实现(IMP)的地址:

NSLog(@"%p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

// 0x1b15d7280 0x102d861e8

然后通过 lldb 可以看出添加了 KVO 监听后,setAge: 实现变成了 Foundation 框架中的 _NSSetIntValueAndNotify 函数:

(lldb) p (IMP)0x1b15d7280
(IMP) $0 = 0x00000001b15d7280 (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP)0x102d861e8
(IMP) $1 = 0x0000000102d861e8 (KVO的本质`-[JFPerson setAge:] at JFPerson.m:12)

提取Foundation框架

越狱手机,可以通过爱思助手从下面路径获取到 动态库共享缓存 文件:

/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e

iOS 系统中,是通过 usr/lib/dyld 来加载动态库的,我们可以从 dyld 的源码中获取到提取动态库的代码,自己编译一个提取工具。

源码地址:https://opensource.apple.com/tarballs/dyld/

下载源码后,找到 dyld3/shared-cache/dec_extractor.cpp 文件,保留红框部分的源码,其他全删除:

然后通过 clang++ 编译,最终得到可执行文件 dsc_extractor

clang++ -o dsc_extractor dsc_extractor.cpp

开始提取:

./dsc_extractor dyld_shared_cache_arm64e test

提取完成后,就可以获得所有动态库的二进制文件,也就是 mach-o 文件:

然后通过反编译工具,比如 IDAHopper,反编译 Foundation 后,就可以找到方法和函数的实现的汇编代码:

通过伪代码也可以大致分析出函数体或方法体里的逻辑。

打印方法名

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 遍历所有方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"methodName=%@", methodName);
    }
    free(methodList);
}

添加 KVO 前后分别调用上面方法,传入类对象:

// 添加KVO前
methodName=age
methodName=setAge:

// 添加KVO后
methodName=setAge:
methodName=class
methodName=dealloc
methodName=_isKVOA