1、KVC
键值编码,通过Key名直接访问对象属性,由NSKeyValueCoding
非正式协议启用的机制
@interface LZPerson : NSObject {
@public
NSString *name;
}
#import "ViewController.h"
#import "LZPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LZPerson *p = [LZPerson alloc];
p->name = @"lz";
[p setValue:@"kvcValue" forKey:@"name"];
NSLog(@"%@",[p valueForKey:@"name"]);
}
@end
// 打印结果
kvcValue
- KVC 本质上是对 NSObject、NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet 等对象,实现
NSKeyValueCoding
分类,赋予它们Key-Value Coding
的能力;详情参考 KVC文档
1.1、赋值流程
- 首先会去找类的 set方法,如果找不到会去找 带下划线的set方法
@implementation LZPerson - (void)setName:(NSString *)name { self->name = @"setValue"; } - (void)_setName:(NSString *)name { self->name = @"_setValue"; } @end
- 如果都找不到,则会看
+(BOOL)accessInstanceVariablesDirectly
方法中的返回(默认为YES)// 按照 _key、_isKey、key、isKey 的顺序找属性赋值 @interface LZPerson : NSObject { @public NSString *_name; NSString *_isName; NSString *name; NSString *isName; } @end
- 返回YES时,会按照 _key、_isKey、key、isKey 的顺序找属性赋值,如果 类中没有上面的这些属性 则会调用
-(void)setValue:(id)value forUndefinedKey:(NSString *)key
方法(自己实现一下,否则报错) - 返回NO时,会直接调用 -(void)setValue:(id)value forUndefinedKey 方法
@implementation LZPerson + (BOOL)accessInstanceVariablesDirectly { return YES; } // 简单实现一下防止崩溃 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSLog(@"%s",__func__); } @end
- 返回YES时,会按照 _key、_isKey、key、isKey 的顺序找属性赋值,如果 类中没有上面的这些属性 则会调用
1.2、取值流程
- 首先取值会按 getKey、key、isKey、_key 的顺序取
- (id)getName { return @"getGetNameValue"; } - (id)name { return @"getNameValue"; } - (id)isName { return @"getIsNameValue"; } - (id)_name { return @"get_NameValue"; } + (BOOL)accessInstanceVariablesDirectly { return YES; } - (id)valueForUndefinedKey:(NSString *)key { return @"valueForUndefineKey"; }
- 找不到也会根据 +(BOOL)accessInstanceVariablesDirectly 返回值
- 返回YES时,会按照 _key、_isKey、key、isKey 的顺序找属性取值,如果 类中没有这些属性 则会调用
-(id)valueForUndefinedKey:(NSString *)key
方法(自己实现一下,否则报错) - 返回NO时,直接调用 -(id)valueForUndefinedKey
- 返回YES时,会按照 _key、_isKey、key、isKey 的顺序找属性取值,如果 类中没有这些属性 则会调用
1.3、API
// 通过 Key 读取和存储
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过 keyPath 读取和存储
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 默认返回YES,若没有找到Set<Key>方法,按照_key、_iskey、key、iskey顺序搜索成员
+ (BOOL)accessInstanceVariablesDirectly;
// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和上一个方法一样,但这个方法是设值
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法时给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组Key,返回该组Key对应的Value,再转成字典返回,用于将Model转到字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
1.4、自定义KVC
相关方法
#import "NSObject+LZKVC.h"
#import <objc/runtime.h>
@implementation NSObject (LZKVC)
- (BOOL)lz_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (NSMutableArray *)getIvarListName{
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
KVC存储
// 自定义KVC-存储
- (void)lz_setValue:(nullable id)value forKey:(NSString *)key{
// 1: 判断key
if (key == nil || key.length == 0) {
return;
}
// 2: setter:set<Key>:→_set<Key>→setIs<Key>
// key要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self lz_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lz_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lz_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断能否直接赋值实例变量,如果accessInstanceVariablesDirectly返回NO,奔溃
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 间接变量
// 获取 ivar -> 遍历 containsObjct
// 4.1: 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key>→_is<Key>→<key>→is<Key>
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
// 4.2: 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3: 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5: 找不到,奔溃
@throw [NSException exceptionWithName:@"LZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
KVC读取
- (nullable id)lz_valueForKey:(NSString *)key{
// 1: 判断key
if (key == nil || key.length == 0) {
return nil;
}
// 2: 找到相关方法get<Key>→<key> countOf<Key> objectIn<Key>AtIndex
// key要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}
else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}
else if ([self respondsToSelector:NSSelectorFromString(isKey)]){
return [self performSelector:NSSelectorFromString(isKey)];
}
else if ([self respondsToSelector:NSSelectorFromString(_key)]){
return [self performSelector:NSSelectorFromString(_key)];
}
else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3: 判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 找相关实例变量进行赋值
// 4.1: 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key>→_is<Key>→<key>→is<Key>
// _name→_isName→name→isName
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
2、KVO
- KVO 全称
Key-Value Observing
(键值观察),是一种机制,允许对象在 其他对象的指定属性发生更改时 收到通知
2.1、KVO 和 NSNotificatioCenter 的差异
- KVO 只能用于监听 对象属性的变化,NSNotificatioCenter 可以监听任何你感兴趣的东西
- KVO 发出消息由 系统控制,NSNotificatioCenter 由 开发者控制
- KVO 自动记录新旧值变化,NSNotificatioCenter 只能记录开发者传递的参数
2.2、监听过程
-
注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- 消息中的 上下文指针context 包含任意数据,这些数据将在相应的更改通知中传回给观察者;可以指定
NULL
并完全依赖keyPath
字符串来确定更改通知的来源,但这样可能会导致 父类由于不同原因也在观察相同键路径的对象时 出现问题
- 消息中的 上下文指针context 包含任意数据,这些数据将在相应的更改通知中传回给观察者;可以指定
-
属性变化通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([keyPath isEqualToString:@"name"]) { NSLog(@"%@",change); } }
-
移除观察者
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
- 如果被观察者是单例,那么如果被观察者所在界面销毁时不移除观察者会崩溃(被观察者未释放,值改变方法还要调用,但界面被释放,这个方法找不到了所以崩溃)
-
设置 context上下文,区分通知来源
static void *PersonNickContext = &PersonNickContext; static void *PersonNameContext = &PersonNameContext; [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext]; [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext]; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if (context == PersonNickContext) { NSLog(@"nick:%@",change); return; } if (context == PersonNameContext){ NSLog(@"name:%@",change); return; } }
2.3、手动关闭KVO、手动触发KVO
+(BOOL)automaticallyNotifiesObserversForKey
手动关闭KVO+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { return NO; }
willChangeValueForKey
、didChangeValueForKey
手动触发KVO[LZPerson willChangeValueForKey:@"name"]; _name = name; [LZPerson didChangeValueForKey:@"name"];
2.4、监听可变数组
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 这种写法不能收到KVO通知,因为KVO基于KVC,访问 集合对象 有三种不同的代理方法
// if(self.person.dateArray.count == 0){
// [self.person.dateArray addObject:@"1"];
// }
// else{
// [self.person.dateArray removeObjectAtIndex:0];
// }
if(self.person.dateArray.count == 0){
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
else{
[[self.person mutableArrayValueForKey:@"dateArray"] removeObjectAtIndex:0];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
- 对
集合对象
访问定义的三种不同的代理方法mutableArrayValueForKey:
和 mutableArrayValueForKeyPath:mutableSetValueForKey:
和 mutableSetValueForKeyPath:mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
- 会打印 NSKeyValueChange 类型的 kind,表示键值变化的类型,执行
addObject
时,kind 打印值为 2;执行removeObjectAtIndex
时,kind 打印值为 3/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information. */ typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, //赋值 NSKeyValueChangeInsertion = 2, //插入 NSKeyValueChangeRemoval = 3, //移除 NSKeyValueChangeReplacement = 4, //替换 };
2.5、技术细节
- 自动键值观察是使用称为
isa-swizzling
的技术实现 - 该 isa 指针指向对象的类,它保持一个调度表,该调度表主要包含指向类实现的方法的指针,以及其他数据
- 当观察者 注册观察对象的某属性 时,被观察对象的 isa 指针被修改,
指向中间类而不是真正的类
;因此,isa 指针的值不一定反映实例的实际类 - 永远
不要依赖 isa 指针来确定类成员身份
,应该使用该 class 方法来确定对象实例的类
2.6、底层原理
isa改变
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LZPerson alloc] init];
NSLog(@"添加KVO观察者之前:%s", object_getClassName(self.person));
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"添加KVO观察者之后:%s", object_getClassName(self.person));
}
// 打印结果
添加KVO观察者之前:LZPerson
添加KVO观察者之后:NSKVONotifying_LZPerson
- 当调用
addObserve
方法时,系统动态
生成当前类的子类NSKVONotifying_类名
(当前类的子类) - 对象会将 isa指针指向这个子类,这个子类会生成对应的 set方法(setName)、构造方法、dealloc、_isKVOA(标记是否为KVO生成的中间类)
- set方法中会调用
willChangeValueForKey
和didChangeValueForKey
两个方法 - 当注册被移除时,对象将isa指针指回正常
-
NSKVONotifying_类名
中的方法- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LZPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; unsigned int intCount; Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LZPerson"), &intCount); for (unsigned int intIndex=0; intIndex<intCount; intIndex++) { Method method = methodList[intIndex]; NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method)); } } // 打印结果 SEL:setNickName:,IMP:0x18a5d8520 SEL:class,IMP:0x18a5d6fd4 SEL:dealloc,IMP:0x18a5d6d58 SEL:_isKVOA,IMP:0x18a5d6d50
-
因为类的属性有set方法,而 成员变量没有set方法,因此
KVO不能监听成员变量
;如果一定要监听成员变量,需要 使用KVC触发[self.person addObserver:self forKeyPath:@"_sex" options:(NSKeyValueObservingOptionNew) context:NULL]; //直接赋值无法触发KVO,要用KVC //self.person->_sex = @"male"; [self.person setValue:@"male" forKey:@"_sex"];
-
可以参考
FBKVOController