阅读 369

iOS底层探索之KVO(二)—KVO原理分析

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

回顾

上一篇博客中,已经介绍了KVO的相关操作,那么接下来就去探索一下KVO的底层逻辑,KVO到底是如何实现的呢?

  • 在官方文档中有如下图中的说明

isa-swizzling

键值观察是使用称为isa-swizzling的技术实现的。

  • isa指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据。

  • 当观察者为对象的属性注册时,被观察对象的 isa 指针被修改,指向中间类不是真正的类。因此,isa 指针的值不一定反映实例的实际类。

  • 你不应该依赖isa指针来确定类的成员。相反,应该使用该class方法来确定实例对象的类。

1. isa-swizzling验证

在添加观察者处,打上断点,再控制台lldb调试看看。

- (void)viewDidLoad {
	[super viewDidLoad];
	self.student = [[JPStudent alloc]init];
	[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
复制代码
  • 控制台打印如下

lldb调试

addObserverstudentJPStudent变成了NSKVONotifying_JPStudent

我们知道,实例对象的关系实际上就是实例对象的isa指向了类对象。所以这里我们可以推断,self.student在调用addObserver方法后,已经从JPStudent类的实例对象,变成了NSKVONotifying_JPStudent 的实例对象。

2. NSKVONotifying_JPStudent子类验证

  • 那么这个NSKVONotifying_JPStudent是什么东西呢?是一开始就直接存在,还是和JPStudent类之间有什么关系呢?那么看看NSKVONotifying_JPStudent是不是一开始就存在的,再次运行代码,断点还是断在添加观察者者处,打印一下

NSKVONotifying_JPStudent生产测试

提醒objc_getClassruntimeapi,一定要导入头文件才可以正常使用,如图所示。

在调用addObserver方法前后分别打印,结果说明NSKVONotifying_JPStudent是系统动态生成添加的一个类。这两个类名字这么相似,有没有可能是JPStudent的子类呢?我们打印一下看看 验证NSKVONotifying_JPStudent是JPStudent的子类

从打印来看,发现了新大陆,NSKVONotifying_JPStudent 确实是继承自JPStudent的。那么这个中间类,有没有可能存在自己的子类呢?我们通过下面这段代码来看看

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
	 // 注册类的总数
	 int count = objc_getClassList(NULL, 0);
	 // 创建一个数组, 其中包含给定对象
	 NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
	 // 获取所有已注册的类
	 Class* classes = (Class*)malloc(sizeof(Class)*count);
	 objc_getClassList(classes, count);
	 for (int i = 0; i<count; i++) {
		  if (cls == class_getSuperclass(classes[i])) {
				[mArray addObject:classes[i]];
		  }
	 }
	 free(classes);
	 NSLog(@"classes = %@", mArray);
}
复制代码
  • 打印结果如下

遍历类以及子类 从打印来看,可以验证NSKVONotifying_JPStudentJPStudent的子类。 那么NSKVONotifying_JPStudent这个类里面都有些什么内容呢?类里面一般也就是存储了成员变量方法协议等信息,那么通过下面这段代码来看看它里面都有什么。

#pragma mark **- 遍历方法-ivar-property**
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
复制代码
  • 打印如下

遍历方法-ivar-property

从打印结果来看,系统重写了setNickNameclassdealloc这几个方法,并且添加了一个叫_isKVOA的方法,来区分是不是系统通过KVO自动生成的。

3. 观察者被移除isa的指向?

官方文档中说:调用addObserver方法会修改isa指向,那么现在我们移除观察者后系统会怎么做呢?我们在dealloc方法中移除观察者这里打上断点,然后继续观察self.studentisa指向。 观察者被移除isa的指向?

移除观察者之后,self.studentisa又指回了JPStudent类。并且生成的子类NSKVONotifying_JPStudent 还在,没有进行销毁。

原因是如果下次继续进行观察者添加,系统就不会再生成新的中间类,而是直接使用这个类了,防止资源的浪费。

4. class方法

在添加观察者之后,我们都知道会生成动态子类NSKVONotifying_JPStudent

那么调用class方法p self.student.class打印的是 NSKVONotifying_JPStudent 吗???

  • 断点在添加观察者之后,我们控制台验证一下

self.student.class 从打印结果看,输出的还是JPStudent,虽然self.studentisa已经指向NSKVONotifying_JPStudent了,但是由于NSKVONotifying_JPStudent重写了class方法,最后打印输出的还是JPStudent ,苹果这么做的目的是为了隐藏系统在背后做的一系列操作,让开发者更少的关注底层逻辑,只关注上层的代码实现就可以。

5. setter方法

既然重写了setter方法观察属性,那么如果有成员变量,是否也可以能观察呢?增加age成员变量,测试一下

@interface JPStudent : NSObject
{
	@public
	int age;
}
@property (nonatomic, copy) NSString *name;

@end 
复制代码

setter方法测试

当对age进行赋值的时候,并没有触发监听的回调方法。那么就说明了只是对属性的setter方法进行的监听。

我们再看看在dealloc中观察者移除isa指回的时候,查看name的值 观察者移除后Name的值

那就说明在KVO生成的类中对name的修改影响到了原始类。

name下个内存断点调试看看 断点调试

  • bt 打印堆栈信息

bt 打印堆栈信息

发现调用了Foundation的一些方法,最后才是[JPStudent setName:]name赋值。

提示Foundation框架是不开源的。

  • Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
  • Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
  • Foundation`_NSSetObjectValueAndNotify

_NSSetObjectValueAndNotify汇编调用主要如下:

_NSSetObjectValueAndNotify

"willChangeValueForKey:"
这里是调用setter方法赋值
"didChangeValueForKey:"
"_changeValueForKey:key:key:usingBlock:"
复制代码

从堆栈信息和汇编可以知道,在_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:中进行赋值后的回调,那么肯定得通知监听者

  • observeValueForKeyPath的回调中打个断点:

在这里插入图片描述

确认是在NSKeyValueNotifyObserver通知中进行的回调。

6.总结

  • KVO添加观察者addObserver动态生成子类NSKVONotifying_XXX
  • 重写class方法,返回父类class信息。父类isa指向子类。

给动态子类添加setter方法(所有要观察的属性)。 消息转发给父类。

  • setter会调用父类原来的方法进行赋值,完成后进行回调通知。
  • 移除observer的时候isa指回父类,动态生成的子类并不会销毁.

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

文章分类
iOS
文章标签