【iOS开发】——KVO与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

注册观察者

 /*
@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 提供的方法进行调用。

手动调用的步骤:

  1. 第一步我们需要认识下面这个方法,如果想要手动调用或自己实现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;
}
  1. 第二步我们需要重写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混写之后如何调用方法
  1. 调用监听的属性设置方法,如 setAge:,都会先调用 NSKVONotify_Person 对应的属性设置方法;
  2. 调用非监听属性设置方法,如 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

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

  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
- (nullable id)valueForKey:(NSString *)key;//直接通过Key来取值

  • 通过keyPath
- (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,即可对属性进行赋值。 只能访问当前类所具有的属性
- (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:的默认实现,给定输入参数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

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

上面是利用字典整体取值,接下来我们来看一下如何批量赋值:在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含keyvalue的字典进去,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_isKeykeyiskey顺序查找成员变量,找到直接赋值;
④ 未找到报错NSUnkonwKeyException错误。
在这里插入图片描述

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

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

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

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值