一、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
@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
@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 *)touches withEvent:(UIEvent *)event {
self.person1.age = 18;
self.person2.age = 33;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"%@的%@属性改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 打印结果:
的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 指向没有改变
ps;iOS开发交流技术群:欢迎你的加入,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长
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 *)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
的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 方法。
ps;iOS开发交流技术群:欢迎你的加入,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长
重写 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