iOS:KVO与KVC

KVC

kvc全称是 key value coding,又称“键值编码”,可以通过key获取或修改其对应值,因此会破坏面向对象思想。

它提供一种机制可以间接访问对象的属性,而不是通过setter/getter方法。

常见API

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

key和keyPah的区别

key:仅用于直接访问对象的属性,不能用于访问嵌套的属性或集合属性的元素。

keyPath:除了可以访问属性,还可以访问嵌套的属性或集合属性的元素。

@interface HFPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) HFAnimal *animal;

@end

@interface HFAnimal : NSObject
@property (nonatomic, copy) NSString *type;
@end
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"]; // 使用 setValue:forKey: 设置 name 属性
[person setValue:@30 forKey:@"age"]; // 使用 setValue:forKey: 设置 age 属性

HFAnimal *animal = [[HFAnimal alloc] init];
animal.type = @"Cat";
person.animal = animal; // 设置 animal 属性
        
NSString *name = [person valueForKey:@"name"]; // 使用 valueForKey: 获取 name 属性值
NSInteger age = [[person valueForKey:@"age"] integerValue]; // 使用 valueForKey: 获取 age 属性值
NSString *type = [person valueForKeyPath:@"animal.type"]; // 使用 valueForKeyPath: 获取嵌套属性值

NSString *name = [person valueForKey:@"name"]; // 使用 valueForKey: 获取 name 属性值

NSString *type = [person valueForKeyPath:@"animal.type"]; // 使用 valueForKeyPath: 获取嵌套属性值

可以直观的发现key和keyPath的不同。

KVC原理

1、-(void)setValue:(id)value forKey:(NSString *)key;

首先按照setKey,_setKey的顺序查找setter方法,找到方法则直接调用并赋值

若未找到方法,则调用+(BOOL)accessInstanceVariablesDirectly方法判断,是否可以直接访问成员变量,默认YES。

若accessInstanceVariablesDirectly返回YES,则按照_key、_isKey、key、_isKey的顺序查找成员变量,找到直接赋值,未找到或accessInstanceVariablesDirectly返回NO抛出NSUnknownKeyException异常就会调用setValue:forUndefinedKey:并抛出NSUnknownKeyException异常。

2、-(nullable id)valueForKey:(NSString *)key

首先会按照getKey、key、isKey、_key的顺序找到getter方法,若找到直接调用取值。

若未找到,则调用+(BOOL)accessInstanceVariablesDirectly方法判断,是否可以直接访问成员变量,默认YES。

若返回YES,则按照_key、_iskey、key、iskey的顺序匹配实例变量。如果找到这样的实例变量,则返回接收器中实例变量的值,若未找到或返回NO则调用-valueForUndefinedKey:,并抛出NSUnknownKeyException异常。


[图片上传中...(836c8db8b5bc1f415847a8628e1c79ce.png-39928c-1713449155581-0)]

注意事项

key的值必须正确,如果拼写错误,就会出现异常。
当key的值是未定义的,会调用valueForUndefinedKey,如果重写了这个方法,key的值出错的时候会调用到此处。

因为类可以反复嵌套,所以有keyPath,用路径实现访问。

可以通过kvc访问私有成员变量/属性。

如果参数类型不是对象指针类型,但值为nil,则调用setNilValueForKey:,-setNilValueForKey:的默认实现引发NSInvalidArgumentException,我们可以重写setNilValueForKey避免非对象传递nil出现的错误。对象传递nil不会调用此方法,会直接报错。

处理非对象,setValue时,如果要赋值的是基本数据类型,需要封装成NSNumber或NSValue类型,valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或NSValue类型。valueForKey可以自动将值封装成对象,但是setValye不行,必须手动转换再执行setValue。

KVC的更多应用场景

1、批量操作

批量存值:dictionaryWithValuesForKeys

批量赋值:setValuesForKeysWithDictionary

HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"]; // 使用 setValue:forKey: 设置 name 属性
[person setValue:@30 forKey:@"age"]; // 使用 setValue:forKey: 设置 age 属性
        
NSDictionary *dicOne = [person dictionaryWithValuesForKeys:@[@"name",@"age"]];
NSLog(@"dicOne = %@",dicOne);
        
NSDictionary *dicTwo = @{@"name":@"Mike",@"age":@35};
HFPerson *personTwo = [[HFPerson alloc] init];
[personTwo setValuesForKeysWithDictionary:dicTwo];
NSLog(@"personTwo.name = %@, age = %ld",personTwo.name,personTwo.age);

输出结果

dicOne = {
    age = 30;
    name = John;
}
personTwo.name = Mike, age = 35

2、字典模型互转

如果模型和dic不匹配

字典转模型:重写setValue:forUndefinedKey
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if([key isEqualToString:@"nickname"]) {
        self.name = (NSString *)value;
    }
}
NSDictionary *dicTwo = @{@"nickname":@"Mike",@"age":@35};
HFPerson *personTwo = [[HFPerson alloc] init];
[personTwo setValuesForKeysWithDictionary:dicTwo];
NSLog(@"personTwo.name = %@, age = %ld",personTwo.name,personTwo.age);

输出personTwo.name = Mike, age = 35
模型转字典,重写valueForUndefinedKey方法
- (nullable id)valueForUndefinedKey:(NSString *)key {
    if([key isEqualToString:@"nickname"]) {
        return self.name;
    }
    return nil;
}
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"];
[person setValue:@30 forKey:@"age"];
NSDictionary *tmp = [person dictionaryWithValuesForKeys:@[@"nickname",@"age"]];
NSLog(@"tmp = %@",tmp);

输出结果:
tmp = {
    age = 30;
    nickname = John;
}

KVO

KVO全称是Key-Value-Observing,又称“键值监听”,可以用于监听某个对象属性值的改变。

KVO和NSNotificationCenter都是iOS观察者模式的一种实现,区别是NSNotificationCenter一对多,KVO是一对一。

常见API

//1、注册KVO监听
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//2、KVO监听实现
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
//3、移除KVO监听
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

1、注册KVO监听

observer:观察者,监听属性变化的对象,该对象必须实现observeValueForKeyPath:ofObject:change:context方法。

keyPath:被观察的属性名称,要和声明的属性名称一致。

options:回调方法中收到被观察者属性的旧值或新值等,用于指定观察行为。

context:上下文标识符,用于区分不同的观察者,可为空。

2、KVO监听实现

keyPath:被观察的属性名称,要和声明的属性名称一致。

object:被观察的对象change:回调方法中收到被观察者属性的旧值或新值等,用于指定观察行为。

context:上下文标识符,用于区分不同的观察者,可为空。

3、移除KVO监听

observer:要移除的观察者
keyPath:要移除观察的属性名称,要和声明的属性名称一致。
context:上下文标识符,用于区分不同的观察者,可为空

注意:

1、removeObserver需要在观察者消失之前,否则会crash。

2、add和remove成对出现。

KVO原理

KVO的本质是为了改变setter方法的调用,实现原理就是当调用addObserverForKeyPath时,系统利用iisa混写技术(isa-swizzling),在运行时动态创建NSKVONotifying_A,将原来A的isa指向NSKVONotifying_A,NSKVONotifying_A的superClass指向原来的类。重写NSKVONotifying_A的setter方法,通过重写setter方法,达到可以通知所有观察者的目的。

@interface HFPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)printDescription;
@end

#import "HFPerson.h"
#import <objc/runtime.h>
@implementation HFPerson
- (void)printDescription {
    NSLog(@"isa = %@, super class = %@", NSStringFromClass(object_getClass(self)), NSStringFromClass(class_getSuperclass(object_getClass(self))));
    NSLog(@"self = %@, [self superclass] = %@",self, [self superclass]);
    NSLog(@"age setter pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(setAge:)));
    NSLog(@"name setter pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
    NSLog(@"printDescription pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(printDescription)));
}
@end
@property (nonatomic, strong) HFPerson *person;
self.person = [[HFPerson alloc]init];
self.person.name = @"HaiFei";
    //
NSLog(@" - - - 监听前打印 - - - ");
[self.person printDescription];
    
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
self.person.name = @"haifei";
NSLog(@" - - - 监听后打印 - - - ");
[self.person printDescription];

[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@" - - - 移除监听后打印 - - - ");
[self.person printDescription];

// 实现观察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"name changed to: %@", change[NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

输出打印

- - - 监听前打印 - - - 
isa = HFPerson, super class = NSObject
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x10ea34860
printDescription pointer: 0x10ea34710
name changed to: haifei
- - - 监听后打印 - - - 
isa = NSKVONotifying_HFPerson, super class = HFPerson
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x7ff800bdd491
printDescription pointer: 0x10ea34710
- - - 移除监听后打印 - - - 
isa = HFPerson, super class = NSObject
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x10ea34860
printDescription pointer: 0x10ea34710

代码分析可知,创建监听后,创建了新的类isa指向NSKVONotifying_HFPerson,并且监听属性的setter方法也发生了改变,从而达到了监听的目的。
在移除监听后isa又指向原来的类,监听属性的setter方法地址也变为原来的地址。
整个过程中未被监听的属性的setter方法地址没有变化,大概可以推断出新的类通过superClass指针获取原类的setter地址。

核心实现是NSKVONotifying_HFPerson内重写setter方法

- (void)setName:(NSString *)name {
    //表示属性值即将发生变化 -keyPath和注册kvo时key的一致
    [self willChangeValueForKey:@"keyPath"];
    //调用父类setter方法,即原类实现
    
    //表示属性值已经发生变化
    [self didChangeValueForKey:@"keyPath"];
}

常见问题

1、直接修改成员变量的值,会不会触发KVO

不会。KVO本质是生成找一个子类,重写父类的setter方法,直接修改员变量的值,不会触发setter方法。

但是可以仿写,在赋值前后加上
willChangeValueForKey
赋值
didChangeValueForKey

2、KVC修改属性会触发KVO吗?

会,在使用setValue:ForKey:修改属性的时候,会调用到属性的setter方法,最终触发到监听回调事件。

3、KVO和KVC的keypath一定是属性吗?

KVC支持实例变量,KVO只能手动支持实例变量的KVO监听。

4、KVO与代理的效率对比

KVO的效率比代理低,因为KVO需要动态的生产中间类,比较耗时。

5、KVO是如何监听数组元素变化的?

需要搭配KVC的mutableArrayValueForKey获得被监听属性,然后更新。

self.mutArray = [NSMutableArray arrayWithObject:@"one"];
[self addObserver:self forKeyPath:@"mutArray" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
[[self mutableArrayValueForKey:@"mutArray"]addObject:@"two"];
[self removeObserver:self forKeyPath:@"mutArray"];

// 实现观察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"mutArray"]) {
        NSLog(@"tmpName changed to: %@", change[NSKeyValueChangeNewKey]);
        NSLog(@"self.mutArray = %@",self.mutArray);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

打印输出
tmpName changed to: (
    two
)
self.mutArray = (
    one,
    two
)

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    恋空K阅读 757评论 0 2
  • 1. KVO 一.KVO原理的使用与证明 我们在开发的过程中经常使用KVO和KVC,但是我们并不了解其底层原理和功...
    周灬阅读 861评论 0 9
  • 【原创博文,转载请注明出处!】之前做iOS开发的时候经常使用KVO来监听对象属性值的变化去执行一些操作,但是从未思...
    RephontilZhou阅读 1,121评论 1 9
  • 一、KVO KVO 的作用: kvo 就是监听某个对象的属性,在该属性的值发生变化时,通知观察者。 KVO 的简单...
    MonStar丶阅读 1,678评论 0 7
  • KVO 基本使用 给 person 对象添加KVO监听 当监听对象的属性值发生改变时,就会调用。 移除监听 本质分...
    gaookey阅读 719评论 0 1