一、KVC
在开发中,我们可以通过使用 KVC
的方式来对某个对象的属性进行赋值/取值操作。
经常会用到以下 API
:
// 设置值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 获取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForKey:(NSString *)key;
复制代码
1.1 赋值操作
接下来我们就研究一下 KVC
的调用原理:
如果我们给某个类定义一个属性,那么编译器会自动生成 getter
和 setter
方法,如果通过 KVC
给该属性进行赋值操作,默认会调用 setter
方法进行赋值。但是这不能完全搞清楚 KVC
是如何工作的。
我们定义一个 Person
类,但是我们并不给 Person
定义任何的属性。接下来创建 person
对象,通过 KVC
的方式给 person
的 age
属性进行赋值操作。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
[person setValue:@(20) forKey:@"age"];
}
return 0;
}
复制代码
- 去
Person
类中查找有没有- (void)setAge:
方法,如果有那么就进行赋值操作;如果没有再去查找有没有- (void)_setAge:
方法,如果有就进行赋值的操作。 - 如果以上两个方法都没找到,那么就会调用
- (Bool)accessInstanceVariablesDirectly
方法,该方法是询问是否可以直接访问成员变量,返回NO
就直接抛出异常未定义的Key
- 如果
- (Bool)accessInstanceVariablesDirectly
返回的是YES
(如果不实现该方法默认返回的就是YES
),那么就直接去成员变量中按顺序查找以下成员变量:_age
、_isAge
、age
、isAge
。如果找到4个成员变量中的1位,那么就进行赋值,否则抛出异常未定义的Key
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject {
@public
int _age; // 最先查找
int _isAge; // 老2
int age; // 老3
int isAge; // 老小
// 如果以上4个成员变量都没有,抛异常
}
@end
// Person.m
#import "Person.h"
@implementation Person
// 如果有最先调用
- (void)setAge:(int)age {
NSLog(@"setAge - %d", age);
}
// 如果没有 setAge 方法,调用该方法
- (void)_setAge:(int)age {
NSLog(@"_setAge - %d", age);
}
// 如果以上两个方法都没有,且该方法返回 YES,就去查找 成员变量
// 如果以上两个方法都没有,且该方法返回 NO,直接抛异常
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
复制代码
1.2 取值操作
KVC
的取值操作也会按照一定的顺序进行操作的。
- 在
Person
的实现文件中,按照-(int)getAge
、- (int)age
、- (int)isAge
、-(int)_age
顺序进行,看有没有实现这4个方法中的其中1个,如果有那么调用 - 如果没有实现上面的4个方法,继续查看
+ (BOOL)accessInstanceVariablesDirectly
方法的返回值是否为YES
- 如果
+ (BOOL)accessInstanceVariablesDirectly
方法返回值为NO
,直接抛出异常,如果为YES
,那么就去按顺序查找Person
的成员变量是不是_age
、_isAge
、age
、isAge
中的一个,如果有4个成员变量中的1个,那么就取他们的值。
// Person.m
#import "Person.h"
@implementation Person
- (int)getAge {
return 11;
}
- (int)age {
return 12;
}
- (int)isAge {
return 13;
}
- (int)_age {
return 14;
}
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person->age = 11;
person->_age = 12;
person->isAge = 13;
person->_isAge = 14;
NSLog(@"%@", [person valueForKey:@"age"]);
}
return 0;
}
复制代码
二、KVO
KVO
即键值观察,可以用来监听一个对象的属性的变化,当该对象的属性的值发生改变的时候,会回调 - (void)observeValueForKeyPath:ofObject:change:context:
方法,在该方法中可以处理一些业务逻辑。
2.1 KVO 的基本使用
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
// ViewController.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 11;
self.person2 = [[Person alloc] init];
self.person2.age= 22;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"man"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 18;
self.person2.age = 33;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@的%@属性改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 打印结果:
<Person: 0x6000019a8270>的age属性改变了 - {
kind = 1;
new = 18;
old = 11;
} - man
复制代码
- 创建一个
Person
类,定义一个age
属性,定义属性后,编译器会自动生成getter
和setter
方法以及带下划线的成员变量 - 创建两个
person
对象,分别给age
属性赋值(本质是调用setAge:
方法)同时给person1
添加观察者self
(即该控制器对象) - 监听
age
属性,监听它的新值和旧值 - 实现
- (void)observeValueForKeyPath:ofObject:change:context:
方法,当age
发生改变后会回调到该方法。 - 在控制器对象销毁时候,将 person1 的观察者移除
以上就是 KVO
的基本使用。接下来我们就研究一下 KVO
的本质
2.2 KVO 的本质
上面的代码,我们改变 age
的值,本质是调用 setter
方法进行 age
的值修改,我们可能会认为程序在运行时 setter
方法做了手脚来实现监听,其实不是的,问题出在 person
对象上。
我们可以通过在为 person1
添加观察者之后来打印一下 person1
和 person2
的 isa
指向来获取他们的类对象
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 11;
self.person2 = [[Person alloc] init];
self.person2.age= 22;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"man"];
// 获取类对象
NSLog(@"%@", object_getClass(self.person1));
NSLog(@"%@", object_getClass(self.person2));
}
// 打印结果:
NSKVONotifying_Person
Person
复制代码
person1
对象的isa
指向发生了变化,指向了NSKVONotifying_Person
,NSKVONotifying_Person
就是person1
的类对象person2
进行KVO
监听,所以person2
的isa
指向没有改变
NSKVONotifying_Person
是在程序运行时为我们动态添加的类,而该类是继承 Person
的,即它的 superclass
指针指向了 Person
,调用下面的代码可以验证该结论。
NSLog(@"%@", [object_getClass(self.person1) superclass]);
// 打印结果:Person
复制代码
KVO 又是怎么对 person1 的 age 属性进行监听的呢?
person1
通过isa
找到它的类对象即NSKVONotifying_Person
,在NSKVONotifying_Person
内部也存储着一个setAge:
方法,该方法内部调用了_NSSetIntValueAndNotify
函数_NSSetIntValueAndNotify
函数内部首先是调用了- (void)willChangeValueForKey:
方法,然后通过[super setAge:]
方法去调用父类真正的赋值操作,最后调用- (void)didChangeValueForKey:
方法- 在
- (void)didChangeValueForKey:
内部调用- (void)observeValueForKeyPath:ofObject:change:context:
方法最终完成属性值的监听操作。
怎么证明是调用了 _NSSetIntValueAndNotify
方法呢?
我们可以利用 lldb
命令来查看一下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 18;
self.person2.age = 33;
// 打印方法的地址
NSLog(@"%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"%p", [self.person2 methodForSelector:@selector(setAge:)]);
}
// 打印结果:
0x7fff207bc2b7
0x108a0ef30
lldb:
p (IMP)0x7fff207bc2b7 => $0 = 0x00007fff207bc2b7 (Foundation`_NSSetIntValueAndNotify)
p (IMP)0x108a0ef30 => $2 = 0x0000000108a0ef30 (KVODemo`-[Person setAge:] at Person.h:13)
复制代码
我们可以通过一些打印来观察一下具体是什么时候进行监听的:
// Person.m
#import "Person.h"
@implementation Person
- (void)setAge:(int)age {
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey:");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey: => begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: => end");
}
@end
// 打印结果:
willChangeValueForKey:
setAge:
didChangeValueForKey: => begin
<Person: 0x600000aac460>的age属性改变了 - {
kind = 1;
new = 18;
old = 11;
} - man
didChangeValueForKey: => end
复制代码
- 重写
Person.m
文件中的setAge:
方法、willChangeValueForKey:
方法以及didChangeValueForKey:
方法 - 通过打印结果可以观察打印顺序,先调用
willChangeValueForKey:
再调用setAge:
方法去修改值,最后再didChangeForKey:
方法中来监听属性的改变
前面已经得出结论,person1
的类对象已经变成了 NSKVONotifying_Person
类,而且 NSKVONotifying_Person
中还重写了 setAge
方法,其实内部不仅仅有 setAge
方法,还有三个方法,分别为 class
,dealloc
方法和 _isKVOA
方法。
- 重写
class
方法的目的是当我们调用[person1 class]
方法时,返回的是Person
类,从而防止NSKVONotifying_Person
类暴露出来,因为苹果本身是不希望我们去过多关注NSKVONotifying_Person
类的。 dealloc
方法在NSKVONotifying_Person
类使用完毕后进行一些收尾的工作,因为是不开源的所以这里也只是一个猜测_isKVOA
方法目的是返回布尔类型告诉系统是否和KVO
有关。
我们可以利用 runtime 来查看一个类对象中的方法名称:
/// 传入类/元类对象,返回其中的方法名称
- (NSString *)printMethodNameOfClass:(Class)cls {
unsigned int count;
// 获取类中的所有方法
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
for (int i = 0; i < count; i++) {
// 获取方法
Method method = methodList[i];
// 获取方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendFormat:@"%@ ", methodName];
}
return methodNames;
}
NSLog(@"%@ - %@", object_getClass(self.person1), [self printMethodNameOfClass:object_getClass(self.person1)]);
NSLog(@"%@ - %@", object_getClass(self.person2), [self printMethodNameOfClass:object_getClass(self.person2)]);
// 打印结果:
NSKVONotifying_Person - setAge: class dealloc _isKVOA
Person - setAge: age
复制代码