iOS ------ KVO KVC

一, KVO

KVO介绍

  • KVO全称KeyValueObserving,俗称键值监听,是苹果提供的一套时事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接受事件。一般继承自NSObject的对象都默认支持KVO
  • KVO和NSNotificationCenter都是iOS观察者模式的一种实现。KVO对被监听对象无侵入性,不需要修改内部代码可以实现监听

实现原理

  • KVO是通过isa_swizzing技术实现的
  • 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改instance对象的属性时,会调用Foundation框架的_NSSetXXXValueAndNotify函数,该函数里面会先调用willChangeValueForkey:然后调用父类的setter方法修改值,最后是didChangeForKey:。didChangeValueForKey内部会触发监听器(Overseer) 的监听方法observeValueForKeyPath:ofObject:context:
  • 并且将class方法重写,返回原类的Class

KVO的使用

1.通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接受keyPath属性的变化事件

  • observer:观察者,监听属性变化的对象
  • keyPath:要观察的属性名称。要和属性声明的名称一致
  • options:回调方法中收到被观察者的属性的旧值或新值等 。对KVO机制进行配置,修改KVO通知的时机以及通知的内容
  • context:传入任意类型的对象,在接受消息回调的代码中可以接受到这个对象,是KVO中的一致传值方式

2.在观察者中实现observeValueForKeyPath:ofObject:change:context方法,当keyPath属性发生改变后,KVO会回调这个方法通知观察者

  • keyPath:被观察对象的属性
  • object:被观察的对象
  • change:字典,存放相关的额值,根据options传入的枚举来返回新值旧值
  • context:注册观察者的时候,context传递过来的值

3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除

  • 调用removeObserver需要在观察者消失之前,否则会导致Crash。
  • 如果已经移除了监听,如果再次移除的时候,就会crash

对类对象进行验证

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self setNameKVO];
}

- (void)setNameKVO {
    self.person = [[Person alloc] init];
    self.person2 = [[Person alloc] init];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object, keyPath, change, context);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = @"cccc";
    self.person2.name = @"aaaa";
}

- (void) dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}

点击屏幕,打印结果

2024-07-29 15:19:56.184170+0800 KVO[43833:2012363] 监听到<Person: 0x600001ba2580>的name属性值改变了 - {
    kind = 1;
    new = cccc;
    old = "<null>";
} - 1111

KVO是通过isa_swizzing技术实现的。我们通过打断点,打印person和person2的isa指针。

(lldb) po self.person->isa
0x020060000383c843

(lldb) po self.person2->isa
Person

通过打印我们知道两者的类对象并不相同,但person具体的类对象并没有打印出来。

导入runtime我们在注册观察者前后对两者的类进行打印

NSLog(@"person添加KVO监听之前 - %@ %@",object_getClass(self.person) , object_getClass(self.person2));
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"person添加KVO监听之前 - %@ %@", object_getClass(self.person), object_getClass(self.person2));

打印结果

2024-07-29 15:19:52.524876+0800 KVO[43833:2012363] person添加KVO监听之前 - Person Person
2024-07-29 15:19:52.525004+0800 KVO[43833:2012363] person添加KVO监听之前 - NSKVONotifying_Person Person

添加KVO监听之后,person的类对象变为了NSKVONotifying_YZPerson,这是苹果为我们生成的中间类。

对setter方法IMP进行验证

当改变name属性的时候,是调用setName:进行的,我们来看看setName:有什么变化

    NSLog(@"person添加KVO监听之前 - %p %p",[self.person methodForSelector:@selector(setName:)], [self.person2 methodForSelector:@selector(setName:)]);
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
        NSLog(@"person添加KVO监听之后 - %p %p",[self.person methodForSelector:@selector(setName:)], [self.person2 methodForSelector:@selector(setName:)]);

打印结果

2024-07-29 15:29:08.844353+0800 KVO[43989:2019265] person添加KVO监听之前 - 0x1043b5ab8 0x1043b5ab8
2024-07-29 15:29:08.844517+0800 KVO[43989:2019265] person添加KVO监听之后 - 0x180b60f08 0x1043b5ab8

添加监听后,self.person的setName的地址发生了改变

进一步打断点获取详细信息

2024-07-29 15:35:05.080958+0800 KVO[44095:2024193] person添加KVO监听之前 - 0x1041d1ab8 0x1041d1ab8
2024-07-29 15:35:05.081123+0800 KVO[44095:2024193] person添加KVO监听之后 - 0x180b60f08 0x1041d1ab8
(lldb) po (IMP) 0x1041d1ab8
(KVO`-[Person setName:] at Person.h:14)
(lldb) po (IMP) 0x180b60f08
(Foundation`_NSSetObjectValueAndNotify)

可以看到添加KVO监听之后,setName:方法的IMP指向了Fondation框架下的_NSSetObjectValueAndNotify

内部调用流程

设置了kvo监听之后,内部调用有什么流程。我们在Person中添加如下代码


#import "YZPerson.h"

@implementation YZPerson
- (void)setName:(NSString *)name{
     _name = name;   
}

- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

打印结果

2024-07-29 15:46:31.300565+0800 KVO[44318:2032881] willChangeValueForKey
2024-07-29 15:46:31.300689+0800 KVO[44318:2032881] didChangeValueForKey - begin
2024-07-29 15:46:31.301102+0800 KVO[44318:2032881] 监听到<Person: 0x600001662480>的name属性值改变了 - {
    kind = 1;
    new = cccc;
    old = "<null>";
} - 1111
2024-07-29 15:46:31.301192+0800 KVO[44318:2032881] didChangeValueForKey - end

发现在调用 [super didChangeValueForKey:key];的时候,监听到对象的改变,进而处理监听逻辑

窥探 NSKVONotifying_Person 的方法

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

-(void)setNameKVO{
    
    self.person = [[YZPerson alloc] init];

    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"person添加KVO监听之后 - self.person的类是:%@   里面的方法有:",object_getClass(self.person));
    [self printMethodNamesOfClass:object_getClass(self.person)];
}

打印结果

2024-07-29 16:11:14.569381+0800 KVO[44880:2053144] NSKVONotifying_Person setName:, class, dealloc, _isKVOA,

系统重写了新建的子类 NSKVONotifying_Person 的setName, class, dealloc,新增了 _isKVOA方法。

setName

在NSKVONotifying_Person ,系统会重写这个方法实现属性变化的通知

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

作用:willChangeValueForKey:didChangeValueForKey:会在属性变化之前和之后调用,触发KVO通知机制。观察者会在didChangeValueForKey:调用时收到通知。

class

- (Class)class {
    return [YZPerson class];
}

重写class方法,使其返回原始类Person,而不是其真实动态生成的KVO子类。保持了对象的分装性和透明性

dealloc

- (void)dealloc {
    // 通常会调用移除所有观察者的代码
    [self removeObserver:self forKeyPath:@"name"];
    // 清理其他资源
    [super dealloc];
}

确保在对象销毁前,所有的KVO观察者都被正确的移除,防止因为访问已经释放的对象而导致崩溃

_isKVOA

- (BOOL)_isKVOA {
    return YES;
}

新增的私有方法,由于标识对象是否被KVO代理

手动调用KVO

KVO监听的关键 willChangeValueForKeydidChangeValueForKey 起了关键作用,一般来说只有监听属性发生变化的时候,才能触发监听,但是如果我们想自己手动调用KVO的话,只要自己手动调用这两个方法就可以了。

-(void)setNameKVO{
    
    self.person = [[YZPerson alloc] init];
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"person添加KVO监听之后 - self.person的类是:%@   里面的方法有:",object_getClass(self.person));
    [self printMethodNamesOfClass:object_getClass(self.person)];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //    self.person.name = @"ccc";
    // 手动调用KVO
    [self.person willChangeValueForKey:@"name"];
    
    [self.person didChangeValueForKey:@"name"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
    // 移除监听
    [self.person removeObserver:self forKeyPath:@"name"];
}

打印结果

2024-07-29 16:44:50.179131+0800 KVO[45535:2079896] 监听到<Person: 0x60000350cf80>的name属性值改变了 - {
    kind = 1;
    new = "<null>";
    old = "<null>";
} - 1111

用于new = null,也就是name的值没有改变,我们手动调用才触发的监听。

KVC

  • KVC是Key Value Coding的简称。它是一种痛过字符串的名字(key)来访问类属性的机制。而不是通过Setter和Getter方法访问。KVC的方法定义在Foundation/NSKeyValueCoding中。
  • KVO和KVC都属于键值编程而底层实现机制都是isa_swizzing

常见的API有

-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
-(void)setValue:(id)value forKey:(NSString *)key;
-(id)valueForKeyPath:(NSString *)keyPath;
-(id)valueForKey:(NSString *)key;

key和keyPath的区别

key:只能访问对象的当前层级属性
keyPath:可以访问对象层次结构中的嵌套属性

多值操作
给定一组Key,获得一组value,以字典的形式返回。该方法为数组中的每个Key调用valueForKey:方法。

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

将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil替换NSNull对象

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;

例子


@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@property (nonatomic, strong) NSString *sex;

@end
Person* person = [[Person alloc] init];
    NSDictionary* dictionary = @{@"name":@"fu",@"age":@66,@"sex":@"sex"};
    [person setValuesForKeysWithDictionary:dictionary];
    NSLog(@"name:%@",person.name);
    NSLog(@"age:%@",person.age);
    NSLog(@"sex:%@",person.sex);
    
    NSDictionary* dictionary1 = [person dictionaryWithValuesForKeys:@[@"name",@"age",@"sex"]];
    NSLog(@"Dictionary : %@", dictionary1);

输出

2024-07-29 21:24:40.266469+0800 KVC1.0[51192:2260944] model.name:fu
2024-07-29 21:24:40.266530+0800 KVC1.0[51192:2260944] model.age:66
2024-07-29 21:24:40.266572+0800 KVC1.0[51192:2260944] model.sex:sex
2024-07-29 21:24:40.266640+0800 KVC1.0[51192:2260944] tempModelDictionary : {
    age = 66;
    name = fu;
    sex = sex;
}

集合类型

FXPerson *person = [FXPerson new];

// 赋值
ThreeFloats floats = {180.0, 180.0, 18.0};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"非对象类型%@", [person valueForKey:@"threeFloats"]);

// 取值
ThreeFloats th;
NSValue *currentValue = [person valueForKey:@"threeFloats"];
[currentValue getValue:&th];
NSLog(@"非对象类型的值%f-%f-%f", th.x, th.y, th.z);

非对象处理
KVC支持基础数据类型和结构体,在使用KVC进行赋值或取值的时候,会自动在非对象值和对象值之间进行转换。

  • 当进行取值如valueForKey:时,如果返回值非对象,会使用该值初始化一个NSNumber(用于基础数据类型)或NSValue(用于结构体)实例,然后返回该实例。
  • 当进行赋值如setValue:forKey:时,如果key的数据类型非对象,则会发送一条<type>Value消息给value对象以提取基础数据,然后赋值给key。

赋值setValue:forKey的原理

  1. 按照setKey:_setKey:的顺序查找方法,如果找到方法就传递参数,调用方法
  2. 如果没有找到。查看accessInstanceVariablesDirectly方法的返回值如果返回NO,调用setValue:forUnderfinedKey:并抛出异常
  3. 如果返回YES,按照_key,_iskey,key,iskey的顺序查找成员变量。如果找到成员变量,就直接赋值
  4. 如果没找到,就调用setValue:forUnderfinedKey:并抛出异常

在这里插入图片描述

取值 valueForKey:的原理

1,按照getKeykeyisKey_key的顺序查找方法,如果找到了,就直接调用
2,如果没找到,就查看accessInstanceVariablesDirectly 方法的返回值,如果返回NO 调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
3,YES 按照_key_isKeykeyisKey的顺序查找成员变量
如果找到了成员变量,就直接取值。
4,如果没有查找到成员变量就调用valueForUndefinedKey:并抛出异常NSUnknownKeyException

在这里插入图片描述

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值