KVC和KVO在实际的运用中是很常见的。所以了解它的底层实现原理是非常不错的一件事。
KVC(NSKeyValueCoding)
KVC就是通过key值,来获取对象的属性进行操作,而不是通过我们明确的存取方式来获取,是一个非正式的Protocol。KVO就是基于KVC来实现的。
KVC的一般使用:
@interface Person : NSObject
{
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
- (void)testName;
@end
@implementation Person
- (NSString *)getName {
NSLog(@"%s",__func__);
return @"D";
}
- (NSString *)name {
NSLog(@"%s",__func__);
return @"D";
}
- (NSString *)isName {
NSLog(@"%s",__func__);
return @"D";
}
- (void)setName:(NSString *)name {
NSLog(@"%s",__func__);
}
//- (NSInteger)countOfName {
// return 2;
//}
//
//- (id)objectInNameAtIndex:(NSInteger)index {
// return @"arrayItem";
//}
- (void)testName {
NSLog(@"_name = %@",_name);
NSLog(@"name = %@",name);
NSLog(@"isName = %@",isName);
NSLog(@"_isName = %@",_isName);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"取值没有找到这个key %@",key);
return nil;
}
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key {
NSLog(@"设值没有找到这个key %@",key);
}
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person valueForKey:@"name"];
[person setValue:@"ADA" forKey:@"name"];
[person testName];
}
运行后,set和get方法都会被执行,但是这与点语法还是有区别的。
KVC有自己的执行机制
在调用 setValue: forKey: 的时,程序优先调用 setName: 方法,如果没有找到 setName: 方法 KVC会检查这个类的 + (BOOL)accessInstanceVariablesDirectly 类方法看是否返回YES(默认YES),返回YES则会继续查找该类有没有名为_name的成员变量,如果还是没有找到则会继续查找_isName成员变量,还是没有则依次查找name,isName。上述的成员变量都没找到则执行setValue:forUndefinedKey: 抛出异常,如果不想程序崩溃应该重写该方法。假如这个类重写了+ (BOOL)accessInstanceVariablesDirectly 返回的是NO,则程序没有找到setName:方法之后,会直接执行setValue:forUndefinedKey: 抛出异常。
在调用valueForKey:的时,会依次按照getName,name,isName的顺序进行调用。如果这3个方法没有找到,那么KVC会按照countOfName,objectInNameAtIndex来查找。如果查找到这两个方法就会返回一个数组。如果还没找到则调用+ (BOOL)accessInstanceVariablesDirectly 看是否返回YES,返回YES则依次按照_name,_isName,name,isName顺序查找成员变量名,还是没找到就调用valueForUndefinedKey:;返回NO直接调用valueForUndefinedKey:
KVC的一些注意
KVC在设置时可能会设置错误的Key值导致程序崩溃,需要重写valueForUndefinedKey:和setValue:forUndefinedKey:。还有一种是在设置中不小心传递了nil,这时候需要重写setNilValueForKey:。
可能还有一些内容我没有提到,读者可自行注释上面所展示的代码来验证查找的顺序,会比较好理解。
KVO(Key-Value Observing)
KVO是OC设计模式中的一种,简单的说就是添加一个被观察对象A的属性,当被观察对象A的属性发生更改时,观察对象会获得通知,并作出相应的处理。NSObject类都实现了KVO ,解决了观察对象和被观察对象的解耦。
KVO的一般使用
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@interface ViewController ()
{
Person *person;
}
@end
@implementation Person
//+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
// return NO;
//}
@end
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
person = [[Person alloc] init];
// 第三个参数代表新值
[person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (IBAction)change:(id)sender {
// [person willChangeValueForKey:@"name"];
person.name = @"ADA";
// [person didChangeValueForKey:@"name"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath = %@",keyPath);
NSLog(@"object = %@",object);
NSLog(@"change = %@",change);
}
- (void)dealloc {
[person removeObserver:self forKeyPath:@"name" context:nil];
}
这个是常见的KVO。
其实这个是自动实现的KVO还有手动实现的KVO
将上诉注释掉的代码打开即可实现。
KVO的底层是通过isa-swizzling实现的。官方文档中第一段有提到
- Automatic key-value observing is implemented using a technique called isa-swizzling
那么这个isa-swizzling是什么呢?
大家可能对Method-Swizzling会比较熟悉,它的实现其实是一个替换函数实现指针的过程。
具体实现的代码:
+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector isClassMethod:(BOOL)isClassMethod {
Class class = [self class];
Method originalMethod;
Method swizzledMethod;
if (isClassMethod) {
originalMethod = class_getClassMethod(class, originalSelector);
swizzledMethod = class_getClassMethod(class, swizzledSelector);
}else {
originalMethod = class_getInstanceMethod(class, originalSelector);
swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
}
if (!originalMethod) {
NSLog(@"original is nil (%@)",originalMethod);
}
method_exchangeImplementations(originalMethod, swizzledMethod);
}
那么isa-swizzling顾名思义就是替换isa的过程。
那isa又是什么呢?
oc是面向对象的语言,每一个对象都是一个类的实例。
每个对象都有一个名为isa的指针,指向该对象的类。每个类中又描述了它的实例的特点,比如成员变量列表,成员函数列表。每一个对象都可以接收消息,而对象能够接收的消息列表都保存在它所对应的类中。NSObject就是一个包含isa指针的结构体
从Class的定义中,我们也可以看出Class也是一个包含isa指针的结构体。每一个类实际上也是一个对象,每一个类也有一个名为isa的指针。
既然每一个类也是一个对象,那它必然是另一个类的实例。这个类就是元类(meta)。元类也是一个对象。元类的isa指针都指向一个根元类.根元类本身的isa指针指向自己。
这一块可能有点绕。
个人理解的isa就是一个Class 类型的指针. 每个实例对象都有一个isa的指针,指向该对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。同样的,元类也是类,它也是对象。元类也有isa指针,最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
isa-swizzling就是在运行时动态地修改 isa 指针的值,达到替换对象整个行为的目的。
既然是替换了类,那么在添加了KVO之后这个类究竟做了什么改变。
我们可以通过object_getClass()来打印出isa指针。
NSLog(@"%@",object_getClass(person));
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"%@",object_getClass(person));
运行后可以在控制台看到:
- 2017-04-19 16:49:55.277 KVODemo[1759:1701820] Person
- 2017-04-19 16:49:55.278 KVODemo[1759:1701820] NSKVONotifying_Person
也就是说pesron对象的isa指针已经指向了NSKVONotifying_Person类了。
那这个NSKVONotifying_Person类究竟是什么呢?
在网上查阅后发现,这个NSKVONotifying_Person是Person的一个子类。
我们可以通过class_getSuperclass来验证。
@implementation Person
- (void)print{
NSLog(@"isa:%@, supper class:%@", NSStringFromClass(object_getClass(self)), class_getSuperclass(object_getClass(self)));
}
@end
然后再添加KVO之前和之后分别调用这个方法,可以在控制台看到:
- 2017-04-19 17:43:33.311 KVODemo[1899:1927921] isa:Person, supper class:NSObject
- 2017-04-19 17:43:33.312 KVODemo[1899:1927921] isa:NSKVONotifying_Person, supper class:Person
所以可以知道NSKVONotifying_Person是Person的子类。
然后还有一点就是系统是自动实现监听类的属性,那么set方法就有可能被重写了,因为消息机制是通过isa查找的,如果子类中没有对应的方法,就会在父类中查找,但是我们在Person中并没有写willChangeValueForKey:和didChangeValueForKey:这两个方法。所以肯定也是在子类中实现的。
在print方法里 在加入一条打印
NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
运行后控制台显示:
- name setter function pointer:0x10c265740
- name setter function pointer:0x10c368c60
证明set方法确实是被重写了。
到这里基本可以确定KVO的实现是:
添加观察后:
系统实现了一个子类,然后将被观察的类对象的isa指针指向这个子类。再重写了setter方法。并在当中添加了willChangeValueForKey:和didChangeValueForKey:。
移除观察就是将isa指针指向原来的类对象中。
那么isa-swizzling做的处理应该是这样的:
大概就这样子,如果有什么不对的地方,欢迎大家提出来。共同进步。
觉得对你有帮助给个喜欢。