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的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

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

iOS之深入解析YYModel的底层原理

iOS底层探索之KVO—FBKVOController分析

iOS底层探索之KVO—自定义KVO

iOS之深入解析渲染的底层原理

iOS之深入解析通知NSNotification的底层原理