iOS 底层OC语法3(探索KVO的本质)

代码地址git

KVO 的用法

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

- (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;
}

复制代码

KVO 疑问 (都调用set方法,可是触发的方法不同)

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

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

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

KVO底层实现分析 (添加KVO 后新生成的类,是元类的子类,重写了原类的set方法)

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

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

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

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

那么如何验证KVO真的如上面所讲的方式实现 (添加KVO 先后,打印set方法的实现地址)

首先通过以前打断点打印isa指针,咱们已经验证了,在执行添加监听的方法时,会将isa指针指向一个经过runtime建立的Person的子类NSKVONotifyin_Person。 另外咱们能够经过打印方法实现的地址来看一下p1和p2的setage的方法实现的地址在添加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监听以前,p1和p2的setAge方法实现的地址相同,而通过KVO监听以后,p1的setAge方法实现的地址发生了变化,咱们经过打印方法实现来看一下先后的变化发现,确实如咱们上面所讲的同样,p1的setAge方法的实现由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_Person同Person类的差异可能就在于其内存储的对象方法及实现不一样。 咱们经过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: class dealloc _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,而nsobect的class的实现大体为返回本身isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,可是apple不但愿将NSKVONotifyin_Person类暴露出来,而且不但愿咱们知道NSKVONotifyin_Person内部实现,因此在内部重写了class类,直接返回Person类,因此外界在调用p1的class对象方法时,是Person类。这样p1给外界的感受p1仍是Person类,并不知道NSKVONotifyin_Person子类的存在。 那么咱们能够猜想NSKVONotifyin_Person内重写的class内部实现大体为

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

复制代码

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

咱们在Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟他们的实现。

- (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方法内部已经调用了observer的observeValueForKeyPath:ofObject:change:context:方法。

手动触发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的值并无发生改变。

KVC

什么是KVC

KVC,俗称“键值编码”,全称是“Key Value Coding”,它是一种能够直接经过字符串的名称(Key)来访问类属性的机制,而不是经过调用Setter或者Getter方法来进行访问。

KVC的经常使用方法以下 (keyPath 赋值的层次比较深)

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (nullable id)valueForKeyPath:(NSString *)keyPath;


复制代码

KVC底层原理

setValue:forkey:赋值流程

  • 首先会按照setKey:、_setKey:的顺序到对象的方法列表中寻找这两个方法,若是找到了方法,则传参而且调用方法。
  • 若是没有找到方法,则经过accessInstanceVariablesDirectly方法的返回值来决定是否可以查找成员变量。若是accessInstanceVariablesDirectly返回YES,则会按照如下顺序到成员变量列表中查找对应的成员变量:

_key _isKey key isKey

  • 若是accessInstanceVariablesDirectly返回NO,则直接抛出NSUnknownKeyException异常。
  • 若是在成员变量列表中找到对应的属性值,则直接进行赋值,若是找不到,则会抛出NSUnknownKeyException异常。

对应流程图以下:

valueForKey:取值流程

  • 首先会按照如下顺序查找方法列表

getKey key isKey _key

  • 若是找到就直接传递参数,调用方法,若是未找到则查看accessInstanceVariablesDi* rectly方法的返回值,若是返回NO,则直接抛出NSUnknownKeyException异常 若是accessInstanceVariablesDirectly方法返回YES,则按以下顺序查找成员变量列表

_key _isKey key isKey

  • 若是能找到对应的成员变量,则直接获取成员变量的值,若是未找到,则抛出NSUnknownKeyException异常。

流程图以下:

小结

KVO的本质是什么?

  • 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改成指向一个全新的经过Runtime动态建立的子类,子类重写了set方法实现。

set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法 。

如何手动触发KVO

要咱们本身调用willChangeValueForKey和didChangeValueForKey方法便可在不改变属性值的状况下手动触发KVO,而且这两个方法缺一不可。

经过KVC修改属性会触发KVO么?

会触发KVO,由于KVC是调用set方法,KVO就是监听set方法

相关文章
相关标签/搜索