KVO原理
- KVO是基于
runtime
机制实现的
当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter
方法。派生类在被重写的setter
方法内实现真正的通知机制 - 如果原类为
Person
,那么生成的派生类名为NSKVONotifying_Person
- 每个类对象中都有一个
isa
指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa
指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter
方法 - 键值观察通知依赖于NSObject 的两个方法:
willChangeValueForKey:
和didChangevlueForKey:
;在一个被观察属性发生改变之前,willChangeValueForKey:
一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:
会被调用,继而 `observeValueForKey:ofObject:change:context: ``也会被调用。
弄清为什么改变isa指针的指向,能修改调用的方法。
- 查看实例对象内部结构
打开xcode
,按command+shift+O
搜索objc.h
,我们可以看到
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
我们可以看出
- Class是个
objc_class
类型的结构体 - 所有实例对象的本质是
objc_object
类型的结构体,里面存放这个实例对象的isa指针。 - id是一个
objc_object
类型的指针,这应该就是id可以指向任意对象的原因 - SEL是个
objc_selector
结构体类型的指针,存放的是方法名 - IMP函数指针,存放方法的具体实现地址。
- 再来看看
objc_class
struct objc_class {
Class _Nonnull isa // 所属类的指针
#if !__OBJC2__
Class _Nullable super_class // 父类
const char * _Nonnull name // 类名
long version // 版本
long info // 运行期使用的位标识
long instance_size // 实例大小
struct objc_ivar_list * _Nullable ivars // 实例变量列表
struct objc_method_list * _Nullable * _Nullable methodLists // 方法列表
struct objc_cache * _Nonnull cache // 方法缓存
struct objc_protocol_list * _Nullable protocols // 协议列表
#endif
} OBJC2_UNAVAILABLE;
我们可以看出类中也有isa
指针,这个isa
指针是指向meta
元类中。实例对象,类和元类的关系其实有一张图很明显。
isa
的指向是从实例对象->类对象->元类对象->根元类对象->自己。
- 当我们向一个实例对象发送消息,即调用了实例方法时,在运行时会顺着
isa
指针指向的类对象中查找相对应的方法,先从缓存中查找,如果缓存未命中,则从方法列表中查找。如果未找到,则一直顺着父类找到NSObject
,如果还未找到,则走消息转发。 - 当我们向一个类对象发送消息时,即使用类方法调用,在运行时会顺着
isa
,找到类的元类,在元类中查找类方法。先从缓存中查找,如果缓存未命中,则从方法列表中查找。如果未找到,则一直顺着父元类一直找直到找到NSObject
,如果还未找到,则走消息转发。
那我们可以知道,由isa指针指向的对象才是真正调用方法的对象。在类中存储着实例对象的实例方法,元类中存储着类对象的类方法,KVO中的isa- swizzle
就是交换isa的指向,本来是在A类中查找调用setter
方法,运行时改成了在B类中查找调用setter
方法。
使用KVO的方法。
- 添加观察者
_dog = [[Animal alloc] init];
[_dog setValue:@"牧尘" forKey:@"name"];
// 添加KVO
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// 触发KVO
_dog.name = @"小明";
- KVO的监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
}
}
- 通知的移除
- (void)dealloc {
[_dog removeObserver:self forKeyPath:@"name"];
}
- 打印结果
2018-06-29 13:51:37.175922+0800 KVO与NSKeyValueObserving[38410:509677] 新值为小明
2018-06-29 13:51:37.176043+0800 KVO与NSKeyValueObserving[38410:509677] 旧值为牧尘
如果是简单使用,那知道这个就可以了。
使用探究
- 我们再看下
KVO
的方法,可以看出这是通过KVO
是对键值进行的观察。
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- 那么我们知道KVC访问的方式是通过下面两中方式。
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
能用第一种setValue:forKey:
方式访问的肯定也能通过setValue: forKeyPath:
方式访问,但是通过keyPath
方式访问变量不一定能用第一种方式访问。
疑问一 :能通过KVO监听对象中另一个对象的属性么?
新增Food
类,实例化为Animal
类的对象。
#import <Foundation/Foundation.h>
@class Food;
/**
动物类
*/
@interface Animal : NSObject
// 姓名
@property (nonatomic, strong) NSString *name;
// 食物
@property (nonatomic, strong) Food *food;
@end
#pragma mark --- 食物
// 食物类
@interface Food : NSObject
// 水果
@property (nonatomic, strong) NSString *fruit;
@end
- 对food.fruit进行监听
[_dog setValue:@"苹果" forKeyPath:@"food.fruit"];
[_dog addObserver:self forKeyPath:@"food.fruit" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog setValue:@"荔枝" forKeyPath:@"food.fruit"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"food.fruit"]) {
NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
}
}
- 结果
2018-06-29 14:13:45.002673+0800 KVO与NSKeyValueObserving[38503:523330] 新值为荔枝
2018-06-29 14:13:45.002896+0800 KVO与NSKeyValueObserving[38503:523330] 旧值为苹果
可以看出可以通过keyPath的方式对对象的对象进行监听。
疑问二 : 通过keyPath
方式监听,只局限于属性监听么,实例对象不能监听么?
属性可以通过点语法的方式访问变量,会调用
setter
方法。KVC
方式会查看如果有``setter方法,调用setter
方式赋值,如果没有setter
方法,检查accessInstanceVariablesDirectly
。如果accessInstanceVariablesDirectly
为YES,顺着_name
,_isName
,name
,isName
方式查找实例变量,有则赋值。没有则抛异常。测试通过实例变量,是不是
setter
方法能进行KVO的监听
去除name的属性,新增isName实例变量。
@interface Animal() {
NSString *isName;
}
@end
@implementation Animal
@end
[_dog setValue:@"牧尘" forKey:@"name"];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog setValue:@"小飞" forKey:@"name"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
}
}
- 结果
2018-06-29 14:36:51.210152+0800 KVO与NSKeyValueObserving[38565:537594] 新值为小飞
2018-06-29 14:36:51.210349+0800 KVO与NSKeyValueObserving[38565:537594] 旧值为牧尘
可以看出,KVO监听不仅能通过对属性进行监听,还能对类中的实例变量进行监听。
- 为什么对实例变量也能监听在文章下说明。
手动触发KVO
- 在
NSKeyValueObserving
中有一个方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
这个方法的含义是否自动对key进行观察,就是自动触发KVO
。默认为YES。我们可以重写方法设置为NO,让我们手动触发。
- 调用
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
方法来实现对key的手动监听。willChangeValueForKey:
顾名思义,将要改变的时候调用的方法,那么didChangeValueForKey:
就是已经改变的时候调用的方法,当NSKeyValueObservingOption
为NSKeyValueObservingOptionOld| NSKeyValueObservingOptionNew
时
- (void)viewDidLoad {
[super viewDidLoad];
_dog = [[Animal alloc] init];
[_dog setValue:@"牧尘" forKey:@"name"];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"即将改变name");
[_dog willChangeValueForKey:@"name"];
// 改变值
[_dog setValue:@"小飞" forKey:@"name"];
// _dog.name = @"小明";
NSLog(@"已经改变name");
[_dog didChangeValueForKey:@"name"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"调用了observeValueForKeyPath方法");
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
}
}
打印
2018-06-29 16:58:38.552276+0800 KVO与NSKeyValueObserving[43395:635717] 即将改变name
2018-06-29 16:58:38.552577+0800 KVO与NSKeyValueObserving[43395:635717] 已经改变name
2018-06-29 16:58:38.552752+0800 KVO与NSKeyValueObserving[43395:635717] 调用了observeValueForKeyPath方法
2018-06-29 16:58:38.552873+0800 KVO与NSKeyValueObserving[43395:635717] 新值为小飞
2018-06-29 16:58:38.552998+0800 KVO与NSKeyValueObserving[43395:635717] 旧值为牧尘
- 可以看出
option
值为new
和old
只会在didChangeValueForKey :
已经改变值后调用一次。
当option 为NSKeyValueObservingOptionPrior时
- 结果
2018-06-29 17:04:28.115616+0800 KVO与NSKeyValueObserving[43412:639884] 即将改变name
2018-06-29 17:04:28.115937+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
2018-06-29 17:04:28.116069+0800 KVO与NSKeyValueObserving[43412:639884] 新值为(null)
2018-06-29 17:04:28.116329+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
2018-06-29 17:04:28.116471+0800 KVO与NSKeyValueObserving[43412:639884] 已经改变name
2018-06-29 17:04:28.116589+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
2018-06-29 17:04:28.116711+0800 KVO与NSKeyValueObserving[43412:639884] 新值为小飞
2018-06-29 17:04:28.116816+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
- 我们可以得出结果
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
方法会在willChangeValueForKey :
调用时触发一次,在didChangeValueForKey :
调用时触发一次,其中调用willChangeValueForKey
后是否触发方法和设置的NSKeyValueObservingOptions
有关。
runtime到底是什么时候修改了对象的isa指针。
- 重写model类的description方法
- (NSString *)description {
// 查看当前isa指针指向的类
Class runtimeClass = object_getClass(self);
// 当前类的父类
Class superClass = class_getSuperclass(runtimeClass);
NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
return @"";
}
- 在添加观察者前后打印代码
[_dog description];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog description];
- 结果
2018-06-29 18:09:05.005066+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = Animal, superClass = NSObject
2018-06-29 18:09:05.005591+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = NSKVONotifying_Animal, superClass = Animal
我们可以看出,KVO在- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
方法调用后,在运行时动态生成了一个原来类的子类NSKVONotifying_XXX
,导致我们通过点语法或者 KVC
为属性赋值的时候,调用的是这个isa指向的类NSKVONotifying_XXX里的方法列表中的setter方法。
回到上面一个问题,实例变量为什么也能监听?
对实例变量通过KVC访问赋值,由于isa指针的混淆,KVC实际是对NSKVONotifying_XXX
类调用。但是由于实例变量无法继承到NSKVONotifying_XXX
中,但是KVO仍然会对NSKVONotifying_XXX
调用- (void)setValue:(id)value forKey:(NSString *)key
方法,NSKVONotifying_XXX
调用失败,会调用super类的- (void)setValue:(id)value forKey:(NSString *)key
,即赋值成功。我们可以测试一下
- 创建一个
Animal
类的子类Cat
。Animal
类中有个实例变量name。不存在name属性。 - 重写
Animal
和Cat
的- (void)setValue:(id)value forKey:(NSString *)key
方法。
- (void)setValue:(id)value forKey:(NSString *)key {
[super setValue:value forKey:key];
NSLog(@"animal类中的value = %@", value);
}
- 我们先将
Cat
类中的super
链打断
- (void)setValue:(id)value forKey:(NSString *)key {
NSLog(@"cat类中的value = %@",value);
// [super setValue:value forKey:key];
}
- 测试打印
Cat *cat = [[Cat alloc] init];
[cat setValue:@"小妖精" forKey:@"name"];
NSLog(@"catName = %@",[cat valueForKey:@"name"]);
- 结果
cat
赋值失败,因为cat
中没有name
实例变量
NSKeyValueObserving[44918:845002] cat类中的value = 小妖精
2018-07-02 10:10:49.221572+0800 KVO与NSKeyValueObserving[44918:845002] catName = (null)
- 我们将
cat
中的super
链连接.
- (void)setValue:(id)value forKey:(NSString *)key {
NSLog(@"cat类中的value = %@",value);
[super setValue:value forKey:key];
}
打印结果,赋值成功,调用了父类中的- (void)setValue:(id)value forKey:(NSString *)key
进行赋值,父类中有name实例变量。
NSKeyValueObserving[44937:846921] cat类中的value = 小妖精
2018-07-02 10:13:04.639062+0800 KVO与NSKeyValueObserving[44937:846921] animal类中的value = 小妖精
2018-07-02 10:13:04.639263+0800 KVO与NSKeyValueObserving[44937:846921] catName = 小妖精
KVO
分别在KVC赋值的前后调用willChangeValueForKey
和didChangeValueForKey
实现监听调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
告诉我们值已经改变了。
这样KVC调用的赋值,在我们实际开发中也有用到,类似TextField
有@"_placeholderLabel.font"
这样的键值,我们在子类化TextField
,创建subClassTextField
这样一个子类后,仍然可以用KVC的方法通过键值对TextField
的placeholderLabel
实例变量进行赋值。
查看KVC对实例变量调用KVO监听过程中,didChangeValueForKey
的调用时机。
- 设置系统自动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return YES;
}
重写- (void)setValue:(id)value forKey:(NSString *)key
方法
- (void)setValue:(id)value forKey:(NSString *)key {
// 查看当前isa指针指向的类
Class runtimeClass = object_getClass(self);
// 当前类的父类
Class superClass = class_getSuperclass(runtimeClass);
NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
NSLog(@"赋值前,name = %@",isName);
[super setValue:value forKey:key];
NSLog(@"赋值后,name = %@",isName);
}
- 输出结果
2018-07-02 11:31:28.751652+0800 KVO与NSKeyValueObserving[45238:898392] runtimeClass = NSKVONotifying_Animal, superClass = Animal
2018-07-02 11:31:28.752193+0800 KVO与NSKeyValueObserving[45238:898392] 赋值前,name = 牧尘
2018-07-02 11:31:28.752513+0800 KVO与NSKeyValueObserving[45238:898392] 调用了observeValueForKeyPath方法
2018-07-02 11:31:28.752667+0800 KVO与NSKeyValueObserving[45238:898392] 新值为小飞
2018-07-02 11:31:28.752795+0800 KVO与NSKeyValueObserving[45238:898392] 旧值为牧尘
2018-07-02 11:31:28.752940+0800 KVO与NSKeyValueObserving[45238:898392] 赋值后,name = 小飞
我们可以看出,KVC调用实例属性触发didChangeValueForKey
是在- (void)setValue:(id)value forKey:(NSString *)key
方法中。
我们可以看出KVO监听的过程实际上是对值的监听,在值改变的前后调用willChangeValueForKey
和didChangeValueForKey
,无论是点语法还是KVC都是直接或间接对值的改变。
手动实现一个KVO
写法思路都一样,推荐一个文章
// 总结如下
1 通过NSSelectorFromString 获取setter的sel。 通过classGetInstanceMethod 获取 method。 判断method是否存在, 不存在return。
2 判断当前类是否是kvo类。 通过object_getClass 获取isa指向的类, 通过NSStringFromClass 回去类名。 判断类名是否有你的前缀。
3 如果不是kvo的类,则要去创建kvo的类。 通过NSClassFromString 获取类, 判断kvo名称的类是否已经被注册。 如果注册,则返回注册的类。 如果没注册,通过objc_allocateClassPair创建类, 通过class_addMethod 添加 class方法的实现,修改kvo class方法的实现, 学习Apple的做法, 隐瞒这个kvo_class。 然后objc_registerClassPair 注册类
4 通过class_addMethod 为kvo class添加setter方法的实现, 实现监听。 通过 valueForKey 获取 原始的指, 然后调用原类的setter方法赋值。 从关联对象中取出观察数组, callback告诉外部修改.
5 把对应的kvo信息存进 关联对象数组里面。
哪些情况下使用kvo会崩溃,怎么防护崩溃
使用不当 会crash,比如:
- 添加和移出不是成对出现且存在多线程添加KVO的情况,经常遇到的crash是移出 - 内存dealloc的时候 或者对象销毁前没有正确移出Observer
如何防护?
- 1.注意移出对象 匹配
- 2.内存野指针问题,一定要在对象销毁前移出观察者
- 3.可以使用第三方库BlockKit添加KVO,blockkit内部会自动移除Observer避免crash.
kvo的优缺点
优点:
- 方便两个对象间同步状态(keypath)更加方便,一般都是在A类要观察B类的属性的变化.
- 非侵入式的得到某内部对象的状态改变并作出响应.(就是在不改变原来对象类的代码情况下即可做出对该对象的状态变化进行监听)
- 可以嵌入更改前后的两个时机的状态. - 可以通过Keypaths对嵌套对象的监听.
缺点:
- 需要手动移除观察者,不移除容易造成crash.
- 注册和移出成对匹配出现.
- keypath参数的类型String, 如果对象的成员变量被重构而变化字符串不会被编译器识别而报错.
- 实现观察的方式是复写NSObjec的相关KVO的方法,应该更加面向protocol的方式会更好.