19 KiB
KVC && KVO
KVO 的实现原理是什么?如何手动触发 KVO?本文来探索下 iOS 中 KVO 底层细节
底层实现分析
Demo1:创建 Person 类,点击事件里触发属性值的改变。
分析:
- 添加过 KVO 的 person1,isa 为系统利用 Runtime 技术动态创建的类,名字为
NSKVONotifying_Person - 没有添加过 KVO 的 person2,isa 为 Person 的类对象
.height = 177本质是[self.person1 setHeight:177]也就是调用 set 方法。
在内存中的结构如下图
整个流程分析下:
- self.person2 调用 setHeight 的时候,首先根据 self.person2 实例对象的 isa 找到 Person 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用
- self.person1 调用 setHeight 的时候,首先根据 self.person1 实例对象的 isa 找到
NSKVONotifying_Person类对象,然后在方法列表中找到 setHeight 方法,然后进行调用。内部实现中,会调用 Foundation 的_NSSetIntValueAndNotify方法。 - 然后调用: willSet、super setHeight、didSet 方法。
当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
Demo2:
分析:
- 可以看到在对 self.person1 添加 KVO 之后,self.person1 的类对象改变了,也就是 self.person1的 isa 改变了,变为系统动态生成的新类
- 在对 self.person1 添加 KVO 之后,self.person1 的 setHeight 方法的实现变了,添加前后 self.person2 的 setHeight 方法,都是 Person 类对象的 setHeight 方法实现。KVO 添加前后都未改变
- 利用
(IMP)方法地址查看,没有进行 KVO 的setHeight是在KVOExplore -[Person setHeight:] at Person.h:14)里。添加过 KVO 的是在Foundation _NSSetLongLongValueAndNotify里。且_NSSetLongLongValueAndNotify是个 c 语言函数。
Demo3:
可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 _NSSetLongLongValueAndNotify,现在是 _NSSetDoubleValueAndNotify
也可以借助于 nm 来查看所有 Foundation 关于 KVO 的方法 nm Foundation | grep ValueAndNotify (需要自己提取真机上的 Foundation 符号表)
NSSet**ValueAndNotify 的内部实现
来对 Person 类增加一些打印方法
可以看出内部实现是:
-
调用
willChangeVlueForKey -
调用原本的 setter
-
调用
didChangeValueForKey。在didChangeValueForKey内部会调用 KVO 的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法
@interface Person()
@property (nonatomic, assign) double height;
@end
@implementation Person
- (void)setHeight:(double)height {
_height = height;
}
@end
@interface NSKVONotifying_Person: Person()
@end
@implementation NSKVONotifying_Person
- (void)setHeight:(double)height {
[self setDoubleValueAndNotify:height];
}
- (void)setDoubleValueAndNotify:(double)height {
[self willChangeValueForKey:@"height"];
[super setHeight: height];
[self didChangeValueForKey:@"height"];
}
- (void)didChangeValueForKey:(NSString *)key {
[observer observeValueForKeyPath:key ofObject:self change:@{} context:nil];
}
- (Class)class {
return [Person class];
}
@end
重写 class 方法
可以看到利用 runtime api,在添加 KVO 之后,类对象为 NSKVONotifying_Person
但是利用 class 方法,添加 KVO 之后,获取类对象依旧为 Person。
好处是:屏蔽了 KVO 底层内部实现,隐藏了 NSKVONotifying_Person 的存在,通过 -(Class)class 方法告诉开发者添加 KVO 之后的类,依旧是 Person,本质上是继承自 Person 的类对象,能力没有改变。
KVO 类的所有方法
利用 runtime api,打印添加 KVO 后,动态创建的 NSKVONotifying_Person 都存在什么方法?
- setHeight:
- class
- dealloc
- _isKVOA
QA:为什么新创建的类没有 getter?
因为新创建的类是子类,父类中存在 getter。子类中增加的方法只是为了触发 KVO,getter 不影响。
修改成员变量的值可以触发 KVO 吗
我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。
也就是说,触发 KVO 的本质是必须要有 setter,且触发 setter。直接修改成员变量,是不会触发 setter 的。
QA:如何手动触发?
手动调用 willChangeValueForKey、 didChangeValueForKey
QA:请描述系统如何实现一个对象的 KVO?KVO 的本质是什么
- 系统利用 runtime 的能力,动态创建一个监听对象的类的子类,子类命名格式为:
NSKVONotifying_类名。 并且让 instance 对象的 isa 指向这个全新的子类 - 当修改 instance 对象的属性时,会调用 Foundation 框架的
_NSSet***ValueAndNotifyc 函数 - 然后调用:
willChangeValueForKey- 原来的 setter
didChangeValueForKey,且 didChangeValueForKey 内部会触发监听器(Observer)的监听方法(- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context)。也就是发消息
QA:如何手动触发 KVO?
上面剖析了 KVO 的系统实现,所以手动触发 KVO 就需要调用 willChangeValueForKey 和 didChangeValueForKey
当没有observer观察任何一个property时,删除动态创建的子类
[self.person removeObserver:self forKeyPath:@"height"]; 该代码调用后,会删除动态创建的子类。
KVC
setValueForKey 用来设置对象的一层属性值修改。
setValueForKeyPath 可以设置对象的某个属性(属性本身是对象)的属性值修改
KVC 设值原理
KVC 之后会触发 KVO 吗?
发现 KVC 触发了 KVO。
问题来了:为什么 KVC 会触发 KVO?探究下 setValueForKey
整个流程如下
[self.person setValue:@10 forKey:@"age"] 会先调用 setKey: 同名的方法,找不到则调用 _setKey: 的方法,如果还是找不到则调用 +(BOOL)accessInstanceVariableDirectlt,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException
@implementation Person
- (void)setAge:(int)age
{
_age = age;
}
- (void)_setAge:(int)age
{
_age = age;
}
+ (BOOL)accessInstanceVariableDirectlt
{
return YES;
}
@end
直接修改成员变量会触发 KVO 吗?
不会。KVO 的实现原理就是在 setter 方法内,调用 willChangeValueForKey、didChangeValueForKey ,所以直接修改成员变量不会触发 KVO。
想要触发,可以手动调用上面2个 API。
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
KVC 取值原理
valueForKey 原理
-
按照 getKey、key、isKey、_key 的顺序寻找方法实现,找到则直接调用方法,返回值
-
如果没找到则调用
+(BOOL)accessInstanceVariableDirectly方法,询问是否可以访问成员变量- 为 NO 则调用
valueForUndefinedKey并抛出NSUnknownKeyException异常 - 为 YES 则按照
__key、_isKey、key、isKey的顺序访问成员变量。找到哪个则返回值
- 为 NO 则调用
-
都没找到则调用
valueForUndefinedKey并抛出NSUnknownKeyException异常
KVC 会破坏面向对象的原则吗?
KVC 有违背面向对象编程思想吗?如果一个类的成员变量是私有的,也没有在 .h 中公开一些方法去设置、修改成员变量,那么外部直接通过 KVC 去修改值,是有违背面向对象编程思想的。
KVC 提供了对应的能力,去保护或者说支持面向对象的原则。
基本用法-字典快速赋值
KVC 可以将字典里面和 model 同名的 property 进行快速赋值 setValuesForKeysWithDictionary。
//前提:model 中的各个 property 必须和 NSDictionary 中的属性一致
- (instancetype)initWithDic:(NSDictionary *)dic{
BannerModel *model = [BannerModel new];
[model setValuesForKeysWithDictionary:dic];
return model;
}
但是这里会有2种特殊情况。
- 情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值
运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
- 情况三:如果 Dictionary 和 Model 中的 property 不同名
我们照样可以利用 setValue:forUndefinedKey: 去处理
//model
@property (nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *sex;
@property (nonatomic,copy) NSString* age;
//NSDictionary
NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"};
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
if([key isEqualToString:@"id"]){
self.age=value;
}
if([key isEqualToString:@"username"]){
self.name=value;
}
}
- 情况四:如果我们观察对象的属性是数组,我们经常会观察不到变化,因为 KVO 是观察 setter 方法。我们可以用
mutableArrayValueForKeyPath进行属性的操作
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
[hobbies addObject:@"Web"];
- 情况五: 注册依赖键.
KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类,Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是,Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
[self.person addObserver:self
forKeyPath:NSStringFromSelector(@selector(dog))
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:ContextMark];
self.person.dog.name = @"啸天犬";
self.person.dog.weight = 50;
// Person.m
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
几个基本的知识点
-
KVO 观察者和属性被观察的对象之间不是强引用的关系
-
KVO 的触发分为
自动触发模式和手动触发模式2种。通常我们使用的都是自动通知,注册观察者之后,当条件触发的时候会自动调用-(void)observeValueForKeyPath。如果需要实现手动通知,我们需要使用下面的方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
- 若类有实例变量 NSString *_foo, 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key ?
都可以
- KVC 的 keyPath 中的集合运算符如何使用
- 必须用在 集合对象 或者 普通对象的集合属性 上
-简单的集合运算符有 @avg、@count、@max、@min、@sum
-
KVO 和 KVC 的 keyPath 一定是属性吗? 可以是成员变量
-
KVO 中 派生类的 setter 方法内部实现调用了 Foundation 框架中的
_NSSetIntValueAndNotify. -
直接修改对象的成员变量会触发 KVO 吗?
不会。因为成员变量没有 setter.
@interface Person: NSObject { @public: int age; } @end
实现机制
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
Apple 文档告诉我们:被观察对象的 isa指针 会指向一个中间类,而不是原来真正的类,
通过对被观察的对象断点调试发现 Person 类在执行过 addObserveValueForKeyPath... 方法后 isa 改变了。NSKVONotifying_Person。
-
KVO 是基于 Runtime 机制实现的
-
当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类(子类)。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现
通知机制 -
如果当前类为 Person,则生成的派生(子类)类名称为
NSKVONotifying_Person -
每个类对象中都有一个
isa指针指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的setter方法 -
键值观察通知依赖于 NSObject 的两个方法:
willChangeValueForKey:、didChangeValueForKey:。在一个被观察属性改变之前,调用willChangeValueForKey:记录旧的值。在属性值改变之后调用didChangeValueForKey:,从而observeValueForKey:ofObject:change:context:也会被调用。
为什么要选择是继承的子类而不是分类呢? 子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
关于分类与子类的关系可以看看我之前的 文章.
QA
iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么)
- 当对一个对象使用了 KVO 监听,系统会修改这个对象的 isa 指针,改为一个指向通过 Runtime 动态创建的子类
- 子类拥有自己对监听属性的 setter 实现,内部会调用
- willChangeValueForKey
- 原来的 setter 实现
- didChangeValueForKey,这个方法内部又会调用监听器的 监听方法
[observer observeValueForKey:ofObject:change:context:]
如何手动触发 KVO?
[p willChangeValueForKey:@"height"];
[p didChangeValueForKey:@"height"];
模拟实现系统的 KVO
- 创建被观察对象的子类
- 重写观察对象属性的 set 方法,同时调用
willChangeValueForKey、didChangeValueForKey - 外界改变 isa 指针(class方法重写)
我们用自己的类模拟系统的 KVO。
//NSObject+LBPKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LBPKVO)
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
//NSObject+LBPKVO.m
#import "NSObject+LBPKVO.h"
#import <objc/message.h>
@implementation NSObject (LBPKVO)
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//生成自定义的名称
NSString *className = NSStringFromClass(self.class);
NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingString:className];
//1. runtime 生成类
Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
// 生成后不能马上使用,必须先注册
objc_registerClassPair(myclass);
//2. 重写 setter 方法
class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
//3. 修改 isa
object_setClass(self, myclass);
//4. 将观察者保存到当前对象里面
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
//5. 将传递的上下文绑定到当前对象里面
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}
//
void setName (id self, SEL _cmd, NSString *name) {
NSLog(@"come here");
//先切换到当前类的父类,然后发送消息 setName,然后切换当前子类
//1. 切换到父类
Class class = [self class];
object_setClass(self, class_getSuperclass(class));
//2. 调用父类的 setName 方法
objc_msgSend(self, @selector(setName:), name);
//3. 调用观察
id observer = objc_getAssociatedObject(self, "observer");
id context = objc_getAssociatedObject(self, "context");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context);
}
//4. 改回子类
object_setClass(self, class);
}
@end
//ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[Person alloc] init];
_person.name = @"杭城小刘";
_person.age = 23;
_person.hobbies = [@[@"iOS"] mutableCopy];
NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" : @"弯弓射大雕"};
[_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.name = @"刘斌鹏";
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
[hobbies addObject:@"Web"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
KVO 的缺陷:
KVO 虽然很强大,你只能重写 -observeValueForKeyPath:ofObject:change:context: 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block 没门儿。感觉如果加入 block 就更棒了。
KVO 的改装:
看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。
