一、KVO
問題
-
iOS
用什么方式實現對一個對象的KVO
?(KVO
的本質是什么?) - 如何手動觸發
KVO
?
1. KVO
使用
KVO
的全稱Key-Value Observing
,俗稱“鍵值監聽”,可以用于監聽某個對象屬性值的改變。通過方法(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
給對象添加監聽
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 2;
p2.age = 5;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
p2.age = 20;
當對象某個屬性的值發生了改變之后會回調方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
}
監聽到<Person: 0x6000000b4300>對象的age屬性改變了,{
kind = 1;
new = 10;
old = 2;
}
上述代碼中可以看出,在添加監聽之后,age
屬性的值在發生改變時,就會通知到監聽者,執行監聽者的observeValueForKeyPath
方法。
記得在合適的時機移除對對象的監聽(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
2. KVO
實現原理
2.1 分析
通過上述代碼我們發現,一旦age
屬性的值發生改變時,就會通知到監聽者(觀察者),并且我們知道賦值操作都是調用set
方法,我們可以來到Persn
類中重寫age
的set
方法,觀察是否是KVO
在set
方法內部做了一些操作來通知監聽者。
我們發現即使重寫了set
方法,p1
對象和p2
對象調用同樣的set
方法,但是我們發現p1
除了調用set
方法之外還會另外執行監聽器的observeValueForKeyPath
方法。
說明KVO
在運行時會對p1
對象做了一些改變。相當于在程序運行過程中,對p1
對象做了一些變化,使得p1
對象在調用setage
方法的時候可能做了一些額外的操作,所以問題出在對象本身。
2.2 本質
首先我們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver
方法對p1
對象做了什么處理?也就是說p1
對象在經過addObserver
方法之后發生了什么改變,我們通過打印isa指針如下所示
// 添加監聽之前的isa
(lldb) po p1->isa
Person
(lldb) po p2->isa
Person
// 添加監聽之后的isa
(lldb) po p1->isa
NSKVONotifying_Person
(lldb) po p2->isa
Person
我們發現,p1
對象執行過addObserver: forKeyPath:
操作之后,p1
實例對象的isa
指針由之前的指向類對象Person
變為指向NSKVONotifyin_Person
類對象,而p2
對象沒有任何改變。也就是說一旦p1
對象添加了KVO
監聽以后,其isa
指針就會發生變化,因此類對象里面的的set
方法的執行效果就不一樣了。
那么我們先來觀察p2
對象在內容中是如何存儲的,然后對比p2
來觀察p1
。
p2
在調用setage
方法的時候,首先會通過p2
實例對象中的isa
指針找到Person
類對象,然后在類對象中找到setage
方法。然后找到方法對應的實現。如下圖所示
2.3 添加KVO
之后的實例對象isa
指向
p1
實例對象的isa
指針在經過KVO
監聽之后已經指向了NSKVONotifyin_Person
類對象。
NSKVONotifyin_Person
其實是Person
的子類,那么也就是說其super_class
指針是指向Person
類對象的,NSKVONotifyin_Person
類是runtime
在運行時生成的。
那么p1
實例對象在調用setage
實例方法的時候,會根據p1
的isa
找到NSKVONotifyin_Person
類對象,在NSKVONotifyin_Person中
找setage的方法及實現。
2.4 NSKVONotifyin_xxx
內部做的操作
NSKVONotifyin_Person
中的setage
方法中其實調用了Fundation
框架中C
語言函數 _NSsetIntValueAndNotify
。
_NSsetIntValueAndNotify
函數內部做的操作相當于:
- 調用
willChangeValueForKey
將要改變方法,類似于[self willChangeValueForKey:]
, - 調用父類的
setage
方法對成員變量賦值,類似于[super setage:]
- 最后調用
didChangeValueForKey
已經改變方法,類似于[self didChangeValueForKey:]
didChangeValueForKey
中會調用監聽者的監聽方法,最終來到監聽者的observeValueForKeyPath
方法中。
3. 驗證KVO
的內部實現
我們已經驗證了,在執行添加監聽的方法時,會將isa
指針指向一個通過runtime
創建的Person
的子類NSKVONotifyin_Person
的類對象。
另外我們可以通過打印方法實現的地址來看一下p1
和p2
的setage:
的方法實現的地址在添加KVO
前后有什么變化。
// 通過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:)]);
進入lldb
:
2020-01-09 16:27:09.786420+0800 KVO的本質[98033:1408550] 添加KVO監聽之前 p1 = 0x109df9040, p2 = 0x109df9040
2020-01-09 16:27:09.786912+0800 KVO的本質[98033:1408550] 添加KVO監聽之后 p1 = 0x7fff2564d626, p2 = 0x109df9040
(lldb) p (IMP)0x109df9040
(IMP) $0 = 0x0000000109df9040 (KVO的本質`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x7fff2564d626
(IMP) $1 = 0x00007fff2564d626 (Foundation`_NSSetIntValueAndNotify)
(lldb)
我們發現在添加KVO
監聽之前,p1
和p2
的setAge:
方法實現的地址相同,而經過KVO
之后,p1
的setAge:
方法實現的地址發生了變化。
我們通過打印方法實現來看一下前后的變化發現,確實如我們上面所講的一樣,p1
的setAge:
方法的實現由Person
類對象中的setAge:
方法轉換為了C
語言Foundation
框架的_NSsetIntValueAndNotify
函數。
Foundation
框架中會根據屬性的類型,調用不同的方法。例如我們之前定義的int
類型的age
屬性,那么我們看到Foundation
框架中調用的_NSsetIntValueAndNotify
函數。那么我們把age
的屬性類型變為double
重新打印一遍:
2020-01-09 16:30:30.633548+0800 KVO的本質[98184:1412183] 添加KVO監聽之前 p1 = 0x10567c010, p2 = 0x10567c010
2020-01-09 16:30:30.633898+0800 KVO的本質[98184:1412183] 添加KVO監聽之后 p1 = 0x7fff2564d3a8, p2 = 0x10567c010
(lldb) p (IMP)0x10567c010
(IMP) $0 = 0x000000010567c010 (KVO的本質`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x7fff2564d3a8
(IMP) $1 = 0x00007fff2564d3a8 (Foundation`_NSSetDoubleValueAndNotify)
(lldb)
我們發現調用的函數變為了_NSSetDoubleValueAndNotify
,那么這說明Foundation
框架中有許多此類型的函數,通過屬性的不同類型調用不同的函數。
那么我們可以推測Foundation
框架中還有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify
等等函數。
我們可以找到Foundation
框架文件,通過命令行查詢關鍵字找到相關函數
4. Foundation
框架的_NSSet*ValueAndNotify
函數的內部邏輯
我們在Person
類中重寫willChangeValueForKey:
和didChangeValueForKey:
方法,模擬他們的實現。
- (void)setAge:(int)age
{
NSLog(@"setAge:");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
再次運行來查看didChangeValueForKey:
的方法內運行過程,通過打印的順序可以看到,確實在didChangeValueForKey:
方法內部已經調用了observer
的observeValueForKeyPath:ofObject:change:context:
方法。
2020-01-09 17:07:17.044803+0800 KVO的本質[890:1450834] willChangeValueForKey: - begin
2020-01-09 17:07:17.044941+0800 KVO的本質[890:1450834] willChangeValueForKey: - end
2020-01-09 17:07:17.045039+0800 KVO的本質[890:1450834] 調用了setAge
2020-01-09 17:07:17.045133+0800 KVO的本質[890:1450834] didChangeValueForKey: - begin
2020-01-09 17:07:17.045323+0800 KVO的本質[890:1450834] 監聽到<Person: 0x6000002f0570>對象的age屬性改變了,{
kind = 1;
new = 249421248;
old = 249421248;
}
2020-01-09 17:07:17.045423+0800 KVO的本質[890:1450834] didChangeValueForKey: - end
5. NSKVONotifyin_Person
類對象存儲的實例方法
NSKVONotifyin_Person
作為Person
的子類,其super_class
指針指向Person
類對象,并且NSKVONotifyin_Person
內部一定對setAge:
方法做了單獨的實現,那么NSKVONotifyin_Person
同Person
類的差別可能就在于其存儲的實例方法及方法實現不同。
我們通過runtime
分別打印Person
類對象和NSKVONotifyin_Person
類對象內存儲的實例方法
// 通過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)];
結果
2020-01-09 16:48:16.718818+0800 KVO的本質[99651:1433359] Person類的方法 - age,setAge:,
2020-01-09 16:48:16.718958+0800 KVO的本質[99651:1433359] NSKVONotifying_Person類的方法 - setAge:,class,dealloc,_isKVOA,
通過上述代碼我們發現NSKVONotifyin_Person
中有4個對象方法。分別為setAge:
、class
、 dealloc
、 _isKVOA
,那么至此我們可以畫出NSKVONotifyin_Person
的內存結構以及方法調用順序。
這里NSKVONotifyin_Person
重寫class
方法是為了隱藏NSKVONotifyin_Person
不被外界所看到。我們在p1
添加過KVO
監聽之后,分別打印p1
和p2
對象的class
方法可以發現他們都返回Person
。
NSLog(@"%@,%@",[p1 class],[p2 class]);
// Person,Person
如果NSKVONotifyin_Person
不重寫class
方法,那么當對象要調用class
對象方法的時候就會一直向上找來到NSObject
,而NSObject
的class
的實現大致為返回自己真實isa
指向的類,就是p1
的isa
指向的類那么打印出來的類就是NSKVONotifyin_Person
.
但是官方不希望將NSKVONotifyin_Person
類暴露出來,并且不希望我們知道NSKVONotifyin_Person
內部實現,所以在內部重寫了class類,直接返回Person類,所以外界在調用p1
的class
對象方法時是Person
類。這樣p1
給外界的感還是Person
類,并不知道NSKVONotifyin_Person
子類的存在。
那么我們可以猜測NSKVONotifyin_Person
內重寫的class
方法內部實現大致為:
- (Class) class {
// 得到自己的類對象,再找到類對象父類
return class_getSuperclass(object_getClass(self));
}
6. 手動觸發KVO
通過手動觸發對象的didChangeValueForKey:
方法可以觸發KVO:
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
[p1 removeObserver:self forKeyPath:@"age"];
打印結果:
監聽到<Person: 0x600003053790>對象的age屬性改變了,{
kind = 1;
new = 2;
old = 2;
}
通過打印我們可以發現,didChangeValueForKey
方法內部成功調用了observeValueForKeyPath:ofObject:change:context:
,并且age
的值并沒有發生改變。
7. 問題:
iOS
用什么方式實現對一個對象的KVO
?(KVO
的本質是什么?)
當一個對象使用了KVO
監聽,iOS
系統會修改這個對象的isa
指針,改為指向一個全新的通過Runtime
動態創建的子類,子類擁有自己的set
方法實現,set
方法實現內部會調用Foundation
的_NSSet*ValueAndNotify
方法。
_NSSet*ValueAndNotify
方法內部順序調用willChangeValueForKey
方法、原來父類的setter
方法實現、didChangeValueForKey
方法,而didChangeValueForKey
方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:
監聽方法。
如何手動觸發KVO
?
被監聽的屬性的值被修改時,就會自動觸發KVO
。如果想要手動觸發KVO
,則需要我們自己調用willChangeValueForKey
和didChangeValueForKey
方法即可在不改變屬性值的情況下手動觸發KVO
,并且這兩個方法缺一不可。
直接修改成員變量的值會觸發KVO
嗎?
KVO
是重寫了set
方法,直接修改成員變量不會觸發set
方法,所以不會觸發KVO
。
person->_age = 2;
二、KVC
問題
- 通過
KVC
修改屬性會觸發KVO
嗎? -
KVC
的賦值和取值過程是怎么樣的?原理是什么?
1. 使用
KVC
的全稱是Key-Value Coding
,俗稱“鍵值編碼”,可以通過一個key
來訪問某個屬性。
常見的API
有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
Person *person = [[Person alloc]init];
[person setValue:@10 forKey:@"age"];
// person對象的cat屬性的weight屬性
person.cat = [[Cat alloc] init];
[person setValue:@20 forKeyPath:@"cat.weight"];
NSLog(@"%@",[person valueForKey:@"age"]);
NSLog(@"%@",[person valueForKeyPath:@"cat.weight"]);
2. setValue:forKey:
設值的原理
2.1 KVC
會觸發KVO
- 通過訪問屬性
Observer *observer = [[Observer alloc]init];
Person *person = [[Person alloc]init];
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 通過KVC修改age屬性的值
[person setValue:@10 forKey:@"age"];
[person removeObserver:observer forKeyPath:@"age"];
我們發現是觸發了KVO
的:
2020-04-10 21:34:18.284166+0800 KVC的本質[29149:8625107] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
需要注意的是這里Person
類是有屬性age``的`,所以系統會自動生成
set```方法。
2.2 原理
通過KVC
的方式賦值的時候其實通過訪問屬性的set
方法和訪問成員變量來達到設值的。
- 首先會調用屬性的
set
方法來賦值:
- (void)setAge:(int)age {
NSLog(@"setAge: %d", _age);
}
- 如果沒有找到屬性的
set
方法,那么會來到_setAge
方法來賦值:
- (void)_setAge:(int)age {
NSLog(@"_setAge: %d", _age);
}
- 如果沒有找到
_setAge
方法,那么會來到accessInstanceVariablesDirectly
方法,來詢問是否可以訪問成員變量來賦值:
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
如果返回NO
,那么會拋出異常,如果返回YES
,則表示可以通過訪問成員變量來賦值。
- 訪問成員變量會按照
_age
、_isAge
、age
、isAge
的順序來逐個訪問賦值,如果這4個成員變量都沒有找到,就拋出異常。
@interface Person : NSObject
{
@public
int _age;
int _isAge;
int age;
int isAge;
}
2.3 KVC
會觸發KVO
- 通過訪問成員變量
現在我們刪除Person
的屬性,添加成員變量_age
@interface Person : NSObject
{
@public
int _age;
}
我們發現通過KVC
改變屬性照樣可以觸發KVO
KVC的本質[30832:8656264] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
我們來重寫Person
的willChangeValueForKey
和didChangeValueForKey
方法:
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey");
}
KVC的本質[30911:8657993] willChangeValueForKey
2020-04-10 22:14:44.426343+0800 KVC的本質[30911:8657993] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
2020-04-10 22:14:44.426514+0800 KVC的本質[30911:8657993] didChangeValueForKey
我們發現KVC
通過訪問成員變量來賦值的時候,其內部會主動觸發KVO
。但是我們自己訪問成員變量不會觸發KVO
。原因就是通過KVC
訪問成員變量的時候,系統會主動觸發KVO
,相當于:
[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];
3. valueForKey:
取值的原理
和賦值一樣的,也是按順序訪問方法和成員變量獲得值:
- 通過
getAge
方法取值
- (int)getAge {
return 10;
}
- 如果沒有
getAge
方法,通過age
方法取值
- (int)age {
return 11;
}
- 如果沒有
age
方法,通過isAge
方法取值
- (int)isAge {
return 12;
}
- 如果沒有
isAge
方法,通過_age
方法取值
- (int)_age {
return 13;
}
- 如果以上方法都沒有找到,那么會通過
accessInstanceVariablesDirectly
方法的返回值來確定是否可以訪問成員變量取值
_age
_isAge
age
isAge