概述
【iOS开发】——KVO与KVC
- KVO
- KVO是什么?
- KVO的基本使用
- 调用方式
- 自动调用
- 手动调用
- KVO实现原理
- NSKVONotifying_Person类内部实现
- setter实现不同
- 总结KVO
- KVC
- KVC基础操作
- KVC取值
- 基于getter取值底层实现
- KVC设值
- 基于setter赋值底层实现
- 多值操作
- 总结KVC
- 通过KVC修改属性会触发KVO么?
- KVC的赋值和取值过程是怎样的?原理是什么?
- 用KVC来访问和修改私有变量
KVO
KVO是什么?
KVO
全称 Key Value Observing
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,只针对属性才会发生作用, 一般继承自 NSObject
的对象都默认支持 KVO。
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。 通过 KVC
的 mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调 KVO
监听的方法。集合对象包含 NSArray
和 NSSet
。
KVO的基本使用
KVO的使用总共分为三个步骤:
- 通过
addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件 - 在观察者中实现
observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者 - 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:
方法将KVO
移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash
注册观察者
/*
@observer:就是观察者,是谁想要观测对象的值的改变。
@keyPath:就是想要观察的对象属性。
@options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值。
@context:想要携带的其他信息,比如一个字符串或者字典什么的。
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@observe:
就是观察者,是谁想要观测对象的值的改变。@keyPath:
就是想要观察的对象属性。@options:
options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
,这样当属性值发生改变时我们可以同时获得旧值和新值, 如果我们只填NSKeyValueObservingOptionNew
则属性发生改变时只会获得新值。@context:
想要携带的其他信息,比如一个字符串或者字典什么的。
监听回调
/*
@keyPath:观察的属性
@object:观察的是哪个对象的属性
@change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
@context:上面添加观察者时携带的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@keyPath:
观察的属性@object:
观察的是哪个对象的属性change:
这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值@context:
上面添加观察者时携带的信息
移除监听
当观察者不需要监听时,可以调用-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
方法将KVO
移除,我们需要在观察者消失之前进行处理,否则就crash
了
observer
:观察者keyPath
: 被观察的对象的属性
但是我们真的可以不手动删除观察者吗?不会报错不等于是错的,可能会有隐患,不移除观察者,系统不会直接报错,但是存在隐患,如果观察者已经销毁了,被观察的对象没有销毁(比如我们对单例中的一个属性进行观察),然后又产生了KVO message,这时候就抛异常了,EXC_BAD_ACCESS
调用方式
自动调用
调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,还可以使用KVC方法
//通过属性的点语法间接调用
objc.name = @"";
// 直接调用set方法
[objc setName:@"Savings"];
// 使用KVC的setValue:forKey:方法
[objc setValue:@"Savings" forKey:@"name"];
// 使用KVC的setValue:forKeyPath:方法
[objc setValue:@"Savings" forKeyPath:@"account.name"];
手动调用
KVO
在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现 KVO
属性的调用,则可以通过 KVO
提供的方法进行调用。
手动调用的步骤:
- 第一步我们需要认识下面这个方法,如果想要手动调用或自己实现KVO需要重写该方法该方法返回YES表示可以调用,返回NO则表示不可以调用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"name"]) {
automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
- 第二步我们需要重写setter方法
- (void)setName:(NSString *)name {
if (name != _name) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
KVO实现原理
KVO是通过isa 混写(isa-swizzling)技术
实现的。 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
看一下这段代码:
NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
NSLog(@"person1添加KVO监听对象之前-类对象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO监听之前-方法实现 -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO监听之前-元类对象 -%@", object_getClass(object_getClass(self.person1)));
[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
NSLog(@"person1添加KVO监听对象之后-类对象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO监听之后-方法实现 -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO监听之后-元类对象 -%@", object_getClass(object_getClass(self.person1)));
//打印结果
KVO-test[1214:513029] person1添加KVO监听对象之前-类对象 -Person
KVO-test[1214:513029] person1添加KVO监听之前-方法实现 -0x100411470
KVO-test[1214:513029] person1添加KVO监听之前-元类对象 -Person
KVO-test[1214:513029] person1添加KVO监听对象之后-类对象 -NSKVONotifying_Person
KVO-test[1214:513029] person1添加KVO监听之后-方法实现 -0x10076c844
KVO-test[1214:513029] person1添加KVO监听之后-元类对象 -NSKVONotifying_Person
//通过地址查找方法
(lldb) p (IMP)0x10f24b470
(IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x10f5a6844
(IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)
通过上面的代码,我们可以发现KVO添加以后发生了如下变化:
- person指向的类对象和元类对象,以及 setAge: 均发生了变化;
- 添加KVO后,person 中的 isa 指向了
NSKVONotifying_Person
类对象; - 添加 KVO 之后,
setAge:
的实现调用的是:Foundation 中 _NSSetLongLongValueAndNotify
方法;
KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。
这也就是上边代码person
中的 isa 从开始指向Person类对象,变成指向了 NSKVONotifying_Person 类对象
未使用KVO监听对象是,对象和类对象之间的关系如下:
使用KVO监听对象后,对象和类对象之间会添加一个中间对象:
NSKVONotifying_Person类内部实现
我们来看一下这个中间类NSKVONotifying_Person
的内部是如何实现的
- (void)setAge:(int)age{
_NSSet*ValueAndNotify();//这个方法调用顺序是什么,它是在调用何处方法,都在setter方法改变中详解
}
- (Class)class {
return [LDPerson class];
}
- (void)dealloc {
// 收尾工作
}
- (BOOL)_isKVOA {
return YES;
}
- isa混写之后如何调用方法
- 调用监听的属性设置方法,如 setAge:,都会先调用 NSKVONotify_Person 对应的属性设置方法;
- 调用非监听属性设置方法,如 test,会通过 NSKVONotify_Person 的 superclass,找到 Person 类对象,再调用其 [Person test] 方法
- 为什么重写
class
方法
-
如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person,就会将该类暴露出来,也给开发者造成困扰,写的是Person,添加KVO之后class方法返回怎么是另一个类。
_isKVOA
有什么作用
-
这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法。
setter实现不同
我们可以看到在添加KVO后set方法的实现从调用setAge:
方法变成调用_NSSetIntValueAndNotify
这样一个C函数
我们不知道_NSSetIntValueAndNotify
到底是什么样的函数,无法得知它的真实结构,也无法去重写NSKVONotifying_Person
这个类,但我们可以利用它的父类Person类来分析其执行过程。
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
//打印结果
KVO-test[1457:637227] willChangeValueForKey
KVO-test[1457:637227] setAge:
KVO-test[1457:637227] didChangeValueForKey - begin
KVO-test[1457:637227] didChangeValueForKey - end
KVO-test[1457:637227] willChangeValueForKey
KVO-test[1457:637227] didChangeValueForKey - begin
KVO-test[1457:637227] didChangeValueForKey - end
通过打印结果,我们可以得出以下结论:
- 首先调用
willChangeValueForKey:
方法。 - 然后调用
setAge:
方法真正的改变属性的值。 - 开始调用
didChangeValueForKey:
这个方法,调用[super didChangeValueForKey:key]
时会通知监听者属性值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
这个方法。
总结KVO
看一下KVO的整个执行流程图:
KVC
KVC(Key-value coding)
键值编码,单看这个名字可能不太好理解。其实是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,很多高级的iOS开发技巧都是基于KVC实现的。 KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。 在NSKeyValueCoding
中提供了KVC通用的访问方法,分别是getter方法valueForKey
和setter方法setValue:forKey
,以及其衍生的keyPath方法
,这两个方法是各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。
KVC基础操作
KVC取值
通过key
- (nullable id)valueForKey:(NSString *)key;//直接通过Key来取值
通过keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath;//通过KeyPath来取值
基于getter取值底层实现
当调用valueForKey
的代码时,其搜索方式如下:
- 通过
getter方法
搜索实例,按照get<Key>
,<key>
,is<Key>
,_<key>
的顺序查找getter`方法。如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。 - 如果没有找到简单的
getter方法
,则搜索其匹配模式的方法countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
。如果找到其中的第一个和其他两个中的一个,则就会返回一个可以响应NSArray
所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子类)。或者说给这个代理集合发送属于NSArray
的方法,就会以countOf<Key>,objectIn<Key>AtIndex
或<Key>AtIndexes
这几个方法组合的形式调用。否则,继续到第三步。代理对象随后将NSArray
接收到的countOf<Key>
objectIn<Key>AtIndex:
、<key>AtIndexes:
的消息给符合KVC规则的调用方。当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。 - 如果没有找到
NSArray
简单存取方法,或者NSArray存取方法组。那么会同时查找countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
命名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet
方法并返回。 否则,继续执行第四步。给这个代理对象发NSSet
的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用。 - 如果没有发现简单getter方法,或集合存取方法组,以及接收类方法
accessInstanceVariablesDirectly
是返回YES的。 搜索一个名为_<key>
、_is<Key>
、<key>
、is<Key>
的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly
返回NO的话,那么会直接调用valueForUndefinedKey:。 - 如果取回的是一个
对象指针
,则直接返回这个结果。 如果取回的是一个基础数据类型
,但是这个基础数据类型是被NSNumber
支持的,则存储为NSNumber
并返回。如果取回的是一个不支持NSNumber
的基础数据类型,则通过NSValue
进行存储并返回。 - 如果所有情况都失败,则调用
valueForUndefinedKey:
方法并抛出异常,这是默认行为。但是子类可以重写此方法。
KVC设值
通过key
直接将属性名当做key,并设置value,即可对属性进行赋值。 只能访问当前类所具有的属性
- (void)setValue:(nullable id)value forKey:(NSString *)key;//通过Key来设值
通过keyPath
除了能访问当前类的属性,还能访问当前类属性的属性,多层访问
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;//通过KeyPath来设值
放一个关于多层访问的demo:
//Person类的定义
#import <Foundation/Foundation.h>
#import "Room.h"
#import "Son.h"
NS_ASSUME_NONNULL_BEGIN
@class Son;
@interface Person : NSObject
@property (nonatomic,strong)Son *son;
@end
NS_ASSUME_NONNULL_END
//Son类的定义
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Son : NSObject
@property (nonatomic,copy) NSString * name;
@end
NS_ASSUME_NONNULL_END
//main函数
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Room.h"
#import "Son.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person1 = [[Person alloc] init];
person1.son = [[Son alloc] init];
[person1 setValue:@"Yep" forKeyPath:@"son.name"];
NSLog(@"%@",person1.son.name);
}
return 0;
}
从这里也就能看出来key和keyPath的区别就是前者是只能访问本类的属性,而后者可以访问当前类属性的属性
基于setter赋值底层实现
这是setValue:forKey:
的默认实现,给定输入参数value
和key
。试图在接收调用对象的内部,设置属性名为key
的value
,通过下面的步骤:
- 查找
set<Key>:
或_set<Key>
命名的setter
,按照这个顺序,如果找到的话,代码通过setter方法完成设置。 - 如果没有找到
setter方法
,KVC
机制会检查+ (BOOL)accessInstanceVariablesDirectly
的返回值,如果accessInstanceVariablesDirectly
类属性返回YES,则查找一个命名规则为_<key>
、_is<Key>
、<key>
、is<Key>
的实例变量。根据这个顺序,如果发现则将value
赋值给实例变量,如果返回值为NO,KVC会执行setValue:forUndefinedKey:
方法。 - 如果没有发现setter或实例变量,则调用
setValue:forUndefinedKey:
方法,并默认提出一个异常,但是一个NSObject
的子类可以提出合适的行为。
多值操作
KVC可以根据给定的一组key
,获取到一组value
,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
上面是利用字典整体取值,接下来我们来看一下如何批量赋值:在对象调用setValuesForKeysWithDictionary:
方法时,可以传入一个包含key
、value
的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
看一个小demo:
//创建一个Student模型,里面的字符串名称必须和key的名称对应,不然该方法会崩溃
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSString *sex;
@property(nonatomic, strong) NSString *age;
@property(nonatomic, strong) NSString *Aka;
@end
NS_ASSUME_NONNULL_END
//在main函数里,声明Stduent类并利用批量赋值给Student对应的属性
Student *student = [[Student alloc] init];
NSDictionary *dictionary = @{@"name":@"wyf",@"sex":@"boy",@"Aka":@"Yep"};
//批量赋值
[student setValuesForKeysWithDictionary:dictionary];
NSLog(@"%@",student);
NSLog(@"%@,%@,%@,%@",student.name,student.sex,student.age,student.Aka);
NSDictionary *dictionaryStudent = [student dictionaryWithValuesForKeys:@[@"name",@"sex",@"Aka"]];
NSLog(@"dictionaryStudent : %@",dictionaryStudent);
通过打印结果我们可以看到:
我们可以看到打印结果中Student里有一个属性的值为null,这是为什么呢?因为在 Student属性和 dictionary 不匹配,可以重写方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"age"]) {
self.age = (NSString *)value;
}
}
总结KVC
通过KVC修改属性会触发KVO么?
会触发,我们看一下以下的代码:
Person *p1 = [[Person alloc]init];
p1.age = 10;
// --------------- VS ----------------
Person *p2 = [[Person alloc]init];
[p2 setValue:@10 forKey:@"age"];
它们的本质都一样,都会调用[self willChangeValueForKey:key];
和 [self didChangeValueForKey:key];
KVC的赋值和取值过程是怎样的?原理是什么?
setValue:forKey:
赋值的原理
① 首先会查找setKey:
、_setKey:
(按顺序查找);
② 如果有直接调用,如果没有,先查看accessInstanceVariablesDirectly方法
;
③ 如果可以,访问会按照 _key
、_isKey
、key
、iskey
的顺序查找成员变量,找到直接赋值;
④ 未找到报错NSUnkonwKeyException
错误。
valueForKey:
取值的原理
① kvc取值按照 getKey
、key
、iskey
、_key
顺序查找;
② 存在直接调用,如果没找到,同样会先查看accessInstanceVariablesDirectly方法
;
③ 如果可以访问会按照 _key
、_isKey
、key
、iskey
的顺序查找成员变量,找到直接赋值
④ 未找到报错NSUnkonwKeyException
错误。
用KVC来访问和修改私有变量
KVC的本质是操作方法列表以及在内存中查找实例变量。
我们可以利用这个特性访问类的私有变量。
同样如果不想让外界使用KVC的方法访问类的成员变量,可以将accessInstanceVariablesDirectly
属性设置为NO
最后
以上就是孤独向日葵为你收集整理的【iOS开发】——KVO与KVCKVOKVC的全部内容,希望文章能够帮你解决【iOS开发】——KVO与KVCKVOKVC所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复