iOS Objective-C KVO 詳解
1. KVO
KVO
即Key-Value Observing
是蘋(píng)果提供給開(kāi)發(fā)者的一套鍵值觀察的API跛溉,KVO
是一種機(jī)制,它允許將其他對(duì)象的指定屬性的更改通知給對(duì)象柄慰。KVO
是建立在KVC
的基礎(chǔ)上的踢械,對(duì)于KVC
的原理及應(yīng)用可以查看我的上一篇文章奸晴。下面我們來(lái)詳細(xì)的介紹KVO
1.1 KVO 可以觀察什么屬性贱枣?
根據(jù)KVO
官方文檔的定義监署,我們可以知道可觀察的屬性分為以下三種:
-
attributes: 簡(jiǎn)單屬性,比如基本數(shù)據(jù)類(lèi)型纽哥,字符串布爾值等钠乏,諸如
NSNumber
和其他一些不可變類(lèi)型,比如NSColor
也被認(rèn)為是簡(jiǎn)單屬性昵仅。 - to-one relationships: 一對(duì)一關(guān)系,一個(gè)屬性的值取決于另一個(gè)值累魔。比如一個(gè)人的全名由姓氏和名字組成摔笤,其中任一一個(gè)改變都會(huì)影響全名的改變;其實(shí)下載進(jìn)度也跟這個(gè)類(lèi)似垦写,下載量和總量的任一改變都會(huì)改變下載進(jìn)度吕世。
-
to-many relationships: 一對(duì)多關(guān)系,在KVO中不支持多對(duì)多的關(guān)系鍵值路徑梯投。一對(duì)多關(guān)系主要是集合對(duì)象屬性命辖,通常就是
NSArray
或者NSSet
等,但是涉及改變就是他們的可變類(lèi)型分蓖。比如有一個(gè)部門(mén)尔艇,有一個(gè)員工數(shù)組,員工有薪資屬性么鹤,部門(mén)有個(gè)總工資屬性终娃,我想監(jiān)聽(tīng)總工資的變化,其實(shí)就是監(jiān)聽(tīng)員工數(shù)組的改變蒸甜,再其改變后可以通過(guò)KVC
的數(shù)組操作符計(jì)算總工資的變化棠耕,然后手動(dòng)調(diào)用willchange
去觸發(fā)總工資改變的監(jiān)聽(tīng)
對(duì)于一對(duì)一和一對(duì)多的關(guān)系可以查看蘋(píng)果官方文檔,進(jìn)一步了解和示例代碼的查看柠新。
1.2 KVO 的三個(gè)步驟
舉個(gè)例子窍荧,如上圖所示Person
對(duì)象有個(gè)Account
屬性,而Account
對(duì)象又有balance
和interestRate
兩個(gè)屬性『拊鳎現(xiàn)在我們想實(shí)現(xiàn)一個(gè)功能:當(dāng)余額和利率變化的時(shí)候需要通知到用戶蕊退,其實(shí)用戶可以通過(guò)輪詢的方式定期去查詢Account
對(duì)象中的balance
和interestRate
,但是這種方式不僅不及時(shí)而且效率低,消耗大咕痛,更好的方式是使用KVO
痢甘,使Person
對(duì)象像收到通知一樣能及時(shí)的知道余額和利率的變動(dòng)。
另外要實(shí)現(xiàn)KVO
的前提是被觀察對(duì)象時(shí)符合KVO
機(jī)制的茉贡,一般來(lái)說(shuō)塞栅,繼承于NSObject
根類(lèi)的對(duì)象及其屬性都自動(dòng)符合KVO
機(jī)制。當(dāng)然我們也可以自己去實(shí)現(xiàn)腔丧,使其同樣符合KVO
機(jī)制放椰,這就是Manual Change Notification
(手動(dòng)變更通知),所以KVO
包含Automatic Change Notification
(自動(dòng)變更通知)和Manual Change Notification
(手動(dòng)變更通知)兩種機(jī)制愉粤。
- 首先是注冊(cè)觀察者
將觀察者實(shí)例Person
與觀察實(shí)例Account
注冊(cè)在一起砾医。Person
對(duì)每個(gè)觀察到的鍵路徑向Account
發(fā)送一個(gè)addObserver:forKeyPath:options:context:消息,將自己命名為觀察者衣厘。這里observer
(監(jiān)聽(tīng)者)如蚜、keyPath
(被監(jiān)聽(tīng)者)、options
(監(jiān)聽(tīng)策略)影暴、context
(上下文)错邦。
- 被觀察者觸發(fā)回調(diào)
為了接收Account
的變更通知,Person
需要實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:
方法型宙。Account
將在任何改變的時(shí)候想Person
發(fā)送該消息撬呢,Person
可以根據(jù)通知做出相應(yīng)的措施。
- 移除觀察
最后妆兑,當(dāng)不需要監(jiān)聽(tīng)的時(shí)候就可以通過(guò)removeObserver:forKeyPath:
方法移除監(jiān)聽(tīng)魂拦,但是移除必須在監(jiān)聽(tīng)者對(duì)象銷(xiāo)毀前執(zhí)行。
1.3 KVO
三個(gè)方法解析
1.3.1 注冊(cè)觀察者
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
-
observer: 觀察者搁嗓,一般都是
self
- keyPath: 被觀察者的屬性
-
options:
NSKeyValueObservingOptions
的組合芯勘,它指定觀察通知中會(huì)回調(diào)什么值 -
context: 上下文,這里是一個(gè)
nullable void *
類(lèi)型的參數(shù)腺逛,我們通常會(huì)傳nil
借尿,其實(shí)應(yīng)該傳NULL
,官方文檔也說(shuō)應(yīng)該傳NULL
屉来。其實(shí)這里我們可以傳一個(gè)void *
類(lèi)型的指針路翻,用來(lái)區(qū)分相同path
的不同對(duì)象的觀察。傳值示例:static void *PersonNameContext = &PersonNameContext;
NSKeyValueObservingOptions:的四個(gè)枚舉值
- NSKeyValueObservingOptionNew: 表明通知中的更改字典應(yīng)該提供新的屬性值茄靠,如果有的話茂契。
- NSKeyValueObservingOptionOld: 表明通知中的更改字典應(yīng)該包含舊的屬性值,如果有的話慨绳。
-
NSKeyValueObservingOptionInitial: 在屬性發(fā)生變化后立即通知觀察者掉冶,這個(gè)過(guò)程甚至早于觀察者注冊(cè)是時(shí)候真竖。如果在注冊(cè)的時(shí)候配置了
NSKeyValueObservingOptionNew
,那么在通知的更改字典中也會(huì)包含NSKeyValueChangeNewKey
厌小,但是不會(huì)包括NSKeyValueChangeOldKey
恢共。(在初始通知中,觀察到的屬性值可能是舊的璧亚,但是對(duì)于觀察者來(lái)說(shuō)是新的)其實(shí)簡(jiǎn)單來(lái)說(shuō)就是這個(gè)枚舉值會(huì)在屬性變化前先觸發(fā)一次observeValueForKeyPath
回調(diào)讨韭。 -
NSKeyValueObservingOptionPrior: 這個(gè)會(huì)先后連續(xù)出發(fā)兩次
observeValueForKeyPath
回調(diào)。同時(shí)在回調(diào)中的可變字典中會(huì)有一個(gè)布爾值的key - notificationIsPrior
來(lái)標(biāo)識(shí)屬性值是變化前還是變化后的癣蟋。如果是變化后的回調(diào)透硝,那么可變字典中就只有new
的值了,如果同時(shí)制定了NSKeyValueObservingOptionNew
的話疯搅。如果你需要啟動(dòng)手動(dòng)KVO
的話濒生,你可以指定這個(gè)枚舉值然后通過(guò)willChange
實(shí)例方法來(lái)觀察屬性值。在出發(fā)observeValueForKeyPath
回調(diào)后再去調(diào)用willChange
可能就太晚了幔欧。
下面我們來(lái)驗(yàn)證一下NSKeyValueObservingOptions
幾個(gè)key
會(huì)有什么樣的結(jié)果罪治。
初始實(shí)現(xiàn)代碼:
static void *PersonNameContext = &PersonNameContext;
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.person.name = @"nameA";
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@"person name change %@ - %@",self, change);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"nameB";
}
NSKeyValueObservingOptionNew:
NSKeyValueObservingOptionOld:
NSKeyValueObservingOptionInitial:
NSKeyValueObservingOptionInitial
會(huì)觸發(fā)兩次回調(diào),第一次是在屬性改變前礁蔗,第二次是在屬性改變后觉义。但是并沒(méi)有返回任何的舊值和新值。其實(shí)第一次是在我們調(diào)用addObserver:forKeyPath:
后就打印了的瘦麸。這與name
是否賦初始值沒(méi)有關(guān)系谁撼,有沒(méi)有初值都會(huì)打印歧胁。
Initial | New | Old:
此時(shí)還是觸發(fā)了兩次回調(diào)滋饲,只不過(guò)第一次返回的新值其實(shí)就是舊值,就是我們初始化時(shí)的值喊巍,第二次返回即包含了新值屠缭,也包含了舊值。其實(shí)我們包含new
在第二次就會(huì)返回新值崭参,包含old
就會(huì)返回舊值呵曹,如果不包含就不會(huì)返回。如果不包含new
第一次就不會(huì)返回新值何暮。
NSKeyValueObservingOptionPrior:
這是也是觸發(fā)了兩次回調(diào)奄喂,不過(guò)這兩次回調(diào)是在值改變后觸發(fā)的,并且第一次多返回了一個(gè)notificationIsPrior
值海洼。
Prior | New | Old:
此時(shí)還是觸發(fā)了兩次回調(diào)跨新,同樣在第一次回調(diào)中包含notificationIsPrior
值。并且第一次回調(diào)中多了舊值坏逢,第二次回調(diào)中即包含舊值也包含新值域帐。同樣我們包含new
在第二次就會(huì)返回新值赘被,包含old
就會(huì)返回舊值,如果不包含就不會(huì)返回肖揣。如果不包含old
第一次就不會(huì)返回舊值民假。
1.3.2 觀察者接收通知
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
除了change
其他參數(shù)跟上面注冊(cè)觀察時(shí)的相同。
change
包含五個(gè)key
龙优,如下:
key | value | 描述 |
---|---|---|
NSKeyValueChangeKindKey | NSNumber類(lèi)型 | 1:Setting羊异,2:Insertion,3:Removal,4:Replacement |
NSKeyValueChangeNewKey | id | 變化后的新值 |
NSKeyValueChangeOldKey | id | 變化后的舊值 |
NSKeyValueChangeIndexesKey | NSIndexSet | 插入、刪除或替換的對(duì)象的索引 |
NSKeyValueChangeNotificationIsPriorKey | NSNumber boolValue | Option為Prior時(shí)標(biāo)識(shí)屬性值是變化前和還是變化后的 |
NSKeyValueChangeKindKey對(duì)應(yīng)的枚舉:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
1.3.3 移除觀察
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
移除和注冊(cè)時(shí)一一對(duì)應(yīng)的陋率,有兩個(gè)方法一個(gè)是帶context
參數(shù)的球化,另一個(gè)是不帶的。在觀察者生命周期結(jié)束前瓦糟,一定要移除觀察筒愚,如果沒(méi)有移除,KVO
機(jī)制會(huì)給一個(gè)不存在的對(duì)象發(fā)送變化回調(diào)消息導(dǎo)致野指針錯(cuò)誤菩浙。另外也不能重復(fù)移除注冊(cè)巢掺,重復(fù)移除會(huì)導(dǎo)致crash
,當(dāng)然為了避免crash
我們可以把移除放在@try里面去執(zhí)行劲蜻。
1.4 自動(dòng)觀察與手動(dòng)觀察
默認(rèn)情況下陆淀,我們只需要按照上面的步驟就可以實(shí)現(xiàn)屬性的觀察,其實(shí)這是由系統(tǒng)完全控制的先嬉,屬于自動(dòng)觀察轧苫。其實(shí)KVO
還給我們提供了手動(dòng)觀察的選項(xiàng)。
如果我們想要開(kāi)啟手動(dòng)觀察就要通過(guò)重寫(xiě)類(lèi)方法+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
疫蔓,如果返回YES
就是自動(dòng)觀察含懊,返回NO
就是手動(dòng)觀察,根據(jù)方法的我們還可以判斷key
值對(duì)不同的key
分別實(shí)現(xiàn)自動(dòng)觀察和手動(dòng)觀察衅胀。
// 自動(dòng)開(kāi)關(guān)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
對(duì)于需要手動(dòng)觀察的key
在改變前需要調(diào)用willChangeValueForKey
方法岔乔,在改變后需要調(diào)用didChangeValueForKey
方法,如果不調(diào)用滚躯,就不會(huì)觸發(fā)KVO
的監(jiān)聽(tīng)雏门。
示例代碼:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
- 我們可以通過(guò)提前檢查是否已更改來(lái)最大程度的減少發(fā)送不必要的通知:
官方示例:
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
- 如果單個(gè)操作導(dǎo)致更改多個(gè)鍵,則必須嵌套更改通知
官方示例:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
- 對(duì)于有序的一對(duì)多關(guān)系掸掏,不僅必須指定已更改的鍵茁影,還必須指定更改的類(lèi)型和所涉及對(duì)象的索引。
- 改變的類(lèi)型的鍵值是一個(gè)
NSKeyValueChange
類(lèi)型的枚舉值丧凤,有三個(gè)分別是:NSKeyValueChangeInsertion
募闲、NSKeyValueChangeRemoval
和NSKeyValueChangeReplacement
。 - 受影響對(duì)象的索引作為NSIndexSet對(duì)象傳遞息裸。
- 改變的類(lèi)型的鍵值是一個(gè)
示例代碼:
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
1.5 Registering Dependent Keys(注冊(cè)從屬關(guān)系的鍵值)
在許多情況下蝇更,一個(gè)屬性的值取決于另一對(duì)象中一個(gè)或多個(gè)其他屬性的值沪编。如果一個(gè)屬性的值發(fā)生更改,則派生屬性的值也應(yīng)標(biāo)記為更改年扩。如何確保為這些從屬屬性發(fā)布鍵值觀察通知取決于關(guān)系的基數(shù)蚁廓。 這個(gè)在上面已經(jīng)有所提到,這里在通過(guò)舉例進(jìn)行詳細(xì)的說(shuō)明厨幻。
1.5.1 一對(duì)一關(guān)系
要自動(dòng)觸發(fā)一對(duì)一關(guān)系的通知相嵌,您應(yīng)該重寫(xiě) keyPathsForValuesAffectingValueForKey:
或?qū)崿F(xiàn)遵循其定義的用于注冊(cè)從屬鍵的模式的合適方法。
例如况脆,一個(gè)人的全名取決于名字和姓氏饭宾。返回全名的方法可以編寫(xiě)如下:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
fullName當(dāng)firstName或lastName屬性更改時(shí),必須通知觀察該屬性的應(yīng)用程序格了,因?yàn)樗鼈儠?huì)影響屬性的值看铆。
第一種方法是我們通過(guò)重寫(xiě)keyPathsForValuesAffectingValueForKey:
指定fullName
的屬性取決于lastName
和firstName
屬性。通常我們應(yīng)該調(diào)用super
并返回一個(gè)集合盛末,該集合包括這樣做所導(dǎo)致的集合中的其他任何成員免受干擾弹惦。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
第二種方法是通過(guò)實(shí)現(xiàn)遵循命名約定的類(lèi)方法keyPathsForValuesAffecting<Key>
來(lái)實(shí)現(xiàn)相同的結(jié)果,其中<Key>
是依賴值的屬性名稱(首字母大寫(xiě))悄但。實(shí)現(xiàn)代碼如下:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
對(duì)于分類(lèi)我們只能以第二種方法進(jìn)行實(shí)現(xiàn)因?yàn)槲覀儾荒茉俜诸?lèi)中覆蓋keyPathsForValuesAffectingValueForKey:
的實(shí)現(xiàn)棠隐。
1.5.2 一對(duì)多關(guān)系
keyPathsForValuesAffectingValueForKey:
方法不支持包含多對(duì)多關(guān)系的鍵路徑。那么對(duì)于這種關(guān)系的鍵值路徑我們?cè)撊绾翁幚砟兀?/p>
例如我們有個(gè)Department
(部門(mén))檐嚣,他又一個(gè)employees
(員工數(shù)組)對(duì)象助泽,部門(mén)跟員工有很多關(guān)系,但是Employee
(員工)具有salary
(薪資)屬性嚎京,這時(shí)我們希望部門(mén)有個(gè)totalSalary
(總工資)屬性嗡贺,那么這個(gè)屬性取決于員工數(shù)組中所有員工的薪資,我們也不能使用keyPathsForValuesAffectingTotalSalary
和employees.salary
作為鍵返回挖藏。
此時(shí)我們可以使用鍵值觀察將父項(xiàng)(在此示例中為Department
)注冊(cè)為所有子項(xiàng)(在此示例中為employees
)的相關(guān)屬性的觀察者暑刃。您必須作為觀察者添加和刪除父對(duì)象厢漩,因?yàn)橐陉P(guān)系中添加或刪除子對(duì)象膜眠。在該observeValueForKeyPath:ofObject:change:context:
方法中,您將響應(yīng)更改來(lái)更新從屬值溜嗜,如以下代碼片段所示:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
另外如果您使用的是Core Data
宵膨,則可以將父項(xiàng)注冊(cè)到應(yīng)用程序的通知中心,作為其托管對(duì)象上下文的觀察者炸宵。父母應(yīng)以類(lèi)似于觀察鍵值的方式響應(yīng)孩子發(fā)布的相關(guān)變更通知辟躏。
2. KVO 底層原理探索
由于KVO的實(shí)現(xiàn)并沒(méi)有開(kāi)源,我們首先看看官方文檔是怎么說(shuō)的:
Automatic key-value observing is implemented using a technique called isa-swizzling. 【譯:】自動(dòng)鍵值觀察使用的是一種叫做
isa-swizzling
的技術(shù)土全。
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.【譯:】isa
指針捎琐,顧名思義会涎,指向的是對(duì)象所屬的類(lèi),這個(gè)類(lèi)維護(hù)了一個(gè)哈希表瑞凑,這個(gè)哈希表實(shí)質(zhì)上包含指向該類(lèi)實(shí)現(xiàn)的方法的指針以及其他數(shù)據(jù)末秃。
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.【譯:】在位對(duì)象的屬性注冊(cè)觀察者時(shí),將修改觀察對(duì)象的isa
指針籽御,指向中間類(lèi)而不是真實(shí)的類(lèi)练慕,因?yàn)?code>isa的值不一定反映的是實(shí)例的實(shí)際的類(lèi)。
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.【譯:】所以我們永遠(yuǎn)不要依靠isa
指針來(lái)確定類(lèi)成員技掏,所以我們應(yīng)該使用class
方法確定對(duì)象實(shí)例的類(lèi)铃将。
2.1 中間類(lèi)(派生類(lèi))
根據(jù)官方文檔的內(nèi)容我們可以知道,在KVO
的底層實(shí)現(xiàn)中會(huì)生成一個(gè)中間類(lèi)哑梳,此時(shí)我們實(shí)例對(duì)象的isa
就指向了這個(gè)中間類(lèi)劲阎,那么我們就來(lái)驗(yàn)證一下:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
NSLog(@"注冊(cè)KVO前%@---%s", NSStringFromClass([self.person class]), object_getClassName(self.person));
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"注冊(cè)KVO后%@---%s", NSStringFromClass([self.person class]), object_getClassName(self.person));
}
打印結(jié)果如下:
通過(guò)打印結(jié)果可以看出在注冊(cè)KVO
觀察后通過(guò)Objective-C
方法打印的的類(lèi)名仍然是LGPerson
,但是在注冊(cè)后通過(guò)Runtime API
打印的確有不同了鸠真,所以說(shuō)Objective-C
方法對(duì)class
方法進(jìn)行了封裝哪工,讓我們?cè)陂_(kāi)發(fā)過(guò)程中對(duì)中間類(lèi)無(wú)感知,但是底層確實(shí)是實(shí)現(xiàn)了一個(gè)中間類(lèi)就是NSKVONotifying_xxx
弧哎。其實(shí)我們也可以通過(guò)打印對(duì)象的isa
來(lái)驗(yàn)證雁比,至此我們就驗(yàn)證了官方文檔所說(shuō)的內(nèi)容。
那么這個(gè)中間類(lèi)跟我們的類(lèi)有什么關(guān)系呢撤嫩?我們不妨打印一下類(lèi)和它的子類(lèi)來(lái)看看偎捎。
打印類(lèi)實(shí)現(xiàn)代碼:
NSLog(@"注冊(cè)KVO前");
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"注冊(cè)KVO后");
[self printClasses:[LGPerson class]];
- (void)printClasses:(Class)cls{
/// 注冊(cè)類(lèi)的總數(shù)
int count = objc_getClassList(NULL, 0);
/// 創(chuàng)建一個(gè)數(shù)組, 其中包含給定對(duì)象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
/// 獲取所有已注冊(cè)的類(lèi)
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
可以看到在LGPerson
的子類(lèi)中有這個(gè)中間類(lèi)序攘,所以說(shuō)這個(gè)中間類(lèi)是類(lèi)的子類(lèi)茴她。
2.2 KVO 觀察
我們知道KVO
是觀察屬性的變化,那么屬性的本質(zhì)是成員變量+getter
+setter
程奠,getter
是取值的丈牢,并不會(huì)修改值,值的變化發(fā)生在setter
和給成員變量賦值兩種情況瞄沙。那么我們分別測(cè)試一下這兩種情況哪一種會(huì)觸發(fā)KVO
的觀察己沛。
聲明代碼:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
驗(yàn)證代碼和結(jié)果:
通過(guò)上圖我們可以看到直接給實(shí)例變量賦值并不會(huì)觸發(fā)KVO
的監(jiān)聽(tīng),但是直接給屬性賦值就觸發(fā)了KVO
的監(jiān)聽(tīng)距境,其實(shí)給屬性賦值就是調(diào)用setter
方法申尼,所以說(shuō)KVO
底層是觀察的setter
方法。
2.3 中間類(lèi)都有哪些方法
我們分別打印原始類(lèi)和中間類(lèi)中的方法進(jìn)行查看:
實(shí)現(xiàn)代碼:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
NSLog(@"原始類(lèi)中的方法");
[self printClassAllMethod:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"派生類(lèi)中的方法");
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
}
printClassAllMethod 代碼:
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
打印結(jié)果:
我們可以看到中間類(lèi)中有屬性的setter
方法垫桂,class
方法师幕,dealloc
方法以及_isKVOA
方法。這里的setter
方法是重寫(xiě)了原始類(lèi)的方法诬滩,其余的都是重寫(xiě)的NSObject
方法霹粥。
- 對(duì)于重寫(xiě)
setter
應(yīng)該是在setter
方法中觸發(fā)監(jiān)聽(tīng)回調(diào)灭将,已經(jīng)給原始類(lèi)中屬性賦值 - 對(duì)于重寫(xiě)
class
,這里也就驗(yàn)證了我們?cè)谏厦娲蛴?code>class時(shí)為什么都是原始類(lèi)的名稱后控。這樣是為了隱藏中間類(lèi)的存在宗侦,讓開(kāi)發(fā)者在使用過(guò)程中保持一致性。 - 對(duì)于重寫(xiě)
dealloc
應(yīng)該是移除監(jiān)聽(tīng)時(shí)需要處理一些邏輯 - 對(duì)于重寫(xiě)
_isKVOA
方法應(yīng)該是返回是否是KVO
的值
2.3 isa 何時(shí)指回原始類(lèi)
其實(shí)這很容易想到忆蚀,當(dāng)我們移除所有觀察后就意味著我們不需要觀察了矾利,此時(shí)在指向中間類(lèi)也就沒(méi)什么意義了。下面我們進(jìn)行驗(yàn)證馋袜。
驗(yàn)證代碼:
- (void)dealloc{
NSLog(@"移除觀察前%@",object_getClass(self.person));
[self.person removeObserver:self forKeyPath:@"nickName"];
NSLog(@"移除觀察后%@",object_getClass(self.person));
[self printClasses:[LGPerson class]];
}
打印結(jié)果:
我們通過(guò)代碼和lldb
進(jìn)行了驗(yàn)證在移除觀察后isa
即指回了原始的類(lèi)男旗。另外我們也驗(yàn)證了指回后是否銷(xiāo)毀中間類(lèi),顯然中間類(lèi)并沒(méi)有被銷(xiāo)毀欣鳖。其實(shí)這也很正常察皇,因?yàn)閯?chuàng)建一個(gè)類(lèi)還是非常耗費(fèi)性能的,雖然移除了觀察泽台,但是也不能保證不再重新開(kāi)始觀察什荣,既然創(chuàng)建了就讓它留著吧,如果下次繼續(xù)開(kāi)始監(jiān)聽(tīng)就不用重新創(chuàng)建了怀酷,也就提高了性能稻爬。
3. 自定義KVO
至此我們就基本分析完畢了KVO
,那么我們可以自己來(lái)實(shí)現(xiàn)以下蜕依。
擱置了NΤ!样眠!
- FaceBook 的 KVOController
- 根據(jù)原生的
KVC
和KVO
反匯編而編寫(xiě)的 DIS_KVC_KVO - 開(kāi)源的
GNUStep
的libs-base
(最接近APPLE源碼的)gnustep/libs-base
4.總結(jié)
-
KVO
是蘋(píng)果提供給開(kāi)發(fā)者的一套鍵值觀察的API -
KVO
由注冊(cè)觀察者友瘤,監(jiān)聽(tīng)通知,移除觀察三個(gè)步驟組成 - 有自動(dòng)觀察和手動(dòng)觀察兩種模式
- 對(duì)于可變集合需要通過(guò)
mutableXXXValueForKey
的相關(guān)方法觸發(fā)更改 - 我們還可以注冊(cè)從屬關(guān)系的鍵值觀察檐束,
KVO
支持一對(duì)一和一對(duì)多兩種 -
KVO
本質(zhì)是isa-swizzling
技術(shù)辫秧,通過(guò)生成中間類(lèi)(派生類(lèi))來(lái)實(shí)現(xiàn)屬性的觀察 - 中間類(lèi)會(huì)重寫(xiě)屬性的
setter
方法以及重寫(xiě)class
方法,dealloc
方法和_isKVOA
方法