KVC
KVC定义
KVC(Key-value coding)键值编码,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定。
也就是说它提供一种机制来间接访问对象的属性,而不是通过调用Setter、Getter方法访问。KVO 就是基于 KVC 实现的关键技术之一。
KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC
KVC常用API
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;//通过keyPath设置值
- (void)setValue:(id)value forKey:(NSString *)key;//通过key设置值
- (id)valueForKeyPath:(NSString *)keyPath;//通过keyPath获取值
- (id)valueForKey:(NSString *)key;//通过key获取值
-
key:只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的,如view.setValue(CGRectZero(),key: “frame”);
-
keypath:除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链,如view.setValue(5,keypath: “layer.cornerRadius”);
KVC其它API
+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
KVC原理
设值
setValue:forKey:
的原理
-
首先会按照
setKey
、_setKey
的顺序查找方法,找到方法,直接调用方法并赋值; -
未找到方法,则调用
+ (BOOL)accessInstanceVariablesDirectly(
是否可以直接访问成员变量,默认返回YES); -
若
accessInstanceVariablesDirectly
方法返回YES,则按照_key
、_isKey
、key
、isKey
的顺序查找成员变量,找到直接赋值,找不到则抛出NSUnknowKeyExpection
异常; -
若
accessInstanceVariablesDirectly
方法返回NO,那么就会调用setValue:forUndefinedKey:
并抛出NSUnknowKeyExpection
异常;
valueForKey:
的原理
-
首先会按照
getKey
、key
、isKey
、_key
的顺序查找方法,找到直接调用取值 -
若未找到,则查看
+ (BOOL)accessInstanceVariablesDirectly
的返回值,若返回NO,则直接抛出NSUnknowKeyExpection
异常; -
若返回的YES,则按照
_ key
、_isKey
、key
、isKey
的顺序查找成员变量,找到则取值; -
找不到则调用
valueForUndefinedKey:
抛出NSUnknowKeyExpection
异常;
如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set:属性名时,会直接用setValue:forUndefinedKey:方法。
使用keyPath
在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性, 但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath。顾名思义,就是按照路径寻找key。
KVC 允许您通过指定属性路径来获取或设置对象的属性值。例如,如果您有一个对象 person
,并且想访问 person
的 address
对象中的 street
属性,可以使用以下语法:
NSString *street = [person valueForKeyPath:@"address.street"];
KVC处理异常
KVC处理nil异常
通常情况下,KVC不允许你要在调用setValue: forKey:
(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:
方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSUInteger age;
}
@end
@implementation Test
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将%@设成nil", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
[test setValue:nil forKey:@"age"];
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
return 0;
}
KVC处理UndefinedKey异常
通常情况下,KVC不允许你要在调用setValue: forKey:
(或者keyPath)时对不存在的key进行操作。 不然,会调用forUndefinedKey
给出异常,重写forUndefinedKey
方法避免崩溃。
#import <Foundation/Foundation.h>
@interface Test: NSObject {
}
@end
@implementation Test
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
[test setValue:@10 forKey:@"age"];
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
return 0;
}
KVC键值验证(Key-Value Validation)
KVC提供了验证Key对应的Value是否可用的方法:
- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;
该方法默认的实现是调用一个如下格式的方法:
- (BOOL)validate<Key>:error:
KVC是不会自动调用键值验证方法的,我们如果想要键值验证则需要手动验证
KVC处理字典
批量存值操作
KVC可以根据给定的一组key
,获取到一组value
,并且以字典的形式返回:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
批量赋值操作
通过KVC
可以使用对象调用setValuesForKeysWithDictionary:
方法时,可以传入一个包好key
、value
的字典进去,KVC
可以将所有数据按照属性名和字典的key
进行匹配,并将value
给对象的属性赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
下面是个例子:
@interface Person : NSObject
@property (nonatomic, copy)NSString* name;
@property (nonatomic, assign)NSInteger age;
@property (nonatomic, copy)NSString* sex;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Person* person = [[Person alloc] init];
[person setValue:@"Bill" forKey:@"name"];
[person setValue:@"12" forKey:@"age"];
[person setValue:@"male" forKey:@"sex"];
NSDictionary* firstDictionary = [person dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
NSLog(@"dictonary = %@", firstDictionary);
NSDictionary* secondDictionary = @{@"name":@"Danny", @"age":@12, @"sex": @"female"};
Person* secondPerson = [[Person alloc] init];
[secondPerson setValuesForKeysWithDictionary:secondDictionary];
NSLog(@"name = %@, age = %ld, sex = %@", secondPerson.name, secondPerson.age, secondPerson.sex);
}
return 0;
}
字典和模型转换
@interface Address()
@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;
@end
@implementation Address
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//模型转字典
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
NSLog(@"%@",dict);
//字典转模型
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
}
return 0;
}
用KVC实现高阶消息传递
当对容器类使用KVC时,valueForKey:
将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
}
return 0;
}
方法capitalizedString被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。 从打印结果可以看出,所有String都成功以转成了大写。 同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里。
KVO
KVO的全称是KeyValueObserving,俗称“键值监听",可以用于监听某个对象属性值的改变;KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。
基本思想:对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。
KVO是苹果提供的在套事件通知机制。KVO和NSNotificationCenter都是iOS中观察者模式的一种实现,区别是:NSNotificationCenter可以是一对多的关系,而KVO是一对一的;
KVO使用
注册KVO监听
通过[addObserver:forKeyPath:options:context:]
方法注册KVO,这样可以接收到keyPath属性的变化事件
-
observer:观察者,监听属性变化的对象。该对象必须实现
observeValueForKeyPath:ofObject:change:context:
方法。 -
keyPath:要观察的属性名称。要和属性声明的名称一致。
-
options:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
-
context:上下文,这个会传递到观察者的函数中,用来区分消息,所以应当是不同的。
options所包括的内容:
NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
实现KVO监听
通过方法[observeValueForKeyPath:ofObject:change:context:]
实现KVO的监听
keyPath
:被观察对象的属性object
:被观察的对象change
:字典,存放相关的值,根据options传入的枚举来返回新值旧值context
:注册观察者的时候,context传递过来的值
移除KVO监听
通过方法[removeObserver:forKeyPath:]
,移除监听;
处理变更通知
每当监听的keyPath发生变化了,就会在这个函数中回调:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions
手动KVO(禁用KVO)
KVO的实现是对注册的keyPath的setter方法中,自动插入并调用了两个函数。
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
手动实现KVO先需要关闭自动生成KVO通知,然后手动的调用。手动通知的好处就是,可以灵活加上自己想要的判断条件。
关闭KVO通知需要调用下面函数:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
接着手动实现属性的 setter 方法,在setter方法中先调用willChangeValueForKey:
接着进行赋值操作,然后调用willChangeValueForKey:
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
KVO和线程
KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Runloop 的处理。手动或者自动调用 -didChangeValueForKey:
会触发 KVO 通知。
KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:
- 保证所有监听某一属性的观察者在setter方法返回前被通知到
- 如果某个键被观察的时候附上了
NSKeyValueObservingOptionPrior
选项,直到observeValueForKeyPath:ofObject:change:context:
被调用之前,监听的属性都会返回同样的值
KVO实现
KVO 是通过 isa-swizzling 实现的。
基本流程:
- 首先编译器自动为被观察对象创造一个派生类(
NSKVONotifying_XXX
),并将被观察的实例对象的isa 指向这个派生类。让NSKVONotifying_XXX
的superclass
指针指向原来的类 - 如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个方法,并在其中添加进行通知的代码。
- Objective-C 在发送消息的时候,会通过 isa 指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。
- 由于编译器对派生类的方法进行了重写,并添加了通知代码,因此会向注册的对象发送通知。
注意派生类只重写注册了观察者的属性方法。
即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。这个类相比较于ObjectA,会重写以下几个方法:
- setter方法
- class方法
- delloc方法
- _isKVOA方法
重写setter方法
在 setter 中,会添加以下两个方法的调用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
然后在 didChangeValueForKey:
中,去调用:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
包含了新值和旧值的通知
重写class
这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass
判断的时候出错
- (Class)class {
- 这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
return class_getSuperclass(object_getClass(self));
}
当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface ObjectA: NSObject
@property (nonatomic) NSInteger age;
@end
@implementation ObjectA
@end
@interface ObjectB: NSObject
@end
@implementation ObjectB
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成对象
ObjectA *objA = [[ObjectA alloc] init];
ObjectB *objB = [[ObjectB alloc] init];
// 添加Observer之后
[objA addObserver:objB forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 输出ObjectA
NSLog(@"%@", [objA class]);
// 输出NSKVONotifying_ObjectA(object_getClass方法返回isa指向)
NSLog(@"%@", object_getClass(objA));
}
return 0;
}
object_getClass()函数可以返回对象的isa指向的实际类(class),而不是对象所属的类的类型。class方法返回的是对象所属的类的类型
重写dealloc
系统重写 dealloc 方法来释放资源。
重写_isKVOA
判断这个类有没有被KVO动态生成子类
总结
KVC
-
key的值必须正确,如果拼写错误,会出现异常。
-
当key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来。
-
因为类可以反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去。
-
NSArray/NSSet等都支持KVC。
-
可以通过KVC访问自定义类型的私有成员。
-
如果对非对象传递一个nil值,KVC会调用setNIlValueForKey方法,我们可以重写这个方法来避免传递nil出现的错误,对象并不会调用这个方法,而是会直接报错。
-
处理非对象,setValue时,如果要赋值的对象是基本类型,需要将值封装成NSNumber或者NSValue类型,valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或者NSValue。valueForKey可以自动将值封装成对象,但是setValue:forKey:却不行。我们必须手动讲值类型转换成NSNumber/NSValue类型才能进行传递initWithBool:(BOOL)value。
KVO
-
调用[removeObserver:forKeyPath:]需要在观察者消失之前,否则会导致Crash。
-
在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。
-
观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。
-
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
-
在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。
问题总结
直接修改成员变量的值,会不会触发KVO?
不会触发KVO,KVO的本质是替换了setter方法的实现,所以只有通过set方法修改才会触发KVO。
KVC修改属性会触发KVO吗?
会的 ,尽管setvalue:forkey:方法不一定会触发instance实例对象的setter:方法,但是setvalue:forkey:
在更改成员变量值的时候,会调用willchangevalueforkey
、didchangevalueforkey
,会触发监听器的回调方法。
KVO怎么监听数组的元素变化?
KVO默认只能监听到数组对象本身的变化,而无法监听到数组内部元素的变化。例如,如果将一个对象添加到数组中,KVO会收到数组count属性的变化通知,但不会收到数组内部元素的变化通知。
当数组中的元素发生变化时,手动触发KVO通知即可实现监听。具体实现方式如下:
使用NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
选项
KVO支持使用NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
选项,来监听可变数组中的元素变化。这两个选项会在KVO通知中包含新旧值的信息,因此可以在观察者中获取到数组中元素的变化。
[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,根据KVO通知中的信息来处理数组元素的变化。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"myArray"]) {
NSArray *oldArray = change[NSKeyValueChangeOldKey];
NSArray *newArray = change[NSKeyValueChangeNewKey];
// 处理数组元素的变化
}
}
这种方式需要被观察的对象的数组属性必须是可变的,而且只能监听到元素的增加、删除和替换操作
标签:调用,KVC,KVO,iOS,NSString,key,方法 From: https://blog.csdn.net/m0_73974920/article/details/140791296