我是靠谱客的博主 孤独向日葵,这篇文章主要介绍【iOS开发】——KVO与KVCKVOKVC,现在分享给大家,希望可以做个参考。

【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 监听的方法。集合对象包含 NSArrayNSSet

KVO的基本使用

KVO的使用总共分为三个步骤:

  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者
  3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash

注册观察者

复制代码
1
2
3
4
5
6
7
8
9
/* @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:想要携带的其他信息,比如一个字符串或者字典什么的。

监听回调

复制代码
1
2
3
4
5
6
7
8
/* @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方法

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
//通过属性的点语法间接调用 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 提供的方法进行调用。

手动调用的步骤:

  1. 第一步我们需要认识下面这个方法,如果想要手动调用或自己实现KVO需要重写该方法该方法返回YES表示可以调用,返回NO则表示不可以调用。
复制代码
1
2
3
4
5
6
7
8
9
10
11
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"name"]) { automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }
  1. 第二步我们需要重写setter方法
复制代码
1
2
3
4
5
6
7
8
- (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实例方法来获取对象类型。

看一下这段代码:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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的内部是如何实现的

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setAge:(int)age{ _NSSet*ValueAndNotify();//这个方法调用顺序是什么,它是在调用何处方法,都在setter方法改变中详解 } - (Class)class { return [LDPerson class]; } - (void)dealloc { // 收尾工作 } - (BOOL)_isKVOA { return YES; }
  • isa混写之后如何调用方法
  1. 调用监听的属性设置方法,如 setAge:,都会先调用 NSKVONotify_Person 对应的属性设置方法;
  2. 调用非监听属性设置方法,如 test,会通过 NSKVONotify_Person 的 superclass,找到 Person 类对象,再调用其 [Person test] 方法
  • 为什么重写class方法
  • 复制代码
    1
    2
    如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person,就会将该类暴露出来,也给开发者造成困扰,写的是Person,添加KVO之后class方法返回怎么是另一个类。
  • _isKVOA有什么作用
  • 复制代码
    1
    2
    这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法。

setter实现不同

我们可以看到在添加KVO后set方法的实现从调用setAge:方法变成调用_NSSetIntValueAndNotify这样一个C函数

我们不知道_NSSetIntValueAndNotify到底是什么样的函数,无法得知它的真实结构,也无法去重写NSKVONotifying_Person这个类,但我们可以利用它的父类Person类来分析其执行过程。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (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

通过打印结果,我们可以得出以下结论:

  1. 首先调用willChangeValueForKey:方法。
  2. 然后调用setAge:方法真正的改变属性的值。
  3. 开始调用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方法valueForKeysetter方法setValue:forKey,以及其衍生的keyPath方法这两个方法是各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。

KVC基础操作

KVC取值

  • 通过key
复制代码
1
2
3
- (nullable id)valueForKey:(NSString *)key;//直接通过Key来取值
  • 通过keyPath
复制代码
1
2
- (nullable id)valueForKeyPath:(NSString *)keyPath;//通过KeyPath来取值

基于getter取值底层实现

当调用valueForKey的代码时,其搜索方式如下:
在这里插入图片描述

  1. 通过getter方法搜索实例,按照get<Key>, <key>, is<Key>, _<key>的顺序查找getter`方法。如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
  2. 如果没有找到简单的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一样。
  3. 如果没有找到NSArray简单存取方法,或者NSArray存取方法组。那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>:名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。 否则,继续执行第四步。给这个代理对象发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>,memberOf<Key>组合的形式调用。
  4. 如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。 搜索一个名为_<key>_is<Key><key>is<Key>的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:。
  5. 如果取回的是一个对象指针,则直接返回这个结果。 如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
  6. 如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

KVC设值

  • 通过key
    直接将属性名当做key,并设置value,即可对属性进行赋值。 只能访问当前类所具有的属性
复制代码
1
2
- (void)setValue:(nullable id)value forKey:(NSString *)key;//通过Key来设值
  • 通过keyPath
    除了能访问当前类的属性,还能访问当前类属性的属性,多层访问
复制代码
1
2
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;//通过KeyPath来设值

放一个关于多层访问的demo:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//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:的默认实现,给定输入参数valuekey。试图在接收调用对象的内部,设置属性名为keyvalue,通过下面的步骤:
在这里插入图片描述

  1. 查找set<Key>:_set<Key>命名的setter按照这个顺序,如果找到的话,代码通过setter方法完成设置。
  2. 如果没有找到setter方法KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly的返回值,如果accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为_<key>_is<Key><key>is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量,如果返回值为NO,KVC会执行setValue:forUndefinedKey:方法。
  3. 果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。

多值操作

KVC可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value

复制代码
1
2
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

上面是利用字典整体取值,接下来我们来看一下如何批量赋值:在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含keyvalue的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。

复制代码
1
2
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

看一个小demo:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//创建一个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

复制代码
1
2
3
4
5
6
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { if([key isEqualToString:@"age"]) { self.age = (NSString *)value; } }

总结KVC

通过KVC修改属性会触发KVO么?

会触发,我们看一下以下的代码:

复制代码
1
2
3
4
5
6
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_isKeykeyiskey顺序查找成员变量,找到直接赋值;
④ 未找到报错NSUnkonwKeyException错误。
在这里插入图片描述

valueForKey: 取值的原理
kvc取值按照 getKeykeyiskey_key 顺序查找;
存在直接调用,如果没找到,同样会先查看accessInstanceVariablesDirectly方法
如果可以访问会按照 _key_isKeykeyiskey的顺序查找成员变量,找到直接赋值
④ 未找到报错NSUnkonwKeyException错误。
在这里插入图片描述

用KVC来访问和修改私有变量

KVC的本质是操作方法列表以及在内存中查找实例变量。
我们可以利用这个特性访问类的私有变量。

同样如果不想让外界使用KVC的方法访问类的成员变量,可以将accessInstanceVariablesDirectly属性设置为NO

最后

以上就是孤独向日葵最近收集整理的关于【iOS开发】——KVO与KVCKVOKVC的全部内容,更多相关【iOS开发】——KVO与KVCKVOKVC内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(106)

评论列表共有 0 条评论

立即
投稿
返回
顶部