Hello, World!

KVO、KVC

字数统计: 2.1k阅读时长: 8 min
2018/09/05 Share

KVO

KVO的使用

KVO(Key Value Observing),键值监听,用于监听对象的属性值的改变。
使用方式如下:

1
2
3
4
5
6
7
8
9
10
// 被监听对象
@interface Animal : NSObject

@property(nonatomic, copy) NSString *name;

@end

@implementation Animal

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[super viewDidLoad];

Animal *animal = [[Animal alloc] init];
animal.name = @"Panda";
// 添加监听
[animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

animal.name = @"Mouse";

// 移除监听
[animal removeObserver:self forKeyPath:@"name" context:nil];
}

// 监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}

log信息如下:

1
2
3
4
5
{
kind = 1;
new = Mouse;
old = Panda;
}

上述代码可以看出,在添加监听之后,name属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。

注意:在合适的时机要移除监听。

KVO的原理

分析
通过上述代码我们发现,一旦name属性的值发生改变,就会通知到观察者,并且赋值操作都是调用set方法实现的,我们可以重写Animal类中name的set方法,观察是否是KVO在setter方法内部做了一些操作来通知监听者。

我们发现即使重写了set方法,animal对象依然会调用set方法和执行监听器的observeValueForKeyPath方法。

说明KVO在运行时会对animal对象做一些改变,使得animal对象在调用set方法的时候可能做了一些额外的操作,所以谜底还是在对象本身。

本质

首先我们对上述代码中添加监听的地方打断点,观察一下addObserver方法对animal对象做了什么处理?也就是说animal对象在经过addObserver方法之后发生了什么改变,我们通过打印isa指针如下所示

1
2
3
4
5
6
7
8
// 添加监听之前的isa
(lldb) po animal->isa
Animal

// 添加监听之后的isa
(lldb) po animal->isa
NSKVONotifying_Animal

我们发现,animal对象执行过addObserver操作之后,animal实例的isa指针由之前的指向类对象Animal变为指向NSKVONotifyin_Animal类对象。也就是说一旦animal对象添加了KVO监听以后,其isa指针就会发生变化,因此类对象里面的的set方法的执行效果就不一样了。

NSKVONotifying_Animal其实是Animal的子类,那么也就是说其super_class指针是指向Animal类对象的,NSKVONotifying_Animal 类是runtime在运行时动态创建的。

那么animal对象在调用setName方法的时候,会根据isa找到NSKVONotifying_Animal类对象,在NSKVONotifying_Animal中找setName的方法及实现。

NSKVONotifying_Animal中的setName方法其实调用了Fundation框架中C语言函数_NSSetCharValueAndNotify

_NSSetCharValueAndNotify函数内部做的操作相当于:

1,调用willChangeValueForKey将要改变方法,类似于[self willChangeValueForKey:],
2,调用父类的setName方法对成员变量赋值,类似于[super setName:]
3,最后调用didChangeValueForKey已经改变方法,类似于[self didChangeValueForKey:]
didChangeValueForKey中会调用监听者的监听方法,最终来到监听者的observeValueForKeyPath方法中。

NSKVONotifying_Animal

NSKVONotifying_Animal作为Animal的子类,其super_class指针指向Animal类对象,并且NSKVONotifying_Animal内部一定对setName:方法做了单独的实现,那么NSKVONotifying_Animal同Animal类的差别可能就在于其存储的实例方法及方法实现不同。

通过runtime打印Animal类对象和NSKVONotifying_Animal类对象内存储的实例方法

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
// 通过runtime打印类对象的方法名
- (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);
}

// 通过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);

// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加KVO监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);

[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];

log信息如下:

1
2
Person类的方法 - name,setName:,
NSKVONotifying_Animal类的方法 - setName:,class,dealloc,_isKVOA,

通过上述代码我们发现NSKVONotifying_Animal中有4个对象方法。分别为setName:、class、 dealloc、 _isKVOA,那么至此我们可以画出NSKVONotifying_Animal的内存结构以及方法调用顺序。

KVO

这里NSKVONotifying_Animal重写class方法是为了隐藏自己不被外界所看到。

我们在添加KVO监听之后,打印animal对象的class方法可以发现返回Animal。

如果NSKVONotifying_Animal不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObject的class的实现大致为返回自己真实isa指向的类,就是animal的isa指向的类那么打印出来的类就是NSKVONotifying_Animal

但是官方不希望将NSKVONotifying_Animal类暴露出来,并且不希望我们知道其内部实现,所以在内部重写了class类,直接返回Animal类,所以外界在调用animal的class对象方法时是Animal类。这样animal实例给外界的感觉还是Animal类,并不知道NSKVONotifying_Animal子类的存在。

那么我们可以猜测NSKVONotifying_Animal内重写的class方法内部实现大致为:

1
2
3
4
- (Class) class {
// 得到自己的类对象,再找到类对象父类
return class_getSuperclass(object_getClass(self));
}

手动触发KVO

通过手动调用对象的didChangeValueForKey方法可以触发KVO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
[super viewDidLoad];

Animal *animal = [[Animal alloc] init];
animal.name = @"Panda";

[animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

[animal willChangeValueForKey:@"name"];
[animal didChangeValueForKey:@"name"];

[animal removeObserver:self forKeyPath:@"name" context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}

log信息如下

1
2
3
4
5
{
kind = 1;
new = Panda;
old = Panda;
}

通过打印我们可以发现,didChangeValueForKey方法内部成功调用了observeValueForKeyPath:ofObject:change:context:,并且name的值并没有发生改变。

总结

KVO原理:
当对象使注册了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的setter方法实现,setter方法实现内部会调用Foundation的_NSSet*ValueAndNotify方法。

_NSSet*ValueAndNotify方法内部顺序调用willChangeValueForKey方法、原父类的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

手动触发KVO:
被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

注意:KVO是重写了setter方法,直接修改成员变量不会触发setter方法,所以不会触发KVO。

KVC

KVC的使用

KVC(Key Value Coding),键值编码,用于通过key来读写属性。

常用API:

1
2
3
4
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

KVC赋值的原理

KVC赋值的方式,是通过访问属性的setter方法和访问成员变量来实现的。

1,首先会调用属性的-setKey:方法来赋值。

1
- setKey:

2,如果没有找到-setKey:方法,那么就会寻找-_setKey:方法来赋值。

1
- _setKey:

3,如果没有找到-_setKey:方法,那么就会寻找-setIsKey:方法,来询问是否可以访问成员变量来赋值。

1
- setIsKey:

4,如果没有找到-setIsKey:方法,那么就会寻找accessInstanceVariablesDirectly方法,来询问是否可以访问成员变量来赋值。

1
+ accessInstanceVariablesDirectly

如果返回NO,那么会抛出异常,如果返回YES,则表示可以通过访问成员变量来赋值。

5,访问成员变量会按照_Key、_isKey、Key、isKey的顺序来逐个访问赋值,如果这4个成员变量都没有找到,就抛出异常。

KVC

KVC会触发KVO

1
2
3
4
5
@interface Animal : NSObject {
@public
NSString *_name;
}
@end

我们发现通过KVC改变成员变量照样可以触发KVO,但是我们自己访问成员变量就不会触发KVO,原因就是通过KVC访问成员变量的时候,系统会主动触发KVO,相当于:

1
2
3
[animal willChangeValueForKey:@"name"];
animal->_name = @"Cat";
[animal didChangeValueForKey:@"name"];

KVC取值的原理

和赋值一样,也是按顺序访问方法和成员变量获得值。

1,通过getKey方法取值。

1
- getKey

2,如果没有getKey方法,通过key方法取值。

1
- key

3,如果没有key方法,通过isKey方法取值。

1
- isKey

4,如果没有isKey方法,通过_key方法取值。

1
- _key

5,如果以上方法都没有找到,那么会通过accessInstanceVariablesDirectly方法的返回值来确定是否可以访问成员变量取值。

1
2
3
4
_key
_isKey
key
isKey

KVC

CATALOG
  1. 1. KVO
    1. 1.1. KVO的使用
    2. 1.2. KVO的原理
    3. 1.3. NSKVONotifying_Animal
    4. 1.4. 手动触发KVO
    5. 1.5. 总结
  2. 2. KVC
    1. 2.1. KVC的使用
    2. 2.2. KVC赋值的原理
    3. 2.3. KVC会触发KVO
    4. 2.4. KVC取值的原理