KVO-KVC的原理探究 - KVO篇

2018-07-19  本文已影响74人  白夜追凶_key

关于KVO的探究

KVO的基本使用

创建Person类,添加属性age:


@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;

@end

在ViewController中添加属性@property (nonatomic, strong) Person * person1;

实例化并添加KVO观察age属性:


self.person1 = [[Person alloc] init];    

self.person1.age = 1;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

添加观察监听回调并打印:


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

 NSLog(@"被监听的 %@ 的值 %@ 改变为 %@", object, keyPath, change);

}

此时准备工作完成,当点击view时就会修改age的值,并且回调打印出监听的结果,这里在ViewController的touchedBegan中修改值:


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 self.person.age = 11;

}

记得在最后移除键值观察


- (void)dealloc {

 [self.person1 removeObserver:self forKeyPath:@"age"];

}

以上为KVO的基本使用。

关于KVO的疑问和分析

再次添加属性 @property (nonatomic, strong) Person * person2;

实例化person2,在touchedBegan方法中修改值但是不添加KVO:


self.person2 = [[Person alloc] init];

self.person2.age = 2;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 self.person.age = 11;

 self.person1.age = 22;

}

点击view可以看到打印台的日志为:


2018-07-17 14:09:26.944619+0800 KVO-KVC[36344:935709] 被监听的 <Person: 0x6040000106d0> 的值 age 改变为 {

 kind = 1;

 new = 11;

 old = 1;

}

这时就可以思考都是修改age属性值,为什么person1会有回调而person2没有,修改的本质都是调用age的set方法。猜想person1和person2的set方法实现可能不一样,但是实例方法都是存放在class中的,set方法应该是一样的才对,在touchesBegan处打断点,然后直接查看person1和person2的isa指针,看看person1和person2的class是否一样:


(lldb) p self.person1.isa

(Class) $0 = NSKVONotifying_Person

 Fix-it applied, fixed expression was: 

 self.person1->isa

(lldb) p self.person2.isa

(Class) $1 = Person

 Fix-it applied, fixed expression was: 

 self.person2->isa

可以看到person1的class为 NSKVONotifying_Person person2的class为 Person ,isa指针指向的就是instance的class,但是为什么person1和person2会不一样呢?我们在添加键值观察之前和之后分别打印person的类型:


NSLog(@"添加前 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

 [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加后 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));

打印的结果为


2018-07-17 14:40:59.918227+0800 KVO-KVC[37038:970983] 添加前 person1 : Person person2 : Person

2018-07-17 14:40:59.918636+0800 KVO-KVC[37038:970983] 添加后 person1 : NSKVONotifying_Person person2 : Person

可以看到添加键值观察之后person1的isa指针指向确实被修改了,指向了 NSKVONotifying_Person 类,结合上面的猜想,会不会是 NSKVONotifying_Person 这个类重新实现了person1的 setAge: ,否则怎么会和person2不一样呢?

我们来验证一下,通过 methodForSelector: 来获取 setAge: 的实现:


NSLog(@"添加前 person1 : %p person2 : %p",

 [self.person1 methodForSelector:@selector(setAge:)],

 [self.person2 methodForSelector:@selector(setAge:)]);

NSLog(@"添加后 person1 : %p person2 : %p",

 [self.person1 methodForSelector:@selector(setAge:)],

 [self.person2 methodForSelector:@selector(setAge:)]);

打印的结果为


2018-07-17 14:46:56.489956+0800 KVO-KVC[37183:978368] 添加前 person1 : 0x102493570 person2 : 0x102493570

2018-07-17 14:46:56.490699+0800 KVO-KVC[37183:978368] 添加后 person1 : 0x1027d9bf4 person2 : 0x102493570

我们知道instance的方法、属性、协议等信息都存在与class中,所以当person1和person2调用 setAge: 时得到的地址应该是一样的,但是在添加键值观察之后person1的调用方法地址改变了,为什么会改变呢?让我们来看看这两个地址的IMP,在添加键值观察之后断点,直接查看两个地址的IMP:


(lldb) p (IMP)0x100a43570

(IMP) $0 = 0x0000000100a43570 (KVO-KVC -[Person setAge:] at Person.m:13)

(lldb) p (IMP)0x100d89bf4

(IMP) $1 = 0x0000000100d89bf4 (Foundation _NSSetLongLongValueAndNotify)

可以看到添加键值观察之后调用 setAge: 方法其实就是调用了 Foundation _NSSetLongLongValueAndNotify

由此可以猜测在添加键值观察之后person1的isa指向了新生成的类 NSKVONotifying_PersonNSKVONotifying_Person 可能继承自 Person 类,并且重写了 setAge: 方法,伪代码如下:


- (void)setAge:(NSInteger)age {

 _NSSetLongLongValueAndNotify();

}

void _NSSetLongLongValueAndNotify() {

 [self willChangeValueForKey:@"age"];

 [super setAge:age];

 [self didChangeValueForKey:@"age"];

}

- (void)didChangeValueForKey:(NSString *)key {

 [observer observeValueForKeyPath:key ofObject:self change:opetions context:nil];

}

综上我们的猜想KVO的实现:instance添加键值观察之后isa指针会被修改为指向 NSKVONotifying_PersonNSKVONotifying_Person 继承自 Person 并且重写了 setAge: 方法,方法实现如上。
在这里就有了那道最经典的面试题:如何手动实现KVO,我们只需要在修改值的时候替换 _NSSetLongLongValueAndNotify 方法里面的 [super setAge:age]; 就好了。

KVO内部实现窥探

由上我们猜测出了KVO的实现原理,下面我们来继续探索一下KVO内部的实现。

我们分别在添加KVO前后打印person1和person2的class,这次我们用两种方式:


NSLog(@"添加前 person1 : %@ -- %@  person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));

NSLog(@"添加后 person1 : %@ -- %@  person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));

打印出的结果为:


2018-07-19 11:05:50.553735+0800 KVO-KVC[40616:2560144] 添加前 person1 : Person -- Person  person2 : Person -- Person

2018-07-19 11:05:52.772905+0800 KVO-KVC[40616:2560144] 添加后 person1 : Person -- NSKVONotifying_Person  person2 : Person -- Person

可以看到我们通常用来获取class的方法在添加前后结果都是 Person ,通过runtime API获取到的class不相同,怎么回事呢?我们先来看一下苹果官方runtime的源码 这里,当然官方的编译是失败,要想调试runtime的请看 这里

我们来分析一下源码:


class方法:

+ (Class)class {

 return self;

}

- (Class)class {

 return object_getClass(self);

}

runtime object_getClass方法:

Class object_getClass(id obj) {

 if (obj) return obj->getIsa();

 else return Nil;

}

class 的类方法或者实例方法最终返回的都是class的self,而 object_getClass 方法返回的是obj的isa指针,所以通过 object_getClass 获取的才是当前obj的真正class,所以在添加KVO之后person1的isa指针确确实实是被修改了。

我们再来看一下捕捉到的 NSKVONotifying_Person 到底是个什么鬼?

先来看一下 NSKVONotifying_Person 的meta-class:


NSLog(@"元类对象 person : %@ person1 : %@",

 object_getClass(object_getClass(self.person1)),

 object_getClass(object_getClass(self.person2)));

打印结果:

2018-07-19 11:39:30.210378+0800 KVO-KVC[41164:2599225] 元类对象 person : NSKVONotifying_Person person1 : Person

NSKVONotifying_Person 的meta-class为 NSKVONotifying_Person

在添加KVO之后打住断点,借用 DLIntrospection 再来查看一下此时class里面方法都有什么:


(lldb) po [[self.person1 class] instanceMethods]

<__NSArrayI 0x60400023daa0>(

- (void)setAge:(q)arg0 ,

- (q)age

)

(lldb) po [object_getClass(self.person1) instanceMethods]

<__NSArrayI 0x60400025fb30>(

- (void)setAge:(q)arg0 ,

- (class)class,

- (void)dealloc,

- (BOOL)_isKVOA

)

结果可以看到 NSKVONotifying_Person 重写了 setAge: 方法,并且还有其他的三个方法,可证上面的猜想确实没错,NSKVONotifying_Person重写了 setAge: 方法,但是还有一个上面的猜想没有验证,那就是 NSKVONotifying_Person 的superClass到底是谁?

类似isa指针的方式,我们断点直接打印:


(lldb) po self.person1.superclass

NSObject

(lldb) po self.person2.superclass

NSObject

咦~~~ 等等,这跟我们猜测的不一样啊,怎么superclass都是NSObject呢?那我们的猜测是不是都错了?

为了看看superClass里面到底是什么下面我们请出 clang 大神:

clang -rewrite-objc Person.m

可以看出编译完成后Person类被编译成了这样:


struct NSObject_IMPL {

 Class isa;

};

struct Person_IMPL {

 struct NSObject_IMPL NSObject_IVARS;

 NSInteger _age;

};

结合runtime源码分析,Class为 typedef struct objc_class *Class; 类型的结构体,再看下结构体里面的结构:


struct objc_object {

private:

 isa_t isa;

 ···

}

struct objc_class : objc_object {

 // Class ISA;

 Class superclass;

 cache_t cache;

 class_data_bits_t bits;

 ···

}

里面确实有superclass,仿照runtime的结构我们自己来创建一个类似的结构体:


struct XFPerson_IMPL {

 Class isa;

 Class super_Class;

 NSInteger _age;

};

用我们自己创建的结构体来接收 NSKVONotifying_Person ,看看他的superclass到底是什么类型:


struct XFPerson_IMPL * xfPerson1 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person1));

struct XFPerson_IMPL * xfPerson2 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person2));

NSLog(@"person1--- %@", xfPerson1->super_Class);

NSLog(@"person2--- %@", xfPerson2->super_Class);

打印结果:

2018-07-19 14:05:38.855549+0800 KVO-KVC[43578:2717734] person1--- Person

2018-07-19 14:05:38.855658+0800 KVO-KVC[43578:2717734] person2--- NSObject

结果可见是符合我们的猜想的,NSKVONotifying_Person 确实是Person的子类,但是为什么上面直接打印instance的superclass却都是NSObject呢?

回过头来看一下上面我们找到的 NSKVONotifying_Person 除了 setAge: 还有三个方法,其中就有class方法,我们已经知道runtime的class的实现,class返回的就是self,而通过 [self.person1 class] 得到的是 Person ,这就证明了 NSKVONotifying_Person 重写了class方法,并且返回的是 Person 类,通过源码查看runtime的superclass方法的实现:


+ (Class)superclass {

 return self->superclass;

}

- (Class)superclass {

 return [self class]->superclass;

}

就是先通过class方法找到class,然后在根据class找到superclass,所以前面直接通过 self.person1.superclass 找到的是 Person,因为此时的class方法返回已经被修改了。

苹果大大可能是因为整个事件中 NSKVONotifying_Person 是个人畜无害的东西,对于开发者使用KVO是可以不用知道的,所以用这种方式来骗骗开发者,真不容易,还好最近看 白夜追凶 看的整个人都比较有耐心了就是要找到真相,哈(不)哈(要)哈(脸)😁。

再看看看其他的两个方法,dealloc 方法可能就是做一些销毁现场的事情,毕竟中间动态创建了 NSKVONotifying_Person ,不用了一定要销毁,而 _isKVOA 返回的一定是 YES ,表示当前确实是在用KVO,到此关于KVO的黑科技已经探究明白了,好了,打完收工,接着去看两集 白夜追凶, 哈哈哈。

上一篇下一篇

猜你喜欢

热点阅读