KVO与KVC
- 1. KVO
- KVO底层实现分析
- 如何验证上面的说法:
- NSKVONotifyin_Person内部结构
- didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法
- 回答问题:
- 2. KVC
- 简介:
- key和keyPath的区别
- key:
- keyPath:
- 批量重复操作
- 字典模型相互转化
- KVC原理
- 赋值原理
- 取值原理
1. KVO
首先需要了解KVO基本使用,KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.Person *p1 = [[Person alloc] init];Person *p2 = [[Person alloc] init];p1.age = 1;p1.age = 2;p2.age = 2;[p1 addObserver:self forKeyPath:@"age"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];p1.age = 10;[p1 removeObserver:self forKeyPath:@"age"];}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);NSLog(@"%@", change[@"old"]);NSLog(@"%@", change[@"new"]);}
@end
KVO底层实现分析
KVO 的底层实现利用了 Objective-C 的 Runtime 特性,动态创建子类并重写属性的 setter 方法,以实现属性变化的观察和通知机制。
通过上面我们可以发现,再对象p1对象执行addObserver操作之后,p1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifyin_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。
上图是p1没有执行addObserver操作之前isa指针的实际指向。但是,在p1添加addObserver操作之后,p1对象的isa指针就如上面所示指向为NSKVONotifying_Person
,NSKVONotifying_Person是Person的子类,也就是说其superclass指针是指向Person类对象的,NSKVONotifyin_Person是runtime在运行时生成的。那么p1对象在调用setage方法的时候,肯定会根据p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及实现。
如何验证上面的说法:
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);[p1 addObserver:self forKeyPath:@"age"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
我们可以发现:**添加KVO监听之前,p1和p2的setAge方法实现的地址相同,而经过KVO监听之后,p1的setAge方法实现的地址发生了变化,**我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1的setAge方法的实现由Person类方法中的setAge方法转换为了C语言的Foundation框架的_NSsetIntValueAndNotify函数。
NSKVONotifyin_Person内部结构
首先我们清楚NSKVONotifyin_Person
,作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部一定对setAge方法做了单独的实现,
- (void)viewDidLoad {[super viewDidLoad];Person *p1 = [[Person alloc] init];p1.age = 1.0;Person *p2 = [[Person alloc] init];p1.age = 2.0;// self 监听 p1的 age属性NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[p1 addObserver:self forKeyPath:@"age" options:options context:nil];[self printMethods: object_getClass(p2)];[self printMethods: object_getClass(p1)];[p1 removeObserver:self forKeyPath:@"age"];
}- (void) printMethods:(Class)cls
{unsigned int count ;Method *methods = class_copyMethodList(cls, &count);NSMutableString *methodNames = [NSMutableString string];[methodNames appendFormat:@"%@ - ", cls];for (int i = 0 ; i < count; i++) {Method method = methods[i];NSString *methodName = NSStringFromSelector(method_getName(method));[methodNames appendString: methodName];[methodNames appendString:@" "];}NSLog(@"%@",methodNames);free(methods);
}
运行结果如下:
通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。
**这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。**不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。
如果NSKVONotifyin_Person
不重写class
方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject
,而NSObject
的class的实现大致为返回自己isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person
,apple不希望将NSKVONotifyin_Person
类暴露出来,并且不希望我们知道NSKVONotifyin_Person
内部实现,所以在内部重写了class类,直接返回Person类。
didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法
在Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟他们的实现。
- (void)setAge:(int)age {NSLog(@"setage:");_age = age;}- (void)willChangeValueForKey:(NSString *)key {NSLog(@"willChangeValueForKey: - begin");[super willChangeValueForKey:key];NSLog(@"willChangeValueForKey: - end");
}- (void)didChangeValueForKey:(NSString *)key
{NSLog(@"didChangeValueForKey: - begin");[super didChangeValueForKey:key];NSLog(@"didChangeValueForKey: - end");
}
运行结果:
回答问题:
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 答. 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
- 如何手动触发KVO? 答. 被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。
2. KVC
简介:
KVC的全称是KeyValueCoding
,俗称“键值编码
”,可以通过一个key来访问某个属性;
KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量;
它是一个非正式的Protocol,提供一种机制来间接访问对象的属性,而不是通过调用Setter、Getter方法访问。KVO 就是基于 KVC 实现的关键技术之一。
常见的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:
Person* person = [[Person alloc] init];
[person setValue:@"I am Father" forKey:@"name"];
NSLog(@"%@", [person valueForKey:@"name"]);
输出结果:
keyPath:
person.son = [[PersonSon alloc] init];
[person setValue:@"I am Son" forKeyPath:@"son.sonName"];
NSLog(@"%@", [person.son valueForKey:@"sonName"]);
输出结果:
批量重复操作
Person* personFirst = [[Person alloc] init];[personFirst setValue:@"lcy" forKey:@"name"];[personFirst setValue:@"20" forKey:@"age"];[personFirst setValue:@"男" forKey:@"sex"];NSLog(@"name = %@, age = %ld, sex = %@",personFirst.name, (long)personFirst.age, personFirst.sex);NSDictionary* dictionary1 = [personFirst dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];NSLog(@"dictionary1 = %@", dictionary1);NSDictionary* dictioinary2 = @{@"name": @"lyt", @"age": @11, @"sex": @"男"};Person* personSecond = [[Person alloc] init];[personSecond setValuesForKeysWithDictionary:dictioinary2];NSLog(@"name = %@, age = %ld, sex = %@",personSecond.name, (long)personSecond.age, personSecond.sex);
输出结果:
字典模型相互转化
如果model属性和dic不匹配,可以重写方法-(void)setValue:(id)value forUndefinedKey:(NSString *)key
。
*重点:-(void)setValue:(id)value forUndefinedKey:(NSString )key 方法在函数中有定义,但是没有实现需要自己来实现,从而供后面来调用。如果自己不重写的话,遇到Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface StudentModel : NSObject@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSString* age;
@property (nonatomic, strong) NSString* studentSex;@endNS_ASSUME_NONNULL_END#import "StudentModel.h"@implementation StudentModel
- (void) setValue:(id)value forUndefinedKey:(NSString *)key {if ([key isEqualToString:@"sex"]) {self.studentSex = (NSString*) value;}
}
@end//main函数
NSDictionary* dictionary = @{@"name": @"stu1", @"age": @66, @"sex": @"nv"};
StudentModel* model = [[StudentModel alloc] init];
[model setValuesForKeysWithDictionary:dictionary];
NSLog(@"model.name: %@", model.name);
NSLog(@"model.age: %@", model.age);
NSLog(@"model.sex: %@", model.studentSex);NSDictionary* tempdict = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
NSLog(@"tempdict = %@", tempdict);
输出结果:
KVC原理
赋值原理
在日常开发中,针对对象属性的赋值,一般有以下两种方式:
- 直接通过setter方法赋值;
- 通过KVC键值编码的相关API赋值;
当调用setValue:forKey:设置属性value时,其底层的执行流程为:
- 【第一步】首先查找是否有这三种setter方法,按照查找顺序为
set<Key>:-> _set<Key> -> setIs<Key>
- 如果有其中任意一个setter方法,则直接设置属性的value(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)
- 如果都没有,则进入【第二步】
- 【第二步】:如果没有第一步中的三个简单的setter方法,则查找
accessInstanceVariablesDirectly
是否返回YES,- 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:
_<key> -> _is<Key> -> <key> -> is<Key>
。- 如果找到其中任意一个实例变量,则赋值。
- 如果都没有,则进入【第三步】。
- 如果返回NO,则进入【第三步】。
- 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:
- 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。
综上所述,KVC通过 setValue:forKey: 方法设值的流程以设置LGPerson的对象person的属性name为例,如下图所示:
取值原理
- kvc取值按照
getKey、key、iskey、_key
顺序查找方法。 - 存在直接调用。
- 没找到同样,先查看
accessInstanceVariablesDirectly
方法。
+ (BOOL)accessInstanceVariablesDirectly{return YES; ///> 可以直接访问成员变量// return NO; ///> 不可以直接访问成员变量, ///> 直接访问会报NSUnkonwKeyException错误 }
- 如果可以访问会按照
_key、_isKey、key、iskey
的顺序查找成员变量。 - 找到直接复制。
- 未找到报错
NSUnkonwKeyException
错误。