這篇文章介紹KVC、KVO的本質(zhì)。如果你對(duì)KVC、KVO不了解,推薦先查看其用法:KVC和KVO學(xué)習(xí)筆記。
1. KVO的本質(zhì)
KVO 是 Key Value Observing 的縮寫,稱為健值觀察。用于監(jiān)聽對(duì)象屬性值的改變。
1.1 KVO的實(shí)現(xiàn)
觀察者模式使用示例如下:
@interface ViewController ()
@property (nonatomic, strong) Child *child1;
@property (nonatomic, strong) Child *child2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.child1 = [[Child alloc] init];
self.child1.age = 1;
self.child2 = [[Child alloc] init];
self.child2.age = 2;
// 添加觀察者
[self.child1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:@"123context"];
}
// 觀察到鍵值發(fā)生改變
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"監(jiān)聽到 %@ 的 %@ 屬性值發(fā)生改變 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
// 移除觀察者
[self.child1 removeObserver:self
forKeyPath:@"age"];
}
@end
1.2 runtime動(dòng)態(tài)創(chuàng)建NSKVONotifying_XXX類
通過(guò)上述代碼可以看到,age屬性發(fā)生改變后,會(huì)通知監(jiān)聽者,即觸發(fā)observeValueForKeyPath:ofObject:change:context:方法。我們知道賦值操作是通過(guò)調(diào)用set方法實(shí)現(xiàn),進(jìn)入Child類,重寫setAge:方法,查看KVO是否通過(guò)修改set方法實(shí)現(xiàn)。
@implementation Child
- (void)setAge:(int)age {
_age = age;
NSLog(@"KVO是否通過(guò)重寫setAge:方法實(shí)現(xiàn)?age:%d", age);
}
@end
測(cè)試后發(fā)現(xiàn),修改child1和child2都會(huì)觸發(fā)setAge:方法,但child1會(huì)額外觸發(fā)KVO。說(shuō)明KVO在運(yùn)行時(shí)對(duì)child1進(jìn)行了修改,使得child1在調(diào)用setAge:時(shí),進(jìn)行了額外的操作。
根據(jù) runtime 的原理,向?qū)嵗龑?duì)象發(fā)送消息時(shí),先根據(jù)實(shí)例對(duì)象的 isa 查找到類對(duì)象,在類對(duì)象的方法列表中查找方法實(shí)現(xiàn)。因此,可以查看child1和child2是否指向同一個(gè)類對(duì)象:
// 打印添加觀察者前實(shí)例對(duì)象的isa
NSLog(@"添加Observer前 child1: %@ - child2: %@", object_getClass(self.child1), object_getClass(self.child2));
// 添加觀察者
[self.child1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:@"123context"];
// 打印添加觀察者后實(shí)例對(duì)象的isa
NSLog(@"添加Observer后 child1: %@ - child2: %@", object_getClass(self.child1), object_getClass(self.child2));
打印結(jié)果如下:
添加Observer前 child1: Child - child2: Child
添加Observer后 child1: NSKVONotifying_Child - child2: Child
如果你對(duì) runtime、isa不了解,可以查看我的另一篇文章:Runtime從入門到進(jìn)階一。
child1添加觀察者后,isa指針由之前的指向Child,變?yōu)榱?code>NSKVONotifying_Child,而child2實(shí)例對(duì)象 isa 的指向沒有發(fā)生改變。
添加觀察者之前,child1實(shí)例在內(nèi)存中的結(jié)構(gòu)如下:

添加觀察者后,child1對(duì)象的isa指針指向了NSKVONotifying_Child類。NSKVONotifying_Child的superclass指針指向Child類。
NSKVONotifying_Child的setAge:方法調(diào)用了_NSSetIntValueAndNotify函數(shù),該函數(shù)依次執(zhí)行以下三步:
- 先調(diào)用
willChangeValueForKey: - 后調(diào)用
[super setAge:] - 最后調(diào)用
didChangeValueForKey:觸發(fā)監(jiān)聽
下面打印添加觀察者前后setAge:方法實(shí)現(xiàn)的變化:
NSLog(@"添加Observer前 child1: %p - child2: %p", [self.child1 methodForSelector:@selector(setAge:)], [self.child2 methodForSelector:@selector(setAge:)]);
// 添加觀察者
...
NSLog(@"添加Observer后 child1: %p - child2: %p", [self.child1 methodForSelector:@selector(setAge:)], [self.child2 methodForSelector:@selector(setAge:)]);
控制臺(tái)輸出如下:
添加Observer前 child1: 0x10b247190 - child2: 0x10b247190
添加Observer后 child1: 0x7fff207bb2b7 - child2: 0x10b247190
可以看到,添加觀察者后,child1的setAge:地址發(fā)生了變化,child2的setAge:地址沒有發(fā)生變化。繼續(xù)打印其具體實(shí)現(xiàn):
(lldb) p (IMP)0x10b247190
(IMP) $0 = 0x000000010b247190 (KVC&KVO的本質(zhì)`-[Child setAge:] at Child.m:12)
(lldb) p (IMP)0x7fff207bb2b7
(IMP) $1 = 0x00007fff207bb2b7 (Foundation`_NSSetIntValueAndNotify)
可以看到,添加觀察者后,child1的setAge:方法實(shí)現(xiàn)轉(zhuǎn)換成了C語(yǔ)言Foundation框架中的_NSSetIntValueAndNotify函數(shù)。如果setAge:參數(shù)類型是double,其會(huì)自動(dòng)調(diào)用_NSSetDoubleValueAndNotify函數(shù),也就是會(huì)根據(jù)類型自動(dòng)轉(zhuǎn)換。
1.3 NSKVONotifying_Child具體實(shí)現(xiàn)
NSKVONotifying_Child繼承自Child,NSKVONotifying_Child內(nèi)部重寫了setAge:方法,通過(guò) runtime 的class_copyMethodList()函數(shù)可以查看對(duì)象的方法列表:
如下所示:
- (void)printMethodList:(Class)cls {
unsigned int methodCount;
// 獲取方法數(shù)組
Method *methodList = class_copyMethodList(cls, &methodCount);
NSMutableString *name = [NSMutableString string];
for (int i=0; i<methodCount; ++i) {
// 獲得方法
Method method = methodList[I];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
[name appendString:methodName];
[name appendString:@", "];
}
// C語(yǔ)言中使用copy、create創(chuàng)建的對(duì)象需釋放。
free(methodList);
NSLog(@"%@ %@", cls, name);
}
添加觀察者后,分別打印child1、child2:
NSKVONotifying_Child setAge:, class, dealloc, _isKVOA,
Child setAge:, age,
可以看到,NSKVONotifying_Child對(duì)象有四個(gè)方法,分別為
-
setAge::觸發(fā)觀察者的具體邏輯。 -
class:重寫class方法,直接返回父類名稱。這樣可以屏蔽KVO內(nèi)部實(shí)現(xiàn),隱藏NSKVONOtifying_Child類的存在。 -
dealloc:釋放時(shí)進(jìn)行收尾工作。 -
_isKVOA:當(dāng)前類是否是添加KVO后系統(tǒng)使用runtime創(chuàng)建的。
添加觀察者之后,child1實(shí)例在內(nèi)存中的結(jié)構(gòu)如下:

另外,還可以手動(dòng)創(chuàng)建名稱為NSKVONotifying_Child、繼承自Child的類。添加后,注冊(cè)觀察者時(shí)會(huì)報(bào)以下錯(cuò)誤:
[general] KVO failed to allocate class pair for name NSKVONotifying_Child, automatic key-value observing will not work for this class
這也從側(cè)面證明了runtime會(huì)動(dòng)態(tài)創(chuàng)建NSKVONOtifying_Child類。
1.4 驗(yàn)證didChangeValueForKey:內(nèi)部會(huì)調(diào)用observeValueForKeyPath:ofObject:change:context:方法
在Child類中重寫willChangeValueForKey:、didChangeValueForKey:,查看哪一步調(diào)用observeValueForKeyPath:ofObject:change:context:方法:
- (void)setAge:(int)age {
NSLog(@"begin - setAge:%d", age);
_age = age;
NSLog(@"end - setAge:%d", age);
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
// 驗(yàn)證didChangeValueForKey:調(diào)用了KVO
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
控制臺(tái)輸出如下:
begin - setAge:11
end - setAge:11
didChangeValueForKey: - begin
監(jiān)聽到 <Child: 0x600000168250> 的 age 屬性值發(fā)生改變 - {
kind = 1;
new = 11;
old = 1;
} - 123context
didChangeValueForKey: - end
可以看到,didChangeValueForKey:方法內(nèi)部調(diào)用了observer的observeValueForKeyPath:ofObject:change:context:方法。
如果沒有實(shí)現(xiàn)
willChangeValueForKey:,只調(diào)用didChangeValueForKey:,其并不會(huì)調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法。
1.5 KVO 面試題
1.5.1 iOS 使用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的kvo?即KVO的本質(zhì)是什么。
KVO是利用runtime API 動(dòng)態(tài)生成一個(gè)子類,并且讓 instance 對(duì)象的isa指向這個(gè)全新的子類,該子類的superclass指針指向原來(lái)的類。
當(dāng)修改instance對(duì)象的屬性時(shí),會(huì)調(diào)用Foundation的_NSSetXXValueAndNotify函數(shù)。其內(nèi)部會(huì)依次執(zhí)行以下方法:
- 調(diào)用
willChangeValueForKey: - 調(diào)用父類的 setter。
- 調(diào)用
didChangeValueForKey:,該方法內(nèi)部會(huì)觸發(fā)監(jiān)聽器的observeValueForKeyPath:ofObject:change:context:。
1.5.2 如何手動(dòng)觸發(fā)KVO?
手動(dòng)調(diào)用willChangeValueForKey:和didChangeValueForKey:。
1.5.3 直接修改成員變量會(huì)觸發(fā)KVO嗎?
由于 runtime 是通過(guò)創(chuàng)建子類,重寫setter方法實(shí)現(xiàn)的監(jiān)聽值改變,直接修改成員變量并不會(huì)調(diào)用setter方法。因此,直接修改成員變量不會(huì)觸發(fā)KVO。
2. KVC的本質(zhì)
KVC 是 Key Value Coding 的縮寫,稱為健值編碼。
2.1 設(shè)值原理setValue:forKey:
KVC設(shè)值方法有以下兩個(gè)API:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
setValue:forKey:原理如下圖所示:

setValue:forKey:原理如下:
- 先查找
setKey:、_setKey:方法,如果找到了,直接傳遞參數(shù),調(diào)用方法;如果找不到,進(jìn)入下一步。 - 查看
accessInstanceVariableDirectly方法返回值,如果返回NO,即不允許訪問(wèn)成員變量,則調(diào)用setValue:forUndefinedKey:方法,并拋出NSUnknownKeyException異常;如果返回YES,進(jìn)入下一步。 - 按照
_key、_isKey、key、isKey順序查找成員變量,如果找到了成員變量,直接賦值。如果找不到,則調(diào)用setValue:forUndefinedKey:方法,并拋出NSUnknownKeyException異常。
2.1.1 有set方法
有set方法時(shí),直接調(diào)用set方法:
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)setAge:(int)age {
_age = age;
NSLog(@"%s %d", __PRETTY_FUNCTION__, age);
}
@end
執(zhí)行后,控制臺(tái)輸出如下:
-[Person setAge:] 10
-[Observer observeValueForKeyPath:ofObject:change:context:] - {
kind = 1;
new = 10;
old = 0;
}
可以看到,KVC先調(diào)用了set方法,然后觸發(fā)了KVO。
如果將屬性設(shè)置為readonly,在其他類中將不能通過(guò)訪問(wèn)器方法修改屬性值,但可以使用KVC修改。這是因?yàn)樵O(shè)置為readonly后,雖然不會(huì)生成set方法,但會(huì)生成_key成員變量。此時(shí),KVC直接為_key賦值,具體原理可以查看后面部分內(nèi)容。

2.1.2 沒有set方法
沒有set方法時(shí),會(huì)先查看accessInstanceVariableDirectly方法返回值,如果返回NO,則調(diào)用setValue:forUndefinedKey:方法,并拋出NSUnknownKeyException異常;如果返回YES,則按照_key、_isKey、key、isKey順序查找成員變量。如果找到了成員變量,直接賦值;如果找不到,則調(diào)用setValue:forUndefinedKey:方法,并拋出NSUnknownKeyException異常。
添加以下成員變量,并移除屬性:
{
@public
// 如果沒有set方法,按照下面順序查找。
int _age;
int _isAge;
int age;
int isAge;
}
使用setValue:forKey:設(shè)置成員變量值后,會(huì)按順序查找上述成員變量,找到后直接賦值。其順序是固定的,與聲明成員變量的先后次序無(wú)關(guān)。

2.2 KVC 內(nèi)部實(shí)現(xiàn)willChangeValueForKey: 和didChangeValueForKey:方法
在Person類添加以下代碼:
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"%s %@", __PRETTY_FUNCTION__, key);
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"%s - begin - %@", __PRETTY_FUNCTION__, key);
[super didChangeValueForKey:key];
NSLog(@"%s - end - %@", __PRETTY_FUNCTION__, key);
}
再次為age設(shè)值,控制臺(tái)輸出如下:
-[Person willChangeValueForKey:] age
-[Person didChangeValueForKey:] - begin - age
- {
kind = 1;
new = 10;
old = 0;
}
-[Person didChangeValueForKey:] - end - age
與KVO類似,其內(nèi)部也實(shí)現(xiàn)了willChangeValueForKey:和didChangeValueForKey:方法,并且是在didChangeValueForKey:后觸發(fā)觀察者的observeValueForKeyPath:ofObject:change:context:方法。
2.3 取值原理valueForKey:
KVC取值方法有以下兩個(gè)API:
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
valueForKey:原理如下圖所示:

valueForKey:原理如下:
- 按照
getKey:、key、isKey、_key順序查找方法,找到后直接調(diào)用方法;如果找不到,則進(jìn)入下一步。 - 查看
accessInstanceVariableDirectly方法返回值,如果返回NO,則調(diào)用setValue:forUndefinedKey:方法,并拋出NSUnknownKeyException異常;如果返回YES,則進(jìn)入下一步。 - 按照
_key、_isKey、key、isKey順序查找成員變量。如果找到了,直接取值;如果找不到,則調(diào)用setValue:forUndefinedKey:方法,并拋出NSUnknownKeyException異常。
在Person類中,添加以下方法:
- (int)getAge {
return 22;
}
使用以下方法取值:
NSLog(@"age: %@", [self.person valueForKey:@"age"]);
可以看到其取出的是getAge的值。你可以自行添加age、isAge、_age、_isAge方法,驗(yàn)證其取值順序。
2.4 KVC面試題
2.4.1 通過(guò)KVC修改屬性會(huì)觸發(fā)KVO嗎?
會(huì)觸發(fā)。其內(nèi)部調(diào)用了willChangeValueForKey:、didChangeValueForKey:,并在調(diào)用didChangeValueForKey:方法后觸發(fā)觀察者的observeValueForKeyPath:ofObject:change:context:方法。
2.4.2 KVC取值、賦值過(guò)程是怎么樣的?原理是什么?
取值、賦值過(guò)程和原理即為上面兩個(gè)圖片內(nèi)容。
Demo名稱:KVC&KVO的本質(zhì)
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/KVC&KVO的本質(zhì)
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/KVC、KVO的本質(zhì).md