【OC语法】KVO的底层实现

目录
一、KVO是什么
二、怎么使用KVO
三、KVO的底层实现
四、KVO常见面试题


一、KVO是什么


KVO全称Key-Value Observing,翻译过来是键值观察,是一种用来观察某个对象属性值变化的机制。


二、怎么使用KVO


使用KVO只需要抓住三个关键词就可以了:被观察者是谁——即想要观察哪个对象哪个属性值的变化;观察者是谁——即想要让谁来观察,确定后就可以给对象添加KVO和移除对象的KVO了;观察者的回调方法——即当对象的属性值发生变化后要触发的方法。

举个简单例子:

假设我们有一个Person类,现在想要观察某个Person对象age属性值的变化。

// Person.h
#import 

@interface INEPerson : NSObject

@property (nonatomic, assign) NSInteger age;

@end

又有一个ViewController类,我们想要让ViewController来观察Person对象age属性值的变化,确定好观察者后,就可以给对象添加KVO和移除对象的KVO了。

// ViewController.m
#import "ViewController.h"
#import "INEPerson.h"

@interface ViewController ()

@property (nonatomic, strong) INEPerson *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[INEPerson alloc] init];
    self.person.age = 25;
    
    // 给person对象添加KVO
    [self.person addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:nil];
}

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

@end

方法解释:

/**
 *  给对象添加KVO
 *
 *  @param  observer    观察者
 *  @param  keyPath     观察者想要观察对象哪个属性值的变化
 *  @param  options     我们想要得到该属性变化后的新值还是旧值(如果想在添加观察者后,立即触发一次观察者的回调方法,可以在这里添上NSKeyValueObservingOptionInitial这个值)
 *  @param  context     额外信息,通常填nil
 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/**
 *  移除对象的KVO
 *
 *  @param  observer    观察者
 *  @param  keyPath     观察者观察的属性
 */
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

然后我们要为观察者实现一个回调方法,以便被观察对象的属性值发生变化后,观察者能够及时收到回调并做自定义的处理。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
    NSLog(@"观察到%@对象的%@属性的值发生了变化:%@", object, keyPath, change);
}

方法解释:

/**
 *  观察者的回调方法
 *
 *  @param  keyPath     观察者观察的属性
 *  @param  object      观察者观察的属性所属的对象
 *  @param  change      属性变化后的新值还是旧值都存在这里(我们还可以通过NSKeyValueChangeKindKey来判断新旧值的变化是重设、新增、替换还是移除)
 *  @param  context     额外信息,addObserver方法传过来是啥这里就是啥,通常是nil
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

经过以上对三个关键词的捕捉与实现,我们就完成了KVO的使用。现在模拟修改一下被观察对象的age属性。

// ViewController.m
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    self.person.age = 26;
}

可以看到控制台打印如下,这说明观察者的回调方法被成功触发了。

// 控制台打印
观察到对象的age属性的值发生了变化:{
    kind = 1;
    new = 26;
    old = 25;
}


三、KVO的底层实现


继续上面的例子:

现在我们创建两个Person对象,并且给person1添加KVO,person2不添加KVO。

// ViewController.m
#import "ViewController.h"
#import "INEPerson.h"

@interface ViewController ()

@property (nonatomic, strong) INEPerson *person1;
@property (nonatomic, strong) INEPerson *person2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[INEPerson alloc] init];
    self.person1.age = 25;
    // 给person对象添加KVO
    [self.person1 addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
    
    
    self.person2 = [[INEPerson alloc] init];
    self.person2.age = 26;
}

- (void)dealloc {
    // 移除person对象的KVO
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
    NSLog(@"观察到%@对象的%@属性的值发生了变化:%@", object, keyPath, change);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    self.person1.age = 26;
    self.person2.age = 27;
}

@end

当我们点击屏幕时,控制台打印如下。

// 控制台打印
观察到对象的age属性的值发生了变化:{
    kind = 1;
    new = 26;
    old = 25;
}

于是我们不禁要问,点击屏幕,person1person2同样是调用setAge:方法,为什么person1就能触发观察者的回调方法,person2就不行呢?我们知道person1person2的唯一区别就是person1添加了KVO,而person2没有添加KVO,那KVO到底对person1做了什么?因此我们需要看看KVO的底层实现是什么,或许能找到问题的答案。

1、结论

我们不妨把结论先摆在这里,然后再去验证。

当我们给某个对象添加KVO之后,KVO的底层实现其实就是:

  • 在运行时动态地创建一个类,这个类继承自原来那个类,并且把使用了KVO的对象的isa指针指向这个新类,也就是说这个对象其实已经不是原来那个类的实例了,而是新类的实例。
  • 然后这个新类还会重写原来类和NSObject类的若干个方法,其中我们最关心的就是被观察属性的setter方法被重写了。重写的setter方法内部主要做了三件事:首先调用willChangeValueForKey:方法表明将要修改属性的值,然后调用原来类的setter方法真正去修改成员变量的值,然后再调用didChangeValueForKey:方法表明属性的值修改完毕,而且didChangeValueForKey:方法内部还会让观察者调用观察者的回调方法。

这也就是解释了“点击屏幕,person1person2同样是调用setAge:方法,为什么person1就能触发观察者的回调方法,person2就不行”,就是因为它俩已经不是同一个类了,这两个类的setAge:方法的实现压根儿不一样。

以下是新类以及新类setter方法的伪代码。

// NSKVONotifying_INEPerson.h
#import "INEPerson.h"

@interface NSKVONotifying_INEPerson : INEPerson

@end


// NSKVONotifying_INEPerson.m
#import "NSKVONotifying_INEPerson.h"

@implementation NSKVONotifying_INEPerson

// 我们最关心的就是被观察属性的setter方法被重写了
- (void)setAge:(NSInteger)age {
    // 表明将要修改属性的值
    [self willChangeValueForKey:@"age"];
    
    // 真正去修改成员变量的值
    [super setAge:age];
    
    // 表明属性的值修改完毕
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key {
    // 让观察者调用回调方法,key就是上面传进来的age
    [observer observeValueForKeyPath:key ofObject:self change:@{@"new": ..., @"old": ..., ...} context:nil];
}

// 重写class方法,我们猜测的原因是苹果为了隐藏KVO的底层实现,让我们开发者感知不到这个新类的存在。这样在开发中,当我们获取某个添加了KVO的对象的类时就不会产生疑惑
- (Class)class {
    return [INEPerson class];
}

- (void)dealloc {
    // 做一些收尾工作
    // ...
}

- (BOOL)_isKVOA {
    // 是否使用了KVO
    return YES;
}

@end

所以在分析KVO的面试题时,我们只需要抓住KVO这两个本质的点就行了,即:

  • KVO会创建一个新类,继承自原来那个类,使用了KVO的对象会指向这个新类。
  • KVO会重写被观察属性的setter方法,里面做了三件事,并且正是在新setter方法里才触发了观察者的回调方法。

2、验证

  • 先验证第一点。
#import 

Class person1Class = object_getClass(self.person1);
Class person1SuperClass = class_getSuperclass(person1Class);
Class person2Class = object_getClass(self.person2);
Class person2SuperClass = class_getSuperclass(person2Class);

NSLog(@"person1所属的类及其父类:%@, %@", person1Class, person1SuperClass);
NSLog(@"person2所属的类及其父类:%@, %@", person2Class, person2SuperClass);

控制台打印如下。

person1所属的类及其父类:NSKVONotifying_INEPerson, INEPerson
person2所属的类及其父类:INEPerson, NSObject

可见我们给person1添加KVO之后,系统确实创建了一个继承自INEPerson的新类NSKVONotifying_INEPerson,并且也确实把person1isa指针指向了NSKVONotifying_INEPerson,也就是说person1已经不是INEPerson的实例了,而是NSKVONotifying_INEPerson的实例。而没添加KVO的person2还是INEPerson的实例,继承自NSObject

  • 再验证第二点。
#import 

- (NSArray *)instanceMethodListOfClass:(Class)cls {
    
    NSMutableArray *instanceMethodList = [@[] mutableCopy];
    
    unsigned int count;
    // 获取类的实例方法列表
    Method *methodList = class_copyMethodList(cls, &count);
    for (NSInteger i = 0; i < count; i++) {
        // 获取方法
        Method method = methodList[i];
        // 获取方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [instanceMethodList addObject:methodName];
    }
    // 释放
    free(methodList);
    
    return instanceMethodList;
}

NSArray *person1MethodList = [self instanceMethodListOfClass:(object_getClass(self.person1))];
NSArray *person2MethodList = [self instanceMethodListOfClass:(object_getClass(self.person2))];
NSLog(@"person1的实例方法列表:%@", person1MethodList);
NSLog(@"person2的实例方法列表:%@", person2MethodList);


IMP person1SetAgeImp = class_getMethodImplementation(object_getClass(self.person1), @selector(setAge:));
IMP person2SetAgeImp = class_getMethodImplementation(object_getClass(self.person2), @selector(setAge:));
NSLog(@"person1 setAge:方法实现的地址:%p", person1SetAgeImp);
NSLog(@"person2 setAge:方法实现的地址:%p", person2SetAgeImp);

控制台打印如下。

person1的实例方法列表:(
    "setAge:",
    class,
    dealloc,
    "_isKVOA"
)
person2的实例方法列表:(
    "setAge:",
    age
)


person1 setAge:方法实现的地址:0x10d8a5688
person2 setAge:方法实现的地址:0x10d54a430

可见新类确实重写了原来类和NSObject类的若干个方法,包括setAge:- classdealloc_isKVOA四个,当然其中我们最关心的还是setAge:方法被重写,这从person1person2两者setAge:方法实现的地址不同,可以更加确认。至于重写的setAge:方法的内部实现,我们暂时无法看到它的源码,反编译后倒是可以看到它对应的汇编代码,但我们不一定能看得懂,所以现在只是通过现象来猜测它内部实现的伪代码。


四、KVO常见面试题


1、像person->age = 1这样直接修改成员变量会触发KVO(的回调方法)吗?

答案:

不会。

因为触发KVO本质上是调用重写后的setter方法内部触发的,而直接修改成员变量person->age = 1是不会调用setter方法的,所以不会触发。

2、如何手动触发KVO?(如何手动触发KVO观察者的回调方法?)

答案:

手动调用willChangeValueForKey:didChangeValueForKey:方法。(注意KVO的API里要求这两个方法必须成对调用)

[self.person1 willChangeValueForKey:@"age"];
// 如果需要修改成员变量值的话
// self.person1->_age = 26;
[self.person1 didChangeValueForKey:@"age"];

你可能感兴趣的:(【OC语法】KVO的底层实现)