iOS底层原理 - 探寻KVO本质

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVObash

首先须要了解KVO基本使用,KVO的全称 Key-Value Observing,俗称“键值监听”,能够用于监听某个对象属性值的改变。app

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 监听 p1的 age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

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

// 打印内容
监听到<Person: 0x604000205460>的age改变了{
    kind = 1;
    new = 10;
    old = 2;
}
复制代码

上述代码中能够看出,在添加监听以后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。框架

一: 探寻KVO底层实现原理

经过上述代码咱们发现,一旦age属性的值发生改变时,就会通知到监听者,而且咱们知道赋值操做都是调用set方法,咱们能够来到Person类中重写ageset方法,观察是不是KVOset方法内部作了一些操做来通知监听者。函数

咱们发现即便重写了set方法,p1对象和p2对象调用一样的set方法,可是咱们发现p1除了调用set方法以外还会另外执行监听器的observeValueForKeyPath方法。ui

说明KVO在运行时获取对p1对象作了一些改变。至关于在程序运行过程当中,对p1对象作了一些变化,使得p1对象在调用setAge:方法的时候可能作了一些额外的操做,因此问题出在对象身上,两个对象在内存中确定不同,两个对象可能本质上并不同。接下来来探索KVO内部是怎么实现的。spa

二: KVO底层实现分析

首先咱们对上述代码中添加监听的地方打断点,看观察一下,addObserver方法对p1对象作了什么处理?也就是说p1对象在通过addObserver方法以后发生了什么改变,咱们经过打印isa指针以下图所示命令行

经过上图咱们发现,p1对象执行过addObserver操做以后,p1对象的isa指针由以前的指向类对象Person变为指向NSKVONotifyin_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听之后,其isa指针就会发生变化,所以set方法的执行效果就不同了。指针

那么咱们先来观察p2对象在内容中是如何存储的,而后对比p2来观察p1。 首先咱们知道,p2在调用setAge:方法的时候,首先会经过p2对象中的isa指针找到Person类对象,而后在类对象中找到setAge:方法。而后找到方法对应的实现。以下图所示code

可是刚才咱们发现p1对象的isa指针在通过KVO监听以后已经指向了NSKVONotifyin_Person类对象,NSKVONotifyin_Person实际上是Person的子类,那么也就是说其superclass指针是指向Person类对象的,NSKVONotifyin_Personruntime在运行时生成的。那么p1对象在调用setAge:方法的时候,确定会根据p1的isa找到NSKVONotifyin_Person,在 NSKVONotifyin_Person中找setAge:的方法及实现。orm

通过查阅资料咱们能够了解到。 NSKVONotifyin_Person中的setAge方法中其实调用了Fundation框架中C语言函数 _NSsetIntValueAndNotify_NSsetIntValueAndNotify内部作的操做至关于,首先调用willChangeValueForKey 将要改变方法,以后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。

三: 那么如何验证KVO真的如上面所讲的方式实现?

首先通过以前打断点打印isa指针,咱们已经验证了,在执行添加监听的方法时,会将isa指针指向一个经过runtime建立的Person的子类NSKVONotifyin_Person。 另外咱们能够经过打印方法实现的地址来看一下p1p2setAge:的方法实现的地址在添加KVO先后有什么变化

// 经过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听以前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加KVO监听以后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
复制代码

咱们发如今添加KVO监听以前,p1p2setAge:方法实现的地址相同,而通过KVO监听以后,p1setAge方法实现的地址发生了变化,咱们经过打印方法实现来看一下先后的变化发现,确实如咱们上面所讲的同样,p1setAge方法的实现由Person类方法中的setAge方法转换为了C语言的Foundation框架的_NSsetIntValueAndNotify函数。

Foundation框架中会根据属性的类型,调用不一样的方法。例如咱们以前定义的int类型的age属性,那么咱们看到Foundation框架中调用的_NSsetIntValueAndNotify函数。那么咱们把age的属性类型变为double从新打印一遍

咱们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么这说明Foundation框架中有许多此类型的函数,经过属性的不一样类型调用不一样的函数。 那么咱们能够推测Foundation框架中还有不少例如_NSSetBoolValueAndNotify_NSSetCharValueAndNotify_NSSetFloatValueAndNotify_NSSetLongValueAndNotify等等函数。 咱们能够找到Foundation框架文件,经过命令行查询关键字找到相关函数

四: NSKVONotifyin_Person内部结构是怎样的?

首先咱们知道,NSKVONotifyin_Person做为Person的子类,其superclass指针指向Person类,而且NSKVONotifyin_Person内部必定对setAge方法作了单独的实现,那么NSKVONotifyin_PersonPerson类的差异可能就在于其内存储的对象方法及实现不一样。 咱们经过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的对象方法

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];
    p1.age = 1.0;
    Person *p2 = [[Person alloc] init];
    p1.age = 2.0;
    // self 监听 p1的 age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

    [self printMethods: object_getClass(p2)];
    [self printMethods: object_getClass(p1)];

    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void) printMethods:(Class)cls
{
    unsigned int count ;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString: methodName];
        [methodNames appendString:@" "];
        
    }
    
    NSLog(@"%@",methodNames);
    free(methods);
}
复制代码

上述打印内容以下

经过上述代码咱们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge:classdealloc_isKVOA, 那么至此咱们能够画出NSKVONotifyin_Person的内存结构以及方法调用顺序。

这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。咱们在p1添加过KVO监听以后,分别打印p1和p2对象的class能够发现他们都返回Person

NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印结果 Person,Person
复制代码

若是NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObjectclass的实现大体为返回本身isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,可是apple不但愿将NSKVONotifyin_Person类暴露出来,而且不但愿咱们知道NSKVONotifyin_Person内部实现,因此在内部重写了class类,直接返回Person类,因此外界在调用p1class对象方法时,是Person类。这样p1给外界的感受p1仍是Person类,并不知道NSKVONotifyin_Person子类的存在。

那么咱们能够猜想NSKVONotifyin_Person内重写的class内部实现大体为

- (Class) class {
     // 获得类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}
复制代码

五: 验证didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

咱们在Person类中重写willChangeValueForKeydidChangeValueForKey方法,模拟他们的实现。

- (void)setAge:(int)age
{
    NSLog(@"setAge:");
    _age = age;
}
- (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");
}
复制代码

再次运行来查看didChangeValueForKey的方法内运行过程,经过打印内容能够看到,确实在didChangeValueForKey方法内部已经调用了observerobserveValueForKeyPath:ofObject:change:context方法。

六: 回答问题:

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

** 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改成指向一个全新的经过Runtime动态建立的子类,子类拥有本身的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context监听方法。**

  • 如何手动触发KVO

被监听的属性的值被修改时,就会自动触发KVO。若是想要手动触发KVO,则须要咱们本身调用willChangeValueForKeydidChangeValueForKey方法便可在不改变属性值的状况下手动触发KVO,而且这两个方法缺一不可。

经过如下代码能够验证

Person *p1 = [[Person alloc] init];
p1.age = 1.0;
   
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
    
[p1 removeObserver:self forKeyPath:@"age"];
复制代码

经过打印咱们能够发现,didChangeValueForKey方法内部成功调用了observeValueForKeyPath:ofObject:change:context:,而且age的值并无发生改变。

相关文章
相关标签/搜索