- 一、KVO 的一個(gè)疑惑
- 二喊式、KVO 的淺層分析
- 三、KVO 淺層分析驗(yàn)證
- 四萧朝、KVO 子類內(nèi)部方法
- 五垃帅、手動(dòng)觸發(fā) KVO
一、KVO 的一個(gè)疑惑
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 給person1對(duì)象添加KVO監(jiān)聽(tīng)
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.age = 21;
self.person2.age = 22;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"監(jiān)聽(tīng)到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
@implementation Person
- (void)setAge:(int)age{
_age = age;
}
@end
上述是一段簡(jiǎn)單的 KVO
使用代碼剪勿,但如果仔細(xì)一想確實(shí)有個(gè)很大的疑惑贸诚。兩個(gè)Person
對(duì)象調(diào)用了類似的方法,其中touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
方法中厕吉,兩個(gè) person 對(duì)象賦值的本質(zhì)都是調(diào)用了Person
類中的 - (void)setAge:(int)age
方法酱固。唯一的不同點(diǎn)在于person1
添加了KVO
監(jiān)聽(tīng)。person1
會(huì)走 KVO 監(jiān)聽(tīng)回調(diào)头朱,person2
卻不走監(jiān)聽(tīng)回調(diào)运悲。
相信有不少開(kāi)發(fā)者多少了解各大概,是通過(guò)運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建子類實(shí)現(xiàn) KVO
機(jī)制项钮。但是不得不承認(rèn)班眯,蘋(píng)果的這個(gè) KVO
機(jī)制設(shè)計(jì)的非常巧妙,僅僅一行代碼便能實(shí)現(xiàn)意想不到的結(jié)果烁巫。為了更深入的了解 KVO
機(jī)制署隘,下面會(huì)結(jié)合相關(guān)底層源碼分析 KVO
機(jī)制。
二亚隙、KVO 的淺層分析
使用 LLDB 在 touchesBegan
方法內(nèi)斷點(diǎn)打印 person1
和 person2
的 isa
指針磁餐,即 p self.person1.isa
和 p self.person2.isa
,會(huì)發(fā)現(xiàn)打印結(jié)果不同。其中person1
的 isa 指針指向的類為NSKVONotifying_Person
,而 person2
的 isa 指針指向的類為Person
阿弃。(說(shuō)明: 實(shí)例對(duì)象的 isa 指向類诊霹,類的 isa 指針指向元類羞延,元類的 isa 指針指向根元類,根元類的 isa 指針指向根元類自身)脾还。
結(jié)合上述 LLDB 調(diào)試結(jié)果伴箩,可以進(jìn)一步分析兩個(gè) person
實(shí)例對(duì)象的內(nèi)存布局。首先來(lái)看鄙漏,未使用 KVO 監(jiān)聽(tīng)對(duì)象的內(nèi)存布局嗤谚,即person2
對(duì)象。person2
的 isa 指針指向 Person 的 Class 對(duì)象泥张, Person 的 Class 對(duì)象中包含 isa 指針呵恢,superclass指針鞠值,以及age屬性
對(duì)應(yīng)的的 setAge:
和 age
方法媚创。總的來(lái)說(shuō)彤恶,person2
對(duì)象的內(nèi)存布局和普通對(duì)象的內(nèi)存布局無(wú)任何特殊之處钞钙。
再來(lái)看看,使用 KVO 監(jiān)聽(tīng)對(duì)象的內(nèi)存布局声离,即 person1
對(duì)象芒炼。
通過(guò)上述 LLDB 調(diào)試可以看出 person1
的 isa 指向 NSKVONotifying_Person
, 該類是借助 runtime 動(dòng)態(tài)生成的類, NSKVONotifying_Person
實(shí)際上是 Person
的子類术徊,故 superclass
指向 Person
類本刽。 self.person1.age = 21
會(huì)調(diào)用 NSKVONotifying_Person
類中的 setAge
方法,setAge
方法中會(huì)調(diào)用 Foundation 框架中的 _NSSetIntValueAndNotify
方法 赠涮, _NSSetIntValueAndNotify
方法內(nèi)部依次調(diào)用willChangeValueForKey
子寓、 super setAge
、 didChangeValueForKey
三個(gè)方法笋除。 didChangeValueForKey
方法通知監(jiān)聽(tīng)器屬性值發(fā)生變化斜友。大概的偽代碼形式如下:
@implementation NSKVONotifying_Person
- (void)setAge:(int)age{
_NSSetIntValueAndNotify();
}
// 偽代碼
void _NSSetIntValueAndNotify(){
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
// 通知監(jiān)聽(tīng)器,某某屬性值發(fā)生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
為了證明偽代碼 _NSSetIntValueAndNotify
的內(nèi)部調(diào)用方法執(zhí)行順序垃它,可重寫(xiě) Person 類的如下方法鲜屏,并輸出 log 信息。
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge:");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
如下打印結(jié)果国拇,可證明偽代碼 _NSSetIntValueAndNotify
的實(shí)現(xiàn)步驟洛史。
willChangeValueForKey
setAge:
didChangeValueForKey - begin
監(jiān)聽(tīng)到<Person: 0x600002a8a900>的age屬性值改變了 - {
kind = 1;
new = 21;
old = 1;
} - 123
didChangeValueForKey - end
三、KVO 淺層分析驗(yàn)證
3.1 驗(yàn)證一
如果在原有工程中酱吝,創(chuàng)建NSKVONotifying_Person
類虹菲,運(yùn)行代碼會(huì)報(bào) KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
錯(cuò)誤,因?yàn)樵泄こ讨幸呀?jīng)存在該類掉瞳,故無(wú)法運(yùn)行時(shí)生成該類毕源。
3.2 驗(yàn)證二
在 person 對(duì)象調(diào)用 addObserver: forKeyPath: options: context:
方法之前和之后添加如下代碼浪漠,打印結(jié)果分別為添加KVO監(jiān)聽(tīng)之前 - Person Person
和 添加KVO監(jiān)聽(tīng)之后 - NSKVONotifying_Person Person
。
NSLog(@"添加KVO監(jiān)聽(tīng)之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));//添加KVO監(jiān)聽(tīng)之前 - Person Person
NSLog(@"添加KVO監(jiān)聽(tīng)之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));//添加KVO監(jiān)聽(tīng)之后 - NSKVONotifying_Person Person
3.3 驗(yàn)證三
在 person 對(duì)象調(diào)用 addObserver: forKeyPath: options: context:
方法之前和之后添加如下代碼,打印結(jié)果分別為 添加KVO監(jiān)聽(tīng)之前 - 0x106515c90 0x106515c90
和 添加KVO監(jiān)聽(tīng)之后 - 0x10686ffc2 0x106515c90
霎褐。
NSLog(@"添加KVO監(jiān)聽(tīng)之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSLog(@"添加KVO監(jiān)聽(tīng)之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
進(jìn)一步借助 LLDB 調(diào)試 p (IMP)0x106515c90
和 p (IMP) 0x10686ffc2
的打印結(jié)果如下:
四址愿、KVO 子類內(nèi)部方法
上圖中可以看出子類
NSKVONotifying_Person
除了重寫(xiě) setAge:
方法,還重寫(xiě)了class
冻璃、dealloc
响谓、以及_isKVOA
方法。為了證明NSKVONotifying_Person
中存在上面提到的四個(gè)方法省艳,可借助class_copyMethodList
方法打印特定類中的方法娘纷。打印結(jié)果為SKVONotifying_Person setAge:, class, dealloc, _isKVOA,
- (void)printMethodNamesOfClass:(Class)cls{
unsigned int count;
// 獲得方法數(shù)組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲(chǔ)方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[I];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
-
dealloc
方法不難理解,主要是做一些收尾工作跋炕,解決內(nèi)存問(wèn)題赖晶。 -
class
方法內(nèi)部大概實(shí)現(xiàn)是return [Person class]
。試想辐烂,如果不這樣重寫(xiě)class
方法遏插,那么 person1 在添加 KVO 監(jiān)聽(tīng)之后,調(diào)用object_getClass(object_getClass(self.person1))
或[self.person1 class]
將返回NSKVONotifying_Person
, 這顯然與實(shí)際情況不符合纠修。因此可以猜測(cè)class
方法內(nèi)部大概實(shí)現(xiàn)是return [Person class]
胳嘲,從而屏蔽了NSKVONotifying_Person
底層類的存在。
五扣草、手動(dòng)觸發(fā) KVO
KVO 的觸發(fā)條件一般是修改監(jiān)聽(tīng)對(duì)象屬性值了牛,但是如何在不修改被監(jiān)聽(tīng)屬性值的情況下觸發(fā) KVO 監(jiān)聽(tīng)回調(diào)。也就是所謂的手動(dòng)觸發(fā) KVO 辰妙,通過(guò)下面兩行代碼可手動(dòng)觸發(fā) KVO, 注意必須同時(shí)調(diào)用下面兩行代碼鹰祸。
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
由此也可以知道直接修改成員變量不會(huì)觸發(fā) KVO 監(jiān)聽(tīng)方法,因?yàn)?KVO 的本質(zhì)是重寫(xiě)了 set 方法上岗, set 方法內(nèi)部調(diào)用了willChangeValueForKey
和 didChangeValueForKey
方法福荸,直接修改成員變量并不會(huì)調(diào)用 set 方法。
另外肴掷,通過(guò) KVC 修改屬性也會(huì)觸發(fā) KVO 監(jiān)聽(tīng)敬锐,因?yàn)?KVC 中setValue:forKey:
會(huì)按照setKey
、_setKey
的順序查找方法呆瞻,setValue:forKey:
內(nèi)部調(diào)用順序如下台夺,這里不再展開(kāi)說(shuō)明。