【iOS】OC底层系列三 - KVO和KVC

面试题:

  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质)
  2. KVC的赋值和取值过程是怎样的?原理是什么?

一、KVO

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

通过一个小案例,回顾一下KVO的使用方法。示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@interface DBPerson : NSObject

@property (nonatomic, assign) int age;

@end

@implementation DBPerson

- (void)setAge:(int)age {
NSLog(@"%@ - 旧值:%d", self, age);
_age = age;
NSLog(@"%@ - 新值:%d", self, _age);
}

@end

@interface ViewController ()

@property (nonatomic, strong) DBPerson *person1;
@property (nonatomic, strong) DBPerson *person2;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// 1. 创建对象
self.person1 = [[DBPerson alloc] init];
self.person2 = [[DBPerson alloc] init];

// 2. person1对象添加KVO监听
// observer:监听者
// keyPath:监听对象的属性
// options:监听配置(一般新值和旧值都要监听)
// context:附加值(如果不需要就传nil)
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"监听age值变化"];
}

// 3. 监听方法(当监听对象的属性值发生改变时,会自动调用)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
int oldValue = [[change valueForKey:NSKeyValueChangeOldKey] intValue];
int newValue = [[change valueForKey:NSKeyValueChangeNewKey] intValue];
NSLog(@"%@:%@的%@属性值从[%d]变为了[%d]", context, object, keyPath, oldValue, newValue);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 4. 改变属性值
self.person1.age = 11;
self.person2.age = 22;
}

- (void)dealloc {
// 5. 移除监听
[self.person1 removeObserver:self forKeyPath:@"age"];
}

@end

/*
输出:
<DBPerson: 0x600002040600> - 旧值:11
<DBPerson: 0x600002040600> - 新值:11
监听age值变化:<DBPerson: 0x600002040600>的age属性值从[0]变为了[11]
<DBPerson: 0x600002040620> - 旧值:22
<DBPerson: 0x600002040620> - 新值:22
*/

1.1. 分析

疑问:self.person1.age = 11self.person2.age = 22的本质都是调用age的setter方法,为什么能够监听到person1属性age的值变化,监听不到person2属性age的值变化?

person1和person2是不同的对象,我们看一下isa指针相关信息。

通过打印看到,person1的isa指向了NSKVONotifying_DBPerson,person2的isa指向了DBPerson。我们创建的person1是DBPerson类,怎么变成了NSKVONotifying_DBPerson?而且我们也没有去创建这个类。

当一个对象使用KVO后,程序就会动态为这个对象的类创建一个新类,类名是NSKVONotifying_类名,并且新创建的类是对象之前类的子类。所以,NSKVONotifying_DBPerson是程序运行期间使用runtime动态创建的DBPerson的子类。

未使用KVO监听的对象,对象的isa指向DBPerson类对象。

使用了KVO监听的对象,对象的isa指向NSKVONotifying_DBPerson类对象,NSKVONotifying_DBPerson类对象的superclass指向DBPerson类对象。NSKVONotifying_DBPerson类对象实现了setAge:方法,所以person1会执行NSKVONotifying_DBPerson类对象中的setAge:方法。

从这一点也可以看出,对象的isa指向不同,最终的结果也可能会不一样。

NSKVONotifying_DBPerson类对象中的setAge:方法内部调用了Foundation框架中的_NSSetIntValueAndNotify函数(是一个C函数)。

NSKVONotifying_DBPerson的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@implementation NSKVONotifying_DBPerson

- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}

void _NSSetIntValueAndNotify() {
// 1. 将要修改age
[self willChangeValueForKey:@"age"];
// 2. 执行父类的setAge:
[super setAge:age];
// 3. age值已经改变
[self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key {
// 4. 通知监听器
[observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

// 屏蔽内部实现,隐藏了NSKVONotifying_DBPerson类的存在
- (Class)class {
return [DBPerson class];
}

// 收尾工作
- (void)dealloc {

}

// 是否实现KVO
- (BOOL)_isKVOA {
return YES;
}

@end

注意:如果一个对象使用了KVO,不要手动创建名为NSKVONotifying_对象isa指向类的类名的类,否则KVO就会失效。

1.2. 验证

1.2.1. 验证NSKVONotifying_DBPerson类的生成时机

runtime中的object_getClass()方法可以获取对象isa指向的类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSLog(@"person1添加KVO[前]:%@", object_getClass(self.person1));
NSLog(@"person2:%@", object_getClass(self.person2));

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"监听age值变化"];

NSLog(@"person1添加KVO[后]:%@", object_getClass(self.person1));
NSLog(@"person2:%@", object_getClass(self.person2));

/*
输出:
person1添加KVO[前]:DBPerson
person2:DBPerson
person1添加KVO[后]:NSKVONotifying_DBPerson
person2:DBPerson
*/

经过验证发现,NSKVONotifying_DBPerson派生类是在对象添加KVO监听后动态生成的。

1.2.2. 验证_NSSetIntValueAndNotify函数的存在

runtime中的methodForSelector()方法可以获取指定方法名的实现地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSLog(@"person1添加KVO[前]:%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person2:%p", [self.person2 methodForSelector:@selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"监听age值变化"];

NSLog(@"person1添加KVO[后]:%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person2:%p", [self.person2 methodForSelector:@selector(setAge:)]);

/*
输出:
person1添加KVO[前]:0x10ae40530
person2:0x10ae40530
person1添加KVO[后]:0x7fff207bc2b7
person2:0x10ae40530
*/

通过lldb指令打印对应的方法实现地址可以发现,已添加KVO的对象调用属性的setter方法是在派生类的_NSSetIntValueAndNotify()函数中实现的。

_NSSetIntValueAndNotify仅仅是因为我们监听的属性是int类型而使用的函数,对应其他类型的有_NSSetBoolValueAndNotify_NSSetObjectValueAndNotify_NSSetRectValueAndNotify等很多类型的函数。

1.2.3. 获取NSKVONotifying_DBPerson类的对象方法

通过runtime的class_copyMethodList()可以获取到一个类已实现的所有对象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 打印指定类的所有实现方法
- (void)printMethodNamesOfClass:(Class)cls {
// 方法个数
unsigned int count;
// 方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *names = [NSMutableString string];
// 遍历方法数组
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[names appendFormat:@"\n%@", methodName];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@:(%@)", cls, names);
}

[self printMethodNamesOfClass:object_getClass(self.person1)];

/*
输出:
NSKVONotifying_DBPerson:(
setAge:
class
dealloc
_isKVOA)
*/

二、KVC

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性。

常见的API有:

1
2
3
4
5
6
// 赋值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
// 取值
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

2.1. setValue:forKey:的原理

+ (BOOL)accessInstanceVariablesDirectly {}:是否允许直接访问成员变量,默认返回值是YES。

2.2. valueForKey:的原理


面试题1:iOS用什么方式实现对一个对象的KVO?(KVO的本质)

解答:利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify()函数:

  • willChangeValueForKey:
  • 父类原来的setter
  • didChangeValueForKey:内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:

面试题2:如何手动触发KVO?

解答:手动调用willChangeValueForKey:didChangeValueForKey:(两个方法缺一不可)。

面试题3:直接修改成员变量会触发KVO么?

解答:不会触发KVO,因为不会触发setter方法。如果想要触发KVO,可以手动触发(面试题2)。

面试题4:通过KVC修改属性会触发KVO么?

解答:会触发KVO。如果没有属性只有成员变量,在setValue:forKey:内部会调用willChangeValueForKey:didChangeValueForKey:,所以会触发KVO。

面试题5:KVC的赋值和取值过程是怎样的?原理是什么?

解答:参考2.1和2.2的图。