细说OC中的KVO

Posted wangjunling888

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了细说OC中的KVO相关的知识,希望对你有一定的参考价值。

KVO这个特异功能相信很多人都应该熟知, 就算工作时没有用到, 那么面试的时候肯定被面试官提到过, 虽然算不上黑魔法, 但是了解一下实现原理, 对我们还是有很大帮助的, 下面笔者将一步一步深挖KVO的实现原理!

创建一个Person类, 添加一个属性name, 当然你可以添加任何你想要的属性, 这里笔者就用name来做演示使用

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

NS_ASSUME_NONNULL_END

//一下为实现文件, 代码内容暂时不要纠结, 接下来会细说

 //控制器中使用KVO
#import "ViewController.h"
#import "Person.h"
#import <objc/runtime.h>

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];
    [self test];


- (void)test 
    Person *person = [[Person alloc] init];
    person.name = @"lisi";
    [person addObserver:self forKeyPath:@"name"     options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:NULL];
    person.name = @"zhangsan";


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 
    if ([keyPath isEqualToString:@"name"]) 
          NSLog(@"%@", change);
    




以上代码运行结果如下

2018-09-05 09:06:04.437498+0800 KVODemo[29221:23851046] 
    kind = 1;
    new = zhangsan;
    old = lisi;

因为是在赋值的时候才能获取到通知, 所以首先应该想到, setter方法, 于是笔者给Setter方法加入log

运行结果如下

2018-09-05 09:19:01.981135+0800 KVODemo[29406:23866065] lisi
2018-09-05 09:19:01.981448+0800 KVODemo[29406:23866065] zhangsan
2018-09-05 09:19:01.981610+0800 KVODemo[29406:23866065] 
    kind = 1;
    new = zhangsan;
    old = lisi;

我们看到在第二次赋值时, 发出了通知, 那么是否是因为Setter方法不一样而导致的呢, 于是笔者加入了打印Setter方法地址的代码如下

- (void)test 
    Person *person = [[Person alloc] init];
    person.name = @"lisi";

    SEL sel = @selector(setName:);

    IMP imp = [person methodForSelector:sel];

    NSLog(@"添加KVO之前的setter地址%p", imp);


    [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:NULL];
    person.name = @"zhangsan";
    sel = @selector(setName:);
    imp = [person methodForSelector:sel];
    NSLog(@"添加KVO之后的setter地址%p", imp);

运行结果如下,

2018-09-05 09:26:02.811444+0800 KVODemo[29515:23878046] lisi
2018-09-05 09:26:02.811580+0800 KVODemo[29515:23878046] 添加KVO之前的setter地址0x1036dc570
2018-09-05 09:26:02.811835+0800 KVODemo[29515:23878046] zhangsan
2018-09-05 09:26:02.811994+0800 KVODemo[29515:23878046] 
    kind = 1;
    new = zhangsan;
    old = lisi;

2018-09-05 09:26:02.812079+0800 KVODemo[29515:23878046] 添加KVO之后的setter地址0x103a359fa

观察结果会发现两次的地址是不一样的, 那么可以确定第二次调用的setName: 方法肯定是变了, 但是明明是同一个对象的同一个方法, 为什么不一样呢, 所以我们要看看两个不同的时间节点两个对象的真是类型, 然后加入以下代码

- (void)test 
    Person *person = [[Person alloc] init];
    person.name = @"lisi";

    SEL sel = @selector(setName:);

    IMP imp = [person methodForSelector:sel];

    Class clazz = object_getClass(person);
    Class supClazz = class_getSuperclass(clazz);


    NSLog(@"添加KVO之前的类: %@, 父类:%@", clazz, supClazz);
    NSLog(@"添加KVO之前的setter地址%p", imp);


    [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:NULL];
    person.name = @"zhangsan";

    clazz =  object_getClass(person);
    supClazz = class_getSuperclass(clazz);

    NSLog(@"添加KVO之后的类名: %@, 父类:%@", clazz, supClazz);


    sel = @selector(setName:);
    imp = [person methodForSelector:sel];
    NSLog(@"添加KVO之后的setter地址%p", imp);


通过运行时方法 object_getClass() 获取运行时当前对象的类型, 并通过class_getSuperclass() 获取父类, 在加入观察者之前, 和之后分别调用以下, 得到以下结果

2018-09-05 10:49:34.127586+0800 KVODemo[30673:23964294] lisi
2018-09-05 10:49:34.127717+0800 KVODemo[30673:23964294] 添加KVO之前的类: Person, 父类:NSObject
2018-09-05 10:49:34.127802+0800 KVODemo[30673:23964294] 添加KVO之前的setter地址0x105bf7500
2018-09-05 10:49:34.128079+0800 KVODemo[30673:23964294] zhangsan
2018-09-05 10:49:34.128283+0800 KVODemo[30673:23964294] 
    kind = 1;
    new = zhangsan;
    old = lisi;

2018-09-05 10:49:34.128388+0800 KVODemo[30673:23964294] 添加KVO之后的类名: NSKVONotifying_Person, 父类:Person
2018-09-05 10:49:34.128468+0800 KVODemo[30673:23964294] 添加KVO之后的setter地址0x105f509fa

从运行结果中可以可以看到, 加入观察者之后, person的实际类型已经变了, 此时的person实际类型为NSKVONotifying_Person, 继承自Person, 到此不难得出一个结论, 即苹果在代码运行时创建了继承自Person类的NSKVONotifying_Person类用来实现对Person类属性的的监测的, 那么他是如何实现的呢? 接下来笔者将继续挖掘.

NSObject有两个方法 分别是 - (void)willChangeValueForKey:(NSString )key; 和- (void)didChangeValueForKey:(NSString )key ;

当有值发生变化的时候系统会调用这个两个方法

接下来我们重写Person类的这两个方法, 来验证KVO的调用过程, 代码如下

@implementation Person

- (void)setName:(NSString *)name 
    _name = name;
    NSLog(@"%@", name);


- (void)willChangeValueForKey:(NSString *)key 
    NSLog(@"willChange前");
    [super willChangeValueForKey:key];
    NSLog(@"willChange后");


- (void)didChangeValueForKey:(NSString *)key 
    NSLog(@"didChange前");
    [super didChangeValueForKey:key];
    NSLog(@"didChange后");


@end

运行结果如下

2018-09-05 13:22:24.169036+0800 KVODemo[32769:24109458] lisi
2018-09-05 13:22:24.169173+0800 KVODemo[32769:24109458] 添加KVO之前的类: Person, 父类:NSObject
2018-09-05 13:22:24.169239+0800 KVODemo[32769:24109458] 添加KVO之前的setter地址0x10e704330
2018-09-05 13:22:24.169510+0800 KVODemo[32769:24109458] willChange前
2018-09-05 13:22:24.169599+0800 KVODemo[32769:24109458] willChange后
2018-09-05 13:22:24.169655+0800 KVODemo[32769:24109458] zhangsan
2018-09-05 13:22:24.169726+0800 KVODemo[32769:24109458] didChange前
2018-09-05 13:22:24.169878+0800 KVODemo[32769:24109458] 
    kind = 1;
    new = zhangsan;
    old = lisi;

2018-09-05 13:22:24.169989+0800 KVODemo[32769:24109458] didChange后
2018-09-05 13:22:24.170089+0800 KVODemo[32769:24109458] 添加KVO之后的类名: NSKVONotifying_Person, 父类:Person
2018-09-05 13:22:24.170168+0800 KVODemo[32769:24109458] 添加KVO之后的setter地址0x10ea5d9fa

当name的值修改为zhangsan时, 系统生成一个子类, NSKVONotifying_Person, 并且重写了setName:, didChangeValueForKey: 以及class方法, 这样在调用setName的时候, 去调用didChangeValueForKey并添加观察者的通知, 而我们 调用[person class]时返回的是Person, 说明在子类中也重写了class方法并返回Person.

以上是对KVO的简单分析, 如有错误之处请各位读者不吝指教!

以上是关于细说OC中的KVO的主要内容,如果未能解决你的问题,请参考以下文章

OC中的KVO

细说KVO & KVC & NSNotificationCenter那些事

Swift中KVO(监听)的使用方法及注意事项

细说OC中的load和initialize方法

OC常用设计模式机制之KVO

[OC学习笔记]KVO原理