iOS KVO本质探索

KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。

KVO使用步骤

1.注册观察者(为被观察这指定观察者以及被观察者属性)
创建一个Person对象,写一个age属性,为age属性添加KVO监听

/* 
options: 有4个值,分别是:
NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法 
NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法 
NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。 
NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。 
 */
//注册一个监听器用于监听指定的key路径
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];

2.实现回调方法

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

3.修改需要监听的属性值,查看是否监听成功

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person1.age = 10;
}
使用注意点

[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"] ;这行代码中context:@"123"这个参数传的值都会
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{}传到回调方法中context中来,用途: 如果说在一个控制器中我们监听了多个属性的变化,而每一个属性的变化之后对应的事件处理也是不一样的,那么在注册观察者时context 传不同的字符串就可以区分判断了

思考:为什么注册观察者之后,修改观察对象属性的值,就会走回调方法?KVO是怎么实现这个功能的呢?
带着这两个问题下面来一探究竟
  • OC是消失机制当我们调用 self.person1.age = 10; 这行代码时实际就是[self.person setAge:10] 然后再转化成objc_msgsend(person,@selector(setAge)),那么是不是KVO在setAge 这个方法中做了文章呢?前几篇文章已经写到对象方法存储到类对象中,那么在控制台输入 po self.person.isa 发现结果并不是 Person 而是 NSKVONotifying_Person 这说明KVO在运行时新建了一个NSKVONotifying_Person类,将person的isa指针指向这个类, 在控制台输入 po [self.person class]发现输出的是Person而不是NSKVONotifying_Person。这是为什么?

猜测:系统在运行时,KVO动态创建一个NSKVONotifying_Person类,将person的isa指针指向这个类。新创建的NSKVONotifying_Person 继承与Person 并且重写了 setAge方法和 class方法

验证1:NSKVONotifying_Person是否继承自Person,是否重写了Class方法
#import 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 20;
    //获取类对象
    Class personClass = object_getClass(self.person1);
    //通过类对象获取父类
    Class personClass02 = class_getSuperclass(personClass);
    NSLog(@"%@-------%@",personClass02,personClass03);
}
输出 NSKVONotifying_Person-------Person

通过这几行代码就可以证明NSKVONotifying_Person 继承自 Person

  • 细节
    再获取类对象时可以使用object_getClass 也可以使用[A class],上面为什么使用了object_getClass而不是使用[A class]呢?
    答案: NSKVONotifying_Person 重写了class方法,[person class]返回的并不是原对象而是原对象的父类也就是Person类,如果没有重写的话,返回person的isa指针指向的类打印结果应该是NSKVONotifyin_Person,但是苹果官方不希望将NSKVONotifyin_Person类的内部实现暴露出来,所以在内部重写了class方法,直接返回Person类,所以我们在调用person的class方法时,返回的是Person类。
验证2:NSKVONotifying_Person是否重写了setAge方法
  //新建另外一个person2对象,不添加监听
 self.person2 = [[Person alloc] init];
 self.person2.age = 2;

//点击修改person2的值
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
    Class personClass = object_getClass(self.person);
    Class personClass02 = object_getClass(personClass);
    Class personClass03 = class_getSuperclass(personClass);
    
    //修改person2的值
    self.person2.age = 20;
}

答案:person2 在调用setAge方法时并没有触发回调方法,而添加了观察者的person对象在调用setAge 方法时触发了方法,说明NSKVONotifying_Person 重写了setAge方法

思考1:NSKVONotifying_Person怎么重写了setAge方法以实现触发回调

经过查看底层源码和相关资料分析们可以知道,NSKVONotifyin_Person中的setAge方法中其实调用了Fundation框架中C语言函数_NSsetIntValueAndNotify,而_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey方法,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey方法。其中didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法。

思考2:NSKVONotifyin_Person的内部结构是怎样的?

NSKVONotifyin_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部的setAge方法做了单独的实现。我们可以通过runtime的方法去分别打印person1person2两个对象和NSKVONotifyin_Person类对象内存储的对象方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
// 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];

    [self printMethods: object_getClass(self.person1)];
    [self printMethods: object_getClass(self.person2)];

    [self.person1 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);
}

打印输出:

Person : setAge:, age,
NSKVONotifying_Person : setAge:, class, dealloc, _isKVOA,

NSKVONotifyin_Person的内存结构及方法调用顺序


NSKVONotifyin_Person的内存结构及方法调用顺序图解
总结:

1、KVO的本质是什么?
当我们给对象注册一个观察者添加了KVO监听时,系统会修改这个对象的isa指针指向。在运行时,动态创建一个新的子类,NSKVONotifying_A类,将A的isa指针指向这个子类,来重写原来类的set方法;set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
2、如何手动触发KVO?
实现调用willChangeValueForKey和didChangeValueForKey方法。

如有疑问:
iOS OC对象的本质窥探(一)
iOS OC对象的本质窥探(对象分类)(二)

特别推荐:
iOS获取手机唯一标示
iOS 高德地图实现大头针展示,分级大头针,自定制大头针,在地图上画线,线和点共存,路线规划(驾车路线规划),路线导航,等一些常见的使用场景

你可能感兴趣的:(iOS KVO本质探索)