這篇文章介紹KVC、KVO的本質。如果你對KVC、KVO不瞭解,推薦先查看其用法:KVC和KVO學習筆記。
1. KVO的本質
KVO 是 Key Value Observing 的縮寫,稱爲健值觀察。用於監聽對象屬性值的改變。
1.1 KVO的實現
觀察者模式使用示例如下:
@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"];
}
// 觀察到鍵值發生改變
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"監聽到 %@ 的 %@ 屬性值發生改變 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
// 移除觀察者
[self.child1 removeObserver:self
forKeyPath:@"age"];
}
@end
1.2 runtime動態創建NSKVONotifying_XXX類
通過上述代碼可以看到,age
屬性發生改變後,會通知監聽者,即觸發observeValueForKeyPath:ofObject:change:context:
方法。我們知道賦值操作是通過調用set方法實現,進入Child
類,重寫setAge:
方法,查看KVO是否通過修改set方法實現。
@implementation Child
- (void)setAge:(int)age {
_age = age;
NSLog(@"KVO是否通過重寫setAge:方法實現?age:%d", age);
}
@end
測試後發現,修改child1
和child2
都會觸發setAge:
方法,但child1
會額外觸發KVO。說明KVO在運行時對child1
進行了修改,使得child1
在調用setAge:
時,進行了額外的操作。
根據 runtime 的原理,向實例對象發送消息時,先根據實例對象的 isa 查找到類對象,在類對象的方法列表中查找方法實現。因此,可以查看child1
和child2
是否指向同一個類對象:
// 打印添加觀察者前實例對象的isa
NSLog(@"添加Observer前 child1: %@ - child2: %@", object_getClass(self.child1), object_getClass(self.child2));
// 添加觀察者
[self.child1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:@"123context"];
// 打印添加觀察者後實例對象的isa
NSLog(@"添加Observer後 child1: %@ - child2: %@", object_getClass(self.child1), object_getClass(self.child2));
打印結果如下:
添加Observer前 child1: Child - child2: Child
添加Observer後 child1: NSKVONotifying_Child - child2: Child
如果你對 runtime、isa不瞭解,可以查看我的另一篇文章:Runtime從入門到進階一。
child1
添加觀察者後,isa指針由之前的指向Child
,變爲了NSKVONotifying_Child
,而child2
實例對象 isa 的指向沒有發生改變。
添加觀察者之前,child1
實例在內存中的結構如下:
添加觀察者後,child1
對象的isa指針指向了NSKVONotifying_Child
類。NSKVONotifying_Child
的superclass
指針指向Child
類。
NSKVONotifying_Child
的setAge:
方法調用了_NSSetIntValueAndNotify
函數,該函數依次執行以下三步:
- 先調用
willChangeValueForKey:
- 後調用
[super setAge:]
- 最後調用
didChangeValueForKey:
觸發監聽
下面打印添加觀察者前後setAge:
方法實現的變化:
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:)]);
控制檯輸出如下:
添加Observer前 child1: 0x10b247190 - child2: 0x10b247190
添加Observer後 child1: 0x7fff207bb2b7 - child2: 0x10b247190
可以看到,添加觀察者後,child1
的setAge:
地址發生了變化,child2
的setAge:
地址沒有發生變化。繼續打印其具體實現:
(lldb) p (IMP)0x10b247190
(IMP) $0 = 0x000000010b247190 (KVC&KVO的本質`-[Child setAge:] at Child.m:12)
(lldb) p (IMP)0x7fff207bb2b7
(IMP) $1 = 0x00007fff207bb2b7 (Foundation`_NSSetIntValueAndNotify)
可以看到,添加觀察者後,child1
的setAge:
方法實現轉換成了C語言Foundation
框架中的_NSSetIntValueAndNotify
函數。如果setAge:
參數類型是double,其會自動調用_NSSetDoubleValueAndNotify
函數,也就是會根據類型自動轉換。
1.3 NSKVONotifying_Child具體實現
NSKVONotifying_Child
繼承自Child
,NSKVONotifying_Child
內部重寫了setAge:
方法,通過 runtime 的class_copyMethodList()
函數可以查看對象的方法列表:
如下所示:
- (void)printMethodList:(Class)cls {
unsigned int methodCount;
// 獲取方法數組
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語言中使用copy、create創建的對象需釋放。
free(methodList);
NSLog(@"%@ %@", cls, name);
}
添加觀察者後,分別打印child1
、child2
:
NSKVONotifying_Child setAge:, class, dealloc, _isKVOA,
Child setAge:, age,
可以看到,NSKVONotifying_Child
對象有四個方法,分別爲
-
setAge:
:觸發觀察者的具體邏輯。 -
class
:重寫class
方法,直接返回父類名稱。這樣可以屏蔽KVO內部實現,隱藏NSKVONOtifying_Child
類的存在。 -
dealloc
:釋放時進行收尾工作。 -
_isKVOA
:當前類是否是添加KVO後系統使用runtime創建的。
添加觀察者之後,child1
實例在內存中的結構如下:
另外,還可以手動創建名稱爲NSKVONotifying_Child
、繼承自Child
的類。添加後,註冊觀察者時會報以下錯誤:
[general] KVO failed to allocate class pair for name NSKVONotifying_Child, automatic key-value observing will not work for this class
這也從側面證明了runtime會動態創建NSKVONOtifying_Child
類。
1.4 驗證didChangeValueForKey:內部會調用observeValueForKeyPath:ofObject:change:context:方法
在Child
類中重寫willChangeValueForKey:
、didChangeValueForKey:
,查看哪一步調用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");
}
// 驗證didChangeValueForKey:調用了KVO
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
控制檯輸出如下:
begin - setAge:11
end - setAge:11
didChangeValueForKey: - begin
監聽到 <Child: 0x600000168250> 的 age 屬性值發生改變 - {
kind = 1;
new = 11;
old = 1;
} - 123context
didChangeValueForKey: - end
可以看到,didChangeValueForKey:
方法內部調用了observer的observeValueForKeyPath:ofObject:change:context:
方法。
如果沒有實現
willChangeValueForKey:
,只調用didChangeValueForKey:
,其並不會調用observer的observeValueForKeyPath:ofObject:change:context:
方法。
1.5 KVO 面試題
1.5.1 iOS 使用什麼方式實現對一個對象的kvo?即KVO的本質是什麼。
KVO是利用runtime API 動態生成一個子類,並且讓 instance 對象的isa指向這個全新的子類,該子類的superclass指針指向原來的類。
當修改instance對象的屬性時,會調用Foundation的_NSSetXXValueAndNotify
函數。其內部會依次執行以下方法:
- 調用
willChangeValueForKey:
- 調用父類的 setter。
- 調用
didChangeValueForKey:
,該方法內部會觸發監聽器的observeValueForKeyPath:ofObject:change:context:
。
1.5.2 如何手動觸發KVO?
手動調用willChangeValueForKey:
和didChangeValueForKey:
。
1.5.3 直接修改成員變量會觸發KVO嗎?
由於 runtime 是通過創建子類,重寫setter方法實現的監聽值改變,直接修改成員變量並不會調用setter方法。因此,直接修改成員變量不會觸發KVO。
2. KVC的本質
KVC 是 Key Value Coding 的縮寫,稱爲健值編碼。
2.1 設值原理setValue:forKey:
KVC設值方法有以下兩個API:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
setValue:forKey:
原理如下圖所示:
setValue:forKey:
原理如下:
- 先查找
setKey:
、_setKey:
方法,如果找到了,直接傳遞參數,調用方法;如果找不到,進入下一步。 - 查看
accessInstanceVariableDirectly
方法返回值,如果返回NO
,即不允許訪問成員變量,則調用setValue:forUndefinedKey:
方法,並拋出NSUnknownKeyException
異常;如果返回YES
,進入下一步。 - 按照
_key
、_isKey
、key
、isKey
順序查找成員變量,如果找到了成員變量,直接賦值。如果找不到,則調用setValue:forUndefinedKey:
方法,並拋出NSUnknownKeyException
異常。
2.1.1 有set方法
有set方法時,直接調用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
執行後,控制檯輸出如下:
-[Person setAge:] 10
-[Observer observeValueForKeyPath:ofObject:change:context:] - {
kind = 1;
new = 10;
old = 0;
}
可以看到,KVC先調用了set方法,然後觸發了KVO。
如果將屬性設置爲readonly
,在其他類中將不能通過訪問器方法修改屬性值,但可以使用KVC修改。這是因爲設置爲readonly
後,雖然不會生成set方法,但會生成_key
成員變量。此時,KVC直接爲_key
賦值,具體原理可以查看後面部分內容。
2.1.2 沒有set方法
沒有set方法時,會先查看accessInstanceVariableDirectly
方法返回值,如果返回NO
,則調用setValue:forUndefinedKey:
方法,並拋出NSUnknownKeyException
異常;如果返回YES
,則按照_key
、_isKey
、key
、isKey
順序查找成員變量。如果找到了成員變量,直接賦值;如果找不到,則調用setValue:forUndefinedKey:
方法,並拋出NSUnknownKeyException
異常。
添加以下成員變量,並移除屬性:
{
@public
// 如果沒有set方法,按照下面順序查找。
int _age;
int _isAge;
int age;
int isAge;
}
使用setValue:forKey:
設置成員變量值後,會按順序查找上述成員變量,找到後直接賦值。其順序是固定的,與聲明成員變量的先後次序無關。
2.2 KVC 內部實現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
設值,控制檯輸出如下:
-[Person willChangeValueForKey:] age
-[Person didChangeValueForKey:] - begin - age
- {
kind = 1;
new = 10;
old = 0;
}
-[Person didChangeValueForKey:] - end - age
與KVO類似,其內部也實現了willChangeValueForKey:
和didChangeValueForKey:
方法,並且是在didChangeValueForKey:
後觸發觀察者的observeValueForKeyPath:ofObject:change:context:
方法。
2.3 取值原理valueForKey:
KVC取值方法有以下兩個API:
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
valueForKey:
原理如下圖所示:
valueForKey:
原理如下:
- 按照
getKey:
、key
、isKey
、_key
順序查找方法,找到後直接調用方法;如果找不到,則進入下一步。 - 查看
accessInstanceVariableDirectly
方法返回值,如果返回NO
,則調用setValue:forUndefinedKey:
方法,並拋出NSUnknownKeyException
異常;如果返回YES
,則進入下一步。 - 按照
_key
、_isKey
、key
、isKey
順序查找成員變量。如果找到了,直接取值;如果找不到,則調用setValue:forUndefinedKey:
方法,並拋出NSUnknownKeyException
異常。
在Person
類中,添加以下方法:
- (int)getAge {
return 22;
}
使用以下方法取值:
NSLog(@"age: %@", [self.person valueForKey:@"age"]);
可以看到其取出的是getAge
的值。你可以自行添加age
、isAge
、_age
、_isAge
方法,驗證其取值順序。
2.4 KVC面試題
2.4.1 通過KVC修改屬性會觸發KVO嗎?
會觸發。其內部調用了willChangeValueForKey:
、didChangeValueForKey:
,並在調用didChangeValueForKey:
方法後觸發觀察者的observeValueForKeyPath:ofObject:change:context:
方法。
2.4.2 KVC取值、賦值過程是怎麼樣的?原理是什麼?
取值、賦值過程和原理即爲上面兩個圖片內容。
Demo名稱:KVC&KVO的本質
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/KVC&KVO的本質
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/KVC、KVO的本質.md