iOS之深入解析KVO的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析KVO的底层原理相关的知识,希望对你有一定的参考价值。
一、KVO 简介
① 概念
- KVO 全称 Key Value Observing,是苹果提供的一套事件通知机制,允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。
- 由于 KVO 的实现机制,所以对属性才会发生作用,一般继承自 NSObject 的对象都默认支持 KVO。
- KVO 和 NSNotificationCenter 都是 ios 中观察者模式的一种实现,区别在于,相对于被观察者和观察者之间的关系,KVO 是一对一的,而 NSNotificationCenter 是一对多的,KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
- KVO 可以监听单个属性的变化,也可以监听集合对象的变化。通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,当代理对象的内部对象发生改变时,会回调 KVO 监听的方法。
② 使用
- 使用 KVO 分为三个步骤:
- 通过 addObserver:forKeyPath:options:context: 方法注册观察者,观察者可以接收 keyPath 属性的变化事件;
- 在观察者中实现 observeValueForKeyPath:ofObject:change:context: 方法,当 keyPath 属性发生改变后,KVO 会回调这个方法来通知观察者;
- 当观察者不需要监听时,可以调用 removeObserver:forKeyPath: 方法将 KVO 移除。需要注意的是,调用 removeObserver 需要在观察者消失之前,否则会导致 Crash。
- KVO 注册方法:
-
- 在注册观察者时,可以传入 options 参数,参数是一个枚举类型,如果传入 NSKeyValueObservingOptionNew 和 NSKeyValueObservingOptionOld 表示接收新值和旧值,默认为只接收新值;如果想在注册观察者后,立即接收一次回调,则可以加入 NSKeyValueObservingOptionInitial 枚举。
-
- 还可以通过方法 context 传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是 KVO 中的一种传值方式;
-
- 在调用 addObserver 方法后,KVO 并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的 Crash。
- KVO 监听方法:
-
- 观察者需要实现 observeValueForKeyPath:ofObject:change:context: 方法,当 KVO 事件到来时会调用这个方法,如果没有实现会导致 Crash。change 字典中存放 KVO 属性相关的值,根据 options 时传入的枚举来返回,枚举会对应相应 key 来从字典中取出值,例如:有 NSKeyValueChangeOldKey 字段,存储改变之前的旧值。
-
- change 中还有 NSKeyValueChangeKindKey 字段,和 NSKeyValueChangeOldKey 是平级的关系,来提供本次更改的信息,对应 NSKeyValueChange 枚举类型的 value。例如:被观察属性发生改变时,字段为 NSKeyValueChangeSetting。
-
- 如果被观察对象是集合对象,在 NSKeyValueChangeKindKey 字段中会包含 NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement 的信息,表示集合对象的操作方式。
- KVO 调用方式:
-
- 调用 KVO 属性对象时,不仅可以通过点语法和 set 语法进行调用,KVO 兼容很多种调用方式:
// 直接调用set方法,或者通过属性的点语法间接调用
[person setName:@"dw"];
// 使用KVC的setValue:forKey:方法
[person setValue:@"dw" forKey:@"name"];
// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"dw" forKeyPath:@"person.name"];
// 通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
-
- KVO 主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用 KVO 实现最为合适。斯坦福大学的 iOS 教程中有一个很经典的案例,通过 KVO 在 Model 和 Controller 之间进行通信。如下图所示:
-
- KVO 的 addObserver 和 removeObserver 需要是成对的,如果重复 remove 则会导致 NSRangeException 类型的 Crash,如果忘记 remove 则会在观察者释放后再次接收到 KVO 回调时 Crash。
-
- 苹果官方推荐的方式是,在 init 的时候进行 addObserver,在 dealloc 时 removeObserver,这样可以保证 add 和 remove 是成对出现的,是一种比较理想的使用方式。
- 手动调用 KVO:
-
- KVO 在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现 KVO 属性的调用,则可以通过 KVO 提供的方法进行调用。
- (void)setAge:(double)theAge {
if (theAge != _age) {
[self willChangeValueForKey:@"age"];
_age = theAge;
[self didChangeValueForKey:@"age"];
}
}
-
- 可以看到调用 KVO 主要依靠两个方法,在属性发生改变之前调用 willChangeValueForKey:方法,在发生改变之后调用 didChangeValueForKey:方法。
-
- 如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的 KVO 调用,则可以重写下面方法,方法返回 YES 则表示可以调用,如果返回 NO 则表示不可以调用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"age"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
二、KVO 的实现原理分析
① 实现原理
- KVO 是通过 isa-swizzling 技术实现的。
- 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的 isa 指向中间类,并且将 class 方法重写,返回原类的Class。
- 所以苹果建议在开发中不应该依赖 isa 指针,而是通过 class 实例方法来获取对象类型。
② 实现方式
- 定义以下调试代码:
@interface YDWObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
#import "YDWObject.h"
#import <objc/runtime.h>
@implementation YDWObject
- (NSString *)description {
NSLog(@"object address : %p \\n", self);
IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
NSLog(@"object setName: IMP %p object setAge: IMP %p \\n", nameIMP, ageIMP);
Class objectMethodClass = [self class];
Class objectRuntimeClass = object_getClass(self);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \\n", objectMethodClass, objectRuntimeClass, superClass);
NSLog(@"object method list \\n");
unsigned int count;
Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"method Name = %@\\n", methodName);
}
return @"";
}
@end
- 调试调用:
- (void)viewDidLoad {
[super viewDidLoad];
self.object = [[YDWObject alloc] init];
self.object.name = @"ydw";
self.object.age = 10;
[self.object description];
[self.object addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object description];
self.object.name = @"dw";
self.object.age = 20;
[self.object removeObserver:self forKeyPath:@"name"];
[self.object removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到 %@ 的 %@ 改变 %@", object, keyPath, change);
}
- 打印结果如下:
object address : 0x600003a59860
object setName: IMP 0x101bea390 object setAge: IMP 0x101bea3f0
objectMethodClass : YDWObject, ObjectRuntimeClass : YDWObject, superClass : NSObject
object method list
method Name = description
method Name = name
method Name = .cxx_destruct
method Name = setName:
method Name = setAge:
method Name = age
object address : 0x600003a59860
object setName: IMP 0x7fff207bbb57 object setAge: IMP 0x7fff207bc799
objectMethodClass : YDWObject, ObjectRuntimeClass : NSKVONotifying_YDWObject, superClass : YDWObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
object address : 0x600003a59860
object setName: IMP 0x7fff207bbb57 object setAge: IMP 0x7fff207bc799
objectMethodClass : YDWObject, ObjectRuntimeClass : NSKVONotifying_YDWObject, superClass : YDWObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
监听到 的 name 改变 {
kind = 1;
new = dw;
old = ydw;
}
object address : 0x600003a59860
object setName: IMP 0x7fff207bbb57 object setAge: IMP 0x7fff207bc799
objectMethodClass : YDWObject, ObjectRuntimeClass : NSKVONotifying_YDWObject, superClass : YDWObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
监听到 的 age 改变 {
kind = 1;
new = 20;
old = 10;
}
- 可以发现:一旦 YDWObject 的 name 和age 属性的值发生改变,就会通知到监听者,因此继续分析:
- 对象被 KVO 后,其真正类型变为 NSKVONotifying_KVOObject 类,已经不是之前的类。
- KVO 会在运行时动态创建一个新类,将对象的 isa 指向新创建的类,新类是原类的子类,命名规则是 NSKVONotifying_xxx 的格式。
- KVO 为了使其更像之前的类,还会将对象的 class 实例方法重写,使其更像原类。
- 在上面结果中还发现有一个 _isKVOA 方法,这个方法可以当做使用了 KVO 的一个标记,如果想判断当前类是否是 KVO 动态生成的类,就可以从方法列表中搜索这个方法。
- 我们知道赋值操作都是调用 set 方法,可以重写 YDWObject 类中 age 的 set 方法,观察 KVO 是否是在 set 方法内部做了一些操作来通知监听者:
- (void)setAge:(NSInteger)age {
NSLog(@"override setAge");
_age = age;
}
- 我们发现即使重写了 set 方法,age 除了调用 set 方法之外还会执行监听者的 observeValueForKeyPath 方法。KVO 会重写 keyPath 对应属性的 setter 方法,没有被 KVO 的属性则不会重写其 setter 方法,在重写的 setter 方法中,修改值之前会调用 willChangeValueForKey: 方法,修改值之后会调用 didChangeValueForKey: 方法,这两个方法最终都会被调用到 observeValueForKeyPath:ofObject:change:context: 方法中。
- 根据上述调试推测:KVO 在运行时对对象进行了改动,使 object 对象在调用 setAge 方法时做了一些额外的操作,因此问题应该出在对象身上,两个对象可能本质上并不一样。
③ 实现分析
- 如下所示,打上调试断点:
- 调试如下:
(lldb) p object_getClassName(self.object)
(const char * _Nonnull) $0 = 0x00000001061e64ab "YDWObject"
(lldb) p object_getClassName(self.object)
(const char * _Nonnull) $1 = 0x0000600001bcd360 "NSKVONotifying_YDWObject"
- 我们发现,object 对象的 isa 指针由之前的指向类对象 YDWObject 变成了指向类对象 NSKVONotifying_YDWObject。因此可以推测,object 对象的 isa 发生改变后,其执行的 setAge 也发生了改变。
- 如果没有对 object 进行 addObserver,object 在调用 setAge 方法时,首先会通过 object 对象的 isa 指针找到 YDWObject 类对象,然后在类对象中找到 setAge 方法,最终找到方法对应的实现。
- 但 object 对象的 isa 在添加 KVO 之后已经指向了 NSKVONotifying_YDWObject 类对象,NSKVONotifying_YDWObject 则是 object 的子类。NSKVONotifying_YDWObject 是 runtime 在运行时生成的,因此,object 对象在调用 setAge 方法时必然会根据 object 的 isa 找到 NSKVONotifying_YDWObject,并在 NSKVONotifying_YDWObject 中找到 setAge 方法及其实现。
- 我们再通过 lldb 指令,去调试中间的变化过程,使用 watchpoint set variable object->_age 指令(watchpoint: 观察变量或者属性,非常有用,如果遇到某一个变量,不知道什么时候值被修改,就可以使用这个东西去定位),如下:(图LLDB)
- NSKVONotifying_YDWObjct 中的 setAge 方法中其实调用了 Foundation 框架中 C 语言函数 _NSSetLongLongValueAndNotify,_NSSetLongLongValueAndNotify 内部的操作大致是:首先调用 willChangeValueForKey 方法,然后调用父类的 setAge 方法对成员变量赋值,最后调用 didChangeValueForKey 方法,didChangeValueForKey 方法中会调用监听者的监听方法,最终调用监听者的 observeValueForKeyPath 方法。
④ 原理验证
- 我们已经通过断点打印类名的方式验证:object 对象在添加 KVO 后,其 isa 指针会指向一个通过 runtime 创建的 YDWObject 的子类 NSKVONotifying_YDWObjct。
- 下面可以通过打印方法实现的地址来看一下 object 的 setAge 方法实现的地址在添加 KVO 前后有什么变化。
NSLog(@"KVO之前:%p", [self.object methodForSelector:@selector(setAge:)]);
self.object addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"KVO之后:%p", [self.object methodForSelector:@selector(setAge:)]);
// 打印如下
KVO之前:0x107208300
KVO之后:0x7fff207bc799
- 可以发现:在添加 KVO 之前,object 的 setAge 方法实现的地址,在添加 KVO 之后发生了改变,通过打印方法实现可以证明,object 的 setAge 方法的实现由 YDWObject 类方法中的 setAge 方法转换成了 Foundation 框架中的 C 函数 _NSSetLongLongValueAndNotify。
⑤ 中间类内部结构
- NSKVONotifying_YDWObjct 作为 YDWObjct 的子类,其 superclass 指针指向 YDWObjct 类,其内部对 setAge 方法做了单独的实现,那么 NSKVONotifying_YDWObjct 同 YDWObjct 类的差别可能就在于其内存储的对象方法及实现不同。
- 我们可以通过 runtime 分别打印 YDWObjct 类对象和 NSKVONotifying_YDWObjct 类对象内存储的对象方法:
[self printMethods:object_getClass(self.object)];
- (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:@"\\n"];
[methodNames appendString:methodName];
}
NSLog(@"methodNames = %@", methodNames);
free(methods);
}
- 打印结果如下:
methodNames == YDWObject -
description
name
.cxx_destruct
setName:
setAge:
age
methodNames == NSKVONotifying_YDWObject -
setAge:
setName:
class
dealloc
_isKVOA
- 可以发现,NSKVONotifying_YDWObjct 中有 5 个对象方法,分别是:
setAge:
setName:
class
dealloc
_isKVOA
- NSKVONotifying_YDWObjct 重写 class 方法是为了隐藏 NSKVONotifying_YDWObjct 不被外界看到,我们在 self.object 添加 KVO 前后,分别打印 self.object 对象的 class,可以发现它们都返回 YDWObjct。
- 验证 didChangeValueForKey: 内部调用 observeValueForKeyPath:ofObject:change:context: 方法,在 YDWObjct 类中重写 willChangeValueForKey: 和 didChangeValueForKey: 方法,模拟它们的实现:
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
- 运行上述代码,可以确定是在 didChangeValueForKey: 方法内部调用了监听者的 observeValueForKeyPath:ofObject:change:context: 方法:
willChangeValueForKey: - begin
willChangeValueForKey: - end
didChangeValueForKey: - begin
监听到 <YDWObject: 0x6000010ed360> 的 name 改变 {
kind = 1;
new = dw;
old = ydw;
}
didChangeValueForKey: - end
willChangeValueForKey: - begin
willChangeValueForKey: - end
override setAge
didChangeValueForKey: - begin
监听到 <YDWObject: 0x6000010ed360> 的 age 改变 {
kind = 1;
new = 20;
old = 10;
}
didChangeValueForKey: - end
- 根据上述原理,可以通过调用 willChangeValueForKey: 和 didChangeValueForKey: 来手动触发 KVO。
三、KVO 性能分析
- 根据上面 KVC 的实现原理,可以看出 KVC 的性能并不如直接访问属性快,虽然这个性能消耗是微乎其微的。因此在使用 KVC 的时候,建议最好不要手动设置属性的 setter、getter,否则会导致搜索步骤变长。
- 尽量不要用 KVC 进行集合操作,例如 NSArray、NSSet 之类的,集合操作的性能消耗更大,而且还会创建不必要的对象。
四、KVO 使用示例
① 私有访问
- KVC 本质上是操作方法列表以及在内存中查找实例变量,我们可以利用这个特性访问类的私有变量。例如,在 .m 中定义的私有成员变量和属性,都可以通过 KVC 的方式访问:
TestObject.m
@interface TestObject () {
NSObject *_objectOne;
}
@property (nonatomic, strong) NSObject *objectTwo;
@end
- 这个操作对 readonly 的属性,@protected 的成员变量,都可以正常访问,如果不想让外界访问类的成员变量,则可以将 accessInstanceVariablesDirectly 属性赋值为 NO。
- KVC 在实践中也有很多用处,例如 UITabbar 或 UIPageControl 这样的控件,系统已经为我们封装好了,但是对于一些样式的改变并没有提供足够的 API,这种情况就需要用 KVC 进行操作。
② 安全性检查
- KVC 存在一个问题,因为传入的 key 或 keyPath 是一个字符串,这样很容易写错或者属性自身修改后字符串忘记修改,这样会导致 Crash。
- 可以利用 iOS 的反射机制来规避这个问题,通过 @selector() 获取到方法的 SEL,然后通过 NSStringFromSelector() 将 SEL 反射为字符串,这样在 @selector() 中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或未实现会报黄色警告:
[self valueForKey:NSStringFromSelector(@selector(object))];
以上是关于iOS之深入解析KVO的底层原理的主要内容,如果未能解决你的问题,请参考以下文章