iOS-KVC+KVO
一、KVC
1.1 什么是KVC
- KVC指的是键值编码;通过key来直接访问对象的属性,然后由NSKeyValueCoding非正式协议启用的机制。
- KVC本质上是对NSObject、NSArray、NSMutableDictionary、NSOrderedSet、NSSet等对象;实现NSKeyValueCoding的分类,赋予他们键值编码的能力。
1.2 KVC的底层流程
主要分为set赋值和get取值两个部分。
1.2.1 set赋值流程
- 首先找类的set方法实现,如果找不到就会找_set方法
@implementation LGPerson - (void)setName:(NSString *)name { self->name = @"setValue"; } - (void)_setName:(NSString *)name { self->name = @"_setValue"; } @end
- 如果找不到,就会看+(BOOL)accessInstanceVariablesDirectly(如果返回yes);默认是yes
@interface LGPerson : NSObject { @public NSString *_name; NSString *_isName; NSString *name; NSString *isName; } @end
- 按照_<key>、_is<Key>、<key>、is<Key>的顺序查找成员变量赋值
- 如果类中没有这些成员变量就会调用
-(void)setValue:(id)value forUndefinedKey:(NSString *)key
方法;抛出NSUnknownKeyException异常(实现了setValue也不会抛弃异常)
- 如果+(BOOL)accessInstanceVariablesDirectly返回No,就会直接调用-(void)setValue:(id)value forUndefinedKey:(NSString *)key;抛出NSUnknownKeyException异常
@implementation LGPerson + (BOOL)accessInstanceVariablesDirectly { return YES; } // 简单实现一下防止崩溃 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSLog(@"%s",__func__); } @end
1.2.2 流程图(参考自https://juejin.cn/post/7115413383813267464)
1.3 get取值流程
-
按照get<Key>、<key>、is<Key>、_<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);默认是yes
- 如果返回yes,会按照按照_<key>、_is<Key>、<key>、is<Key> 的顺序查找成员变量,找到则取值找不到则调用valueForUndefinedKey:并抛出NSUnknownKeyException异常;(实现了也不会抛出异常)
- 如果返回No,直接调用 -(id)valueForUndefinedKey
1.3.2 流程图(参考自https://juejin.cn/post/7115413383813267464)
1.4 常用的一些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.5 自定义KVC
1.5.1 API实现
#import "NSObject+LGKVC.h" #import <objc/runtime.h> @implementation NSObject (LGKVC) - (BOOL)lg_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; }
1.5.2 KVC存储(set)
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{ if (key == nil || key.length == 0) {return;} NSString *Key = key.capitalizedString; NSString *setKey = [NSString stringWithFormat:@"set%@:",Key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([self respondsToSelector:NSSelectorFromString(setKey)]) { [self performSelector:NSSelectorFromString(setKey) withObject:value]; return; }else if ([self respondsToSelector:NSSelectorFromString(_setKey)]) { [self performSelector:NSSelectorFromString(_setKey) withObject:value]; return; } #pragma clang diagnostic pop if (![self.class accessInstanceVariablesDirectly]) { @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; } NSString *_key = [NSString stringWithFormat:@"_%@",key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key]; NSString *isKey = [NSString stringWithFormat:@"is%@",Key]; NSMutableArray *mArray = [self getIvarListName]; 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; }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; } @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil]; }
1.5.3 KVC读取(get)
- (nullable id)lg_valueForKey:(NSString *)key{ if (key == nil || key.length == 0) { return nil; } NSString *Key = key.capitalizedString; NSString *getKey = [NSString stringWithFormat:@"get%@",Key]; NSString *_key = [NSString stringWithFormat:@"_%@",key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key]; NSString *isKey = [NSString stringWithFormat:@"is%@",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)]; } #pragma clang diagnostic pop if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; } NSMutableArray *mArray = [self getIvarListName]; 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 @""; }
二、KVO
2.1 什么是KVO
KVO是键值观察,是一种允许对象在其他对象的指定属性发生更改时收到通知的机制。
2.2 KVO和NSNotification的差异
1.KVO 只能用于监听 对象属性的变化,NSNotificatioCenter 可以监听任何你感兴趣的东西 2.KVO 发出消息由 系统控制,NSNotificatioCenter 由 开发者控制 3.KVO 自动记录新旧值变化,NSNotificatioCenter 只能记录开发者传递的参数
2.3 监听过程(注册观察者;属性变化通知;移除观察者)
2.3.1 注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- 消息中的 上下文指针context 包含任意数据,这些数据将在相应的更改通知中传回给观察者;可以指定
NULL
并完全依赖keyPath
字符串来确定更改通知的来源,但这样可能会导致 父类由于不同原因也在观察相同键路径的对象时 出现问题
2.3.2 属性变化通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([keyPath isEqualToString:@"name"]) { NSLog(@"%@",change); } }
2.3.3 移除观察者
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
- 如果被观察者是单例模式创建的,那么如果被观察者所在界面销毁时不移除观察者会崩溃(因为被观察者在全局区,不会随界面的销毁而释放,值改变方法还要被调用,但界面被释放导致这个方法找不到了,所以崩溃)
2.3.4 设置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.4 手动关闭、触发KVO的API
2.4.1 手动关闭的API
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { return NO; }
2.4.3 手动触发的API (willChangeValueForKey;didChangeValueForKey)
[LGPerson willChangeValueForKey:@"name"]; _name = name; [LGPerson didChangeValueForKey:@"name"];
2.5 监听可变数组(与属性的监听是不同的API)
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.6 注意点
- KVO是通过isa-swizzling的技术实现的
- 该isa指针指向对象的类,它保持一个调度表,该调度表主要包含指向类实现的方法的指针,以及其他数据
- 当观察者注册观察者的某属性时,被观察对象的isa指针被修改,指向中间类而不是真正的类;因此,isa指针的值不一定反映实例的实际类
- 在KVC模式下可通过getclass的方式来确定对象的类,因为isa指针的指向有可能被修改。
2.7 KVO底层原理
2.7.1 isa指针指向的改变
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson 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观察者之前:LGPerson 添加KVO观察者之后:NSKVONotifying_LGPerson
- 当调用
addObserve
方法时,系统动态
生成当前类的子类NSKVONotifying_类名
(当前类的子类) - 对象会将 isa指针指向这个子类,这个子类会生成对应的 set方法(setName)、构造方法、dealloc、_isKVOA(标记是否为KVO生成的中间类)
- 子类生成的set方法中会调用
willChangeValueForKey
和didChangeValueForKey
两个方法 - 当注册被移除时,对象将isa指针指回正常
2.7.2 NSKVONotifying_类名中的方法
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson 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
2.7.3 KVO对成员变量的监听
因为类的属性有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"];