iOS 底層探索系列html
- iOS 底層探索 - alloc & init
- iOS 底層探索 - calloc 和 isa
- iOS 底層探索 - 類
- iOS 底層探索 - cache_t
- iOS 底層探索 - 方法
- iOS 底層探索 - 消息查找
- iOS 底層探索 - 消息轉發
- iOS 底層探索 - 應用加載
- iOS 底層探索 - 類的加載
- iOS 底層探索 - 分類的加載
- iOS 底層探索 - 類拓展和關聯對象
- iOS 底層探索 - KVC
- iOS 底層探索 - KVO
iOS 查漏補缺系列git
在
Objective-C
和Cocoa
中,有許多事件之間進行通訊的方式,而且每一個都有不一樣程度的形式和耦合:NSNotification
&NSNotificationCenter
提供了一箇中央樞紐,一個應用的任何部分均可能通知或者被通知應用的其餘部分的變化。惟一須要作的是要知道在尋找什麼,主要是通知的名字。例如,UIApplicationDidReceiveMemoryWarningNotification
是給應用發了一個內存不足的信號。Key-Value Observing
鍵值觀察經過偵聽特定鍵路徑上的更改,能夠在特定對象實例之間進行特殊的事件自省。例如:一個ProgressView
能夠觀察 網絡請求的numberOfBytesRead
來更新它本身的progress
屬性。Delegate
是一個流行的傳遞事件的設計模式,經過定義一系列的方法來傳遞給指定的處理對象。例如:UIScrollView
每次它的scroll offset
改變的時候都會發送scrollViewDidScroll:
到它的代理Callbacks
無論是像NSOperation
裏的completionBlock
(當isFinished==YES
的時候會觸發),仍是C
裏邊的函數指針,傳遞一個函數鉤子好比SCNetworkReachabilitySetCallback(3)
。github
根據蘋果官方文檔的定義,KVO
(Key Value Observing) 鍵值觀察是創建在 KVC
基礎之上的,因此若是對 KVC
不是很瞭解的讀者能夠查看上一篇 KVC
底層探索的文章。編程
我相信大多數開發者應該對於 KVO
都能熟練掌握,不過咱們仍是回顧一下官網對於 KVO
的解釋吧。設計模式
KVO
?KVO
提供了一種當其餘對象的屬性發生變化就會通知觀察者對象的機制。根據官網的定義,屬性的分類能夠分爲下列三種:api
NSNumber
和其它一些不可變類型好比 NSColor
也能夠被認爲是簡單屬性Account
對象可能具備一個 owner
屬性,該屬性是 Person
對象的實例,而 Person
對象自己具備 address
屬性。owner
的地址能夠更改,但卻而無需更改 Account
持有的 owner
屬性。也就是說 Account
的 owner
屬性未被更改,只是 address
被更改了。NSArray
或 NSSet
的實例來持有此集合。而 KVO
對於這三種屬性都能適用。下面舉一個例子:數組
如上所示,Person
對象有一個 Account
屬性,而 Account
對象又有 balance
和 interestRate
兩個屬性。而且這兩個屬性對於 Person
對象來講都是可讀寫的。若是想實現一個功能:當餘額或利率變化的時候須要通知到用戶。通常來講可使用輪詢的方式,Person
對象按期從 Account
屬性中取出 balance
和 interestRate
。但這種方式是效率低下且不切實際的,更好的方式是使用 KVO
,相似於餘額或利率變更時, Person
對象收到了通知同樣。安全
要實現 KVO
的前提是要確保被觀察對象是符合 KVO
機制的。通常來講,繼承於 NSObject
根類的對象及其屬性都自動符合 KVO
這一機制。固然也能夠本身去實現 KVO
符合。也就是說實際上 KVO
機制分爲自動符合和手動符合。bash
一旦肯定了對象和屬性是 KVO
符合的話,就須要歷經三個步驟:markdown
Person
對象須要將本身註冊到 Account
的某一個具體屬性上。這個過程是經過 addObserver:forKeyPath:options:context:
實現的,這個方法須要指定監聽者(observer
)、監聽誰(keypath
)、監聽策略(options
)、監聽上下文(context
)
Person
對象要接收 Account
被監聽屬性改動後發出的通知,須要自身實現 observeValueForKeyPath:ofObject:change:context:
方法來接收通知。
在觀察者不須要再監聽或自身生命週期結束的時候,須要取消註冊。具體實現是經過向被觀察對象發出 removeObserver:forKeyPath:
消息。
KVO
機制的最大好處你不須要本身去實現一個機制來獲取對象屬性什麼時候改變以及改變後的結果。
KVO
三大流程解析- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context 複製代碼
observer:註冊
KVO
通知的對象。觀察者必須實現key-value observing
方法observeValueForKeyPath:ofObject:change:context:
。 keyPath:被觀察者的屬性的keypath
,相對於接受者,值不能是nil
。 options:NSKeyValueObservingOptions
的組合,它指定了觀察通知中包含了什麼 context:在observeValueForKeyPath:ofObject:change:context:
傳給observer
參數的上下文
前兩個參數很好理解,而 options
和 context
參數則須要額外注意。
options
表明 NSKeyValueObservingOptions
的位掩碼,須要注意 NSKeyValueObservingOptionNew & NSKeyValueObservingOptionOld
,由於這些是你常常要用到的,能夠跳過 NSKeyValueObservingOptionInitial & NSKeyValueObservingOptionPrior:
。
NSKeyValueObservingOptionNew
: 代表通知中的更改字典應該提供新的屬性值,如何能夠的話。NSKeyValueObservingOptionOld
: 代表通知中的更改字典應該包含舊的屬性值,如何能夠的話。NSKeyValueObservingOptionInitial
: 這個枚舉值比較特殊,若是指定了這個枚舉值, 在屬性發生變化後當即通知觀察者,這個過程甚至早於觀察者註冊。若是在註冊的時候配置了NSKeyValueObservingOptionNew
,那麼在通知的更改字典中也會包含NSKeyValueChangeNewKey
,可是不會包括NSKeyValueChangeOldKey
。(在初始通知中,觀察到的屬性值多是舊的,可是對於觀察者來講是新的)其實簡單來講就是這個枚舉值會在屬性變化前先觸發一次observeValueForKeyPath
回調。NSKeyValueObservingOptionPrior
: 這個枚舉值會前後連續出發兩次observeValueForKeyPath
回調。同時在回調中的可變字典中會有一個布爾值的key
-notificationIsPrior
來標識屬性值是變化前仍是變化後的。若是是變化後的回調,那麼可變字典中就只有new
的值了,若是同時制定了NSKeyValueObservingOptionNew
的話。若是你須要啓動手動KVO
的話,你能夠指定這個枚舉值而後經過willChange
實例方法來觀察屬性值。在出發observeValueForKeyPath
回調後再去調用willChange
可能就太晚了。
這些選項容許一個對象在發生變化的先後獲取值。在實踐中,這不是必須的,由於從當前屬性值獲取的新值通常是可用的 也就是說
NSKeyValueObservingOptionInitial
對於在反饋KVO
事件的時候減小代碼路徑是頗有好處的。好比,若是你有一個方法,它可以動態的使一個基於text
值的按鈕有效,傳NSKeyValueObservingOptionInitial
可使事件隨着它的初始化狀態觸發一旦觀察者被添加進去的話。
如何設置一個好的
context
值呢?這裏有個建議:
static void * XXContext = &XXContext; 複製代碼
就是這麼簡單:一個靜態變量存着它本身的指針。這意味着它本身什麼也沒有,使 <NSKeyValueObserving>
更完美。
咱們簡單測試一下在註冊觀察者時指定不一樣的枚舉值會有怎麼樣的結果:
NSKeyValueObservingOptionNew
NSKeyValueObservingOptionOld
NSKeyValueObservingOptionInitial
能夠看到,只指定了 NSKeyValueObservingOptionInitial
後觸發了兩個回調,而且一次是在屬性值變化前,一次是在屬性值變化後。同時而且沒有新值和舊值返回,咱們加一個 NSKeyValueObservingOptionNew
和 NSKeyValueObservingOptionOld
:
在咱們加上新值和舊值的枚舉以後,新值在兩次回調後被返回,可是第一次的新值實際上是最開始的屬性值,第二次纔是改變以後的屬性值,而舊值在第二次真正屬性值被改變後返回。
NSKeyValueObservingOptionPrior
能夠看到,NSKeyValueObservingOptionPrior
枚舉值是在屬性值發生變化後觸發了兩次回調,同時也沒有新值和舊值的返回,咱們加一個 NSKeyValueObservingOptionNew
和 NSKeyValueObservingOptionOld
:
能夠看到,在第一次回調裏沒有新值,第二次纔有,而舊值在兩次回調裏面都有。
咱們在註冊觀察者的時候,要求傳入的 keyPath
是字符串類型,若是咱們拼寫錯誤的話,編譯器是不能幫咱們檢查出來的,全部最佳實踐應該是使用 NSStringFromSelector(SEL aSelector)
,好比咱們要觀察 tableView
的 contentSize
屬性,咱們能夠這樣使用:
NSStringFromSelector(@selector(contentSize)) 複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 複製代碼
這個方法就是觀察者接收通知的地方,除了 change
參數以外,其餘三個參數都與觀察者註冊的時候傳入的三個參數一一對應。
keypath
默認狀況下,咱們在 addObserver:forKeyPath:options:context:
方法的最後一個參數傳入的是 NULL
,由於這個方法簽名中最後一個參數 context
是 void *
,因此須要傳入一個空指針,而根據下圖咱們可知,nil
只是一個對象的字面零值,這裏須要的是一個指針,因此須要傳 NULL
。
可是若是是不一樣的對象都監聽同一屬性,咱們就須要給 context
傳入一個能夠區分不一樣對象的字符串指針:
static void *StudentNameContext = &StudentNameContext; static void *PersonNameContext = &PersonNameContext; [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext]; [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext]; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if (context == PersonNameContext) { } else if (context == StudentNameContext) { } } 複製代碼
superclass
的 observe
事務對於
Objective-C
,不少時候Runtime
系統都會自動幫助處理superclass
的方法。譬如對於dealloc
,假設類Father
繼承自NSObject
,而類Son
繼承自Father
,建立一個Son
的實例aSon
,在aSon
被釋放的時候,Runtime
會先調用Son#dealloc
,以後會自動調用Father#dealloc
,而無需在Son#dealloc
中顯式執行[super dealloc]
;。但KVO
不會這樣,因此爲了保證父類(父類可能也會本身observe
事務要處理)的observe
事務也能被處理。- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath >isEqualToString:@"contentSize"]) { [self configureView]; } else { [super observeValueForKeyPath:keyPath ofObject:object >change:change context:context]; } } 複製代碼
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; 複製代碼
取消註冊有兩個方法,不過建議仍是跟註冊和通知兩個流程統一,選用帶有 context
參數的方法。
一旦對某個對象上的屬性註冊了鍵值觀察,能夠選擇在收到屬性值變化後取消註冊,也能夠在觀察者聲明週期結束以前(好比:dealloc
方法) 取消註冊,若是忘記調用取消註冊方法,那麼一旦觀察者被銷燬後,KVO
機制會給一個不存在的對象發送變化回調消息致使野指針錯誤。
取消註冊也不能對同一個觀察者重複屢次,爲了不 crash
,能夠把取消註冊的代碼包裹在 try&catch
代碼塊中:
static void * ContentSizeContext = &ContentSizeContext; - (void)viewDidLoad { [super viewDidLoad]; // 1. subscribe [_tableView addObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) options:NSKeyValueObservingOptionNew context:ContentSizeContext]; } // 2. responding - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ContentSizeContext) { // configure view } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)dealloc { @try { // 3. unsubscribe [_tableView removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) context:ContentSizeContext]; } @catch (NSException *exception) { } } 複製代碼
默認狀況下,咱們只須要按照前面說的 三步曲
的方式來實現對屬性的鍵值觀察,不過這屬因而 「自動擋」,什麼意思呢?就是說屬性值變化徹底是由系統控制,咱們只須要告訴系統監聽什麼屬性,而後就直接等系統告訴咱們就完事了。而實際上,KVO
還支持「手動擋」。
要讓系統知道咱們想開啓手動擋,須要修改類方法 automaticallyNotifiesObserversForKey:
的返回值,這個方法若是返回 YES
就是自動擋,返回 NO
就是手動擋。同時該類方法還能精準實策,讓咱們選擇對哪些屬性是自動,哪些屬性是手動。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; } 複製代碼
一樣的,如上代碼所示,咱們使用 automaticallyNotifiesObserversForKey
的最佳實踐仍然須要把咱們須要手動或自動的代碼排除後去調用下父類的方法來確保不會有問題出現。
KVO
觸發方式// Call the accessor method. [account setName:@"Savings"]; // Use setValue:forKey:. [account setValue:@"Savings" forKey:@"name"]; // Use a key path, where 'account' is a kvc-compliant property of 'document'. [document setValue:@"Savings" forKeyPath:@"account.name"]; // Use mutableArrayValueForKey: to retrieve a relationship proxy object. Transaction *newTransaction = <#Create a new transaction for the account#>; NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction]; 複製代碼
如上代碼所示是自動 KVO
的觸發方式
KVO
觸發方式其實手動 KVO
能夠幫助咱們將多個屬性值的更改合併成一個,這樣在回調的時候就有一次了,同時也能最大程度地減小處於應用程序特定緣由而致使的通知發生。
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } 複製代碼
如上代碼所示,最樸素的手動 KVO
使用方法就是在屬性值改變前對觀察者發送 willChangeValueForKey
實例方法,在屬性值改變以後對觀察者發送 didChangeValueForKey
實例方法,參數都是所觀察的鍵。 固然,上面這種方式不是最佳的,爲了性能最佳,能夠在屬性的 setter
中判斷是否要執行 will
+ did
:
- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } } 複製代碼
可是,若是對一個屬性的改變會影響到多個鍵的話,則須要以下的操做:
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; [self willChangeValueForKey:@"itemChanged"]; _balance = theBalance; _itemChanged = _itemChanged+1; [self didChangeValueForKey:@"itemChanged"]; [self didChangeValueForKey:@"balance"]; } 複製代碼
對於有序的一對多關係屬性,不只必須指定已更改的鍵,還必須指定更改的類型和所涉及對象的索引。 更改的類型是 NSKeyValueChange
,它指定 NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或 NSKeyValueChangeReplacement
,受影響的對象的索引做爲 NSIndexSet
對象傳遞:
- (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"];
}
複製代碼
KVO
所謂從屬關係,指的是一個對象的某個屬性的值取決於另外一個對象的一個或多個屬性。對於不一樣類型的屬性,有不一樣的方式來實現。
要觸發 一對一 類型屬性的自動 KVO
,有兩種方式。一種是重寫 keyPathsForValuesAffectingValueForKey
方法,一種是實現一個合適的方法。
- (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@",firstName, lastName]; } 複製代碼
好比上面的代碼,fullName
由 firstName
和 lastName
組成,因此重寫 fullName
屬性的 getter
方法。這樣,不管是 firstName
仍是 lastName
發生了改變,監聽 fullName
屬性的觀察者都會收到通知。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } 複製代碼
如上代碼所示,經過實現類方法 keyPathsForValuesAffectingValueForKey
來返回一個集合。值得注意的是,這裏須要先對父類發送 keyPathsForValuesAffectingValueForKey
消息,以避免干擾父類中對此方法的重寫。
實際上還有一個便利的方法,就是 keyPathsForValuesAffecting<Key>
,Key
是屬性的名稱(須要首字母大寫)。這個方法的效果和 keyPathsForValuesAffectingValueForKey
是同樣的,但針對的某個具體屬性。
+ (NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"lastName", @"firstName", nil]; } 複製代碼
相對來講,在分類中去使用 keyPathsForValuesAffectingFullName
更合理,由於分類中是不容許重載方法的,因此 keyPathsForValuesAffectingValueForKey
方法確定是不能在分類中使用的。
keyPathsForValuesAffectingValueForKey
:方法不支持包含一對多關係的 Key Path
。例如,假設你有一個 Department
對象,該對象與 Employee
有一對多關係(即 employees
屬性),而 Employee
具備 salary
屬性。 若是須要在 Department
對象上增長totalSalary
屬性,而該屬性取決於關係中全部 Employees
的薪水。例如,您不能使用 keyPathsForValuesAffectingTotalSalary
和返回 employees.salary
做爲鍵來執行此操做。
- (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; } 複製代碼
如上代碼所示,將 Department
實例對象註冊爲觀察者,而後觀察對象爲 totalSalary
屬性,可是在通知回調中會手動調用 totalSalary
屬性的 setter
方法,而且傳入值是經過 KVC
的集合運算符的方式取出 employees
屬性所對應的集合中全部 sum
值之和。而後在 totalSalary
屬性的 setter
方法中,會相應的調用 willChangeValueForKey:
和 didChangeValueForKey:
方法。
若是使用的是 Core Data
,你還能夠把 Department
註冊到 NSNotificationCenter
中來做爲託管對象上下文的觀察者。Department
應以相似於觀察鍵值的方式響應 Employee
發佈的相關變動通知。
Automatic key-value observing is implemented using a technique called isa-swizzling. 【譯】自動的鍵值觀察的實現基於
isa-swizzling
。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
指針,顧名思義,指向的是對象所屬的類,這個類維護了一個哈希表。這個哈希表基本上存儲的是方法的SEL
和IMP
的鍵值對。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.
【譯】當一個觀察者註冊了對一個對象的某個屬性鍵值觀察以後,被觀察對象的
isa
指針所指向的內容發生了變化,指向了一個中間類而不是真正的類。這也致使isa
指針並不必定是指向實例所屬的真正的類。
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.
【譯】你永遠不該依靠
isa
指針來肯定類成員身份。相反,你應該使用class
方法來肯定對象實例所屬的類。
根據官網文檔的內容,咱們初步判斷,在 KVO
底層實現中,會有一個所謂的中間類生成。而這個中間類會讓對象的 isa
指針發生變化。咱們不妨測試一下:
如上圖所示,person
對象和 personForTest
對象都是屬於 JHPerson
類的,而 person
對象又實現了 KVO
,可是在控制檯打印結果裏面能夠看到它們兩者的類都是 JHPerson
類。不是說會有一箇中間類生成嗎?難道是這個中間類生成又被幹掉了?咱們直接LLDB
大法測試一下:
Bingo~,所謂的中間類 NSKVONotifying_JHPerson
被咱們找出來了。那麼其實這裏顯然,系統是重寫了中間類 NSKVONotifying_JHPerson
的 class
方法,讓咱們覺得對象的 isa
指針一直指向的都是 JHPerson
類。那麼這個中間類和原來的類是什麼關係呢?咱們能夠測試一下:
其中 printClasses
實現以下:
- (void)printClasses:(Class)cls{ int count = objc_getClassList(NULL, 0); NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls]; 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); } 複製代碼
最終打印結果以下:
classes = ( JHPerson ) classes = ( JHPerson, "NSKVONotifying_JHPerson" ) 複製代碼
結果很清晰,中間類 NSKVONotifying_JHPerson
是做爲原始真正的類 JHPerson
的子類的角色。
KVO
觀察的是什麼?KVO
所關注的是屬性值的變化,而屬性值本質上是成員變量+getter
+setter
,getter
是用來獲取值的,而顯然只有 setter
和成員變量賦值兩種方式能夠改變屬性值。咱們測試一下這兩種方式:
// JHPerson.h @interface JHPerson : NSObject { @public NSString *_nickName; } @property (nonatomic, copy) NSString *name; @end 複製代碼
如上圖所示,setter
方法對屬性 name
作了修改被 KVO
監聽到了,而成員變量 _nickName
的修改並無被監聽到,說明 KVO
底層其實觀察的是 setter
方法。
咱們能夠經過打印原始類和中間類的方法列表來驗證:
printClassAllMethod
方法實現以下:
- (void)printClassAllMethod:(Class)cls{ NSLog(@"*********************"); 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); } 複製代碼
能夠看到如上圖所示,原始類和中間類都有 setter
方法。根據咱們前面所探索的消息發送以及轉發流程,這裏的中間類應該是重寫了 setName:
、class
、 dealloc
和 _isKVOA
方法。
由咱們上一小節的測試結果可知,中間類重寫的 class
方法結果仍然是返回的是原始類,顯然系統這樣作的目的就是隱藏中間類的存在,讓調用者調用 class
方法結果先後一致。
KVO
中間類什麼時候指回去?咱們推斷 KVO
註冊觀察者到移除觀察者這一個流程裏面,被觀察對象的 isa
指針纔會指向中間類,咱們用代碼測試一下:
由上圖可知,觀察者的 dealloc
方法中的移除觀察者以後,對象的 isa
指針已經指回了原始的類。那麼是否是此時中間類就被銷燬了呢,咱們不妨打印一下此時原始類的全部子類信息:
結果代表中間類仍然存在,也就是說移除觀察者並不會致使中間類銷燬,顯然這樣對於屢次添加和移除觀察者來講性能上更好。
KVO
調用順序而咱們前面說了,有一箇中間類的存在,既然要生成中間類,確定是有意義的,咱們梳理一下整個 KVO
的流程,從註冊觀察者到觀察者的回調通知,既然有回調通知,那麼確定是在某個地方發出回調的,而因爲中間類是不能編譯的,因此咱們對中間類的父類也就是 JHPerson
類,咱們重寫一下相應的 setter
方法,咱們不妨測試一下:
// JHPerson.m - (void)setName:(NSString *)name { _name = name; } - (void)willChangeValueForKey:(NSString *)key{ [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey"); } - (void)didChangeValueForKey:(NSString *)key{ NSLog(@"didChangeValueForKey - begin"); [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey - end"); } 複製代碼
打印結果以下:
也就是說 KVO
的調用順序是:
willChangeValueForKey:
setter
實現didChangeValueForKey:
也就是說 didChangeValueForKey:
內部必然是調用了 observer
的observeValueForKeyPath:ofObject:change:context:
方法。
咱們已經初步瞭解了 KVO
底層原理,接下來咱們嘗試本身簡單實現一下 KVO
。 咱們直接跳轉到 addObserver:forKeyPath:options:context:
方法的聲明處:
能夠看到,跟 KVC
同樣,KVO
在底層也是以分類的形式加載的,這個分類叫作 NSKeyValueObserverRegistration
。咱們不妨也以這種方式來自定義實現一下 KVO
。
// NSObject+JHKVO.h @interface NSObject (JHKVO) // 觀察者註冊 - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(nullable void *)context; // 回調通知觀察者 - (void)jh_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; // 移除觀察者 - (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context; @end 複製代碼
這裏爲了不與系統的方法衝突,因此添加了一個方法前綴。同時對於觀察策略,爲了簡化實現,這裏只聲明瞭新值和舊值兩種策略。
在開始以前,咱們回憶下自定義 KVC
的時候的第一個步驟就是判斷 key
或者 keyPath
,那麼 KVO
是否也須要進行這樣的判斷呢?通過筆者實際測試,若是觀察對象的一個不存在的屬性的話,並不會報錯,也不會來到 KVO
回調方法,因而可知,判斷 keyPath
是否存在並無必要。可是,咱們回想一下上一節 KVO
底層原理,KVO
關注的是屬性的 setter
方法,那其實判斷對象所屬的類是否有這樣的 setter
就至關於同時判斷了 keyPath
是否存在。接着咱們就須要去動態的建立子類,建立子類的過程當中包括了重寫 setter
等一系列方法。而後就須要保存觀察者和 keyPath
等信息,這裏咱們藉助關聯對象來實現,咱們把傳入的觀察者對象、keyPath
和觀察策略封裝成一個新的對象存儲在關聯對象中。由於同一個對象的屬性能夠被不一樣的觀察者所觀察,因此這裏實質上是以對象數組的方式存儲在關聯對象裏面。 話很少說,直接上代碼:
// JHKVOInfo.h typedef NS_OPTIONS(NSUInteger, JHKeyValueObservingOptions) { JHKeyValueObservingOptionNew = 0x01, JHKeyValueObservingOptionOld = 0x02, }; @interface JHKVOInfo : NSObject @property (nonatomic, weak) NSObject *observer; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, assign) JHKeyValueObservingOptions options; - (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options; @end // JHKVOInfo.m @implementation JHKVOInfo - (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options { if (self = [super init]) { _observer = observer; _keyPath = keyPath; _options = options; } return self; } @end 複製代碼
上面的代碼是自定義的 JHKVOInfo
對象。
static NSString *const kJHKVOPrefix = @"JHKVONotifying_"; static NSString *const kJHKVOAssiociateKey = @"kJHKVO_AssiociateKey"; - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(void *)context { // 1.判斷 getter 是否存在 SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSString *reason = [NSString stringWithFormat:@"對象 %@ 的 key %@ 沒有 setter 實現", self, keyPath]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; return; } // 2.動態建立中間子類 Class newClass = [self createChildClassWithKeyPath:keyPath]; // 3.將對象的isa指向爲新的中間子類 object_setClass(self, newClass); // 4.保存觀察者 JHKVOInfo *info = [[JHKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options]; NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); if (!observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1]; [observerArr addObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } 複製代碼
上面的代碼是完整的添加觀察者的流程:
keyPath
對應的 setter
方法這裏的
setterForGetter
實現以下:static NSString * setterForGetter(NSString *getter) { // 判斷 getter 是否爲空字符串 if (getter.length <= 0) { return nil; } // 取出 getter 字符串的第一個字母並轉大寫 NSString *firstLetter = [[getter substringToIndex:1] uppercaseString]; // 取出剩下的字符串內容 NSString *remainingLetters = [getter substringFromIndex:1]; // 將首字母大寫的字母與剩下的字母拼接起來獲得 `set<KeyPath>` 格式的字符串 NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters]; return setter; } 複製代碼
setter
方法,那麼就建立有對應前綴的中間子類這裏的
createChildClassWithKeyPath
實現以下:- (Class)createChildClassWithKeyPath:(NSString *)keyPath{ // 得到原始類的類名 NSString *oldClassName = NSStringFromClass([self class]); // 在原始類名前添加中間子類的前綴來得到中間子類名 NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJHKVOPrefix,oldClassName]; // 經過中間子類名來判斷是否建立過 Class newClass = NSClassFromString(newClassName); // 若是建立過中間子類,直接返回 if (newClass) return newClass; // 若是沒有建立過,則須要建立一下, objc_allocateClassPair 方法的三個參數分別爲: 1.父類 2.新類的名字 3.建立新類所需額外的空間 newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0); // 註冊中間子類 objc_registerClassPair(newClass); // 從父類上拿到 `class` 方法的 `SEL` 以及類型編碼,而後在中間子類上添加一個新的子類實現 `jh_class` SEL classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)jh_class, classTypes); // 從父類上拿到 `getter` 方法的 `SEL` 以及類型編碼,而後在中間子類上添加一個新的子類實現 `jh_setter` SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP)jh_setter, setterTypes); return newClass; } 複製代碼
jh_class
的實現以下:Class jh_class(id self,SEL _cmd) { // 經過 class_getSuperclass 來返回父類的 `Class`,達到對調用者隱藏中間子類的效果 return class_getSuperclass(object_getClass(self)); } 複製代碼
jh_setter
的實現以下:static void jh_setter(id self,SEL _cmd,id newValue){ // 由於 `_cmd` 做爲方法的第二個參數其實就是 `setter` 的 `SEL`,這裏反向得到對應 `getter` 字符串形式做爲 `keyPath`,而後經過 `KVC` 來獲取到舊的屬性值 NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; // 由於是重寫父類的 `setter`,因此還須要經過消息發送的方式手動執行如下父類的 `setter` 方法 // 經過強轉的方式將 `objc_msgSendSuper` 轉成 `jh_msgSendSuper` 函數指針,同時,因爲 `objc_msgSendSuper` 要比咱們常見的 `objc_msgSend` 多一個父類結構體參數,因此須要手動構建一下這個父類結構體,結構體有兩個屬性,分別是實例對象以及實例對象的類的父類 void (*jh_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper; // void /* struct objc_super *super, SEL op, ... */ struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; // 準備工做完成後手動調用 `jh_msgSendSuper`,由於 `superStruct` 是結構體類型,而 `jh_msgSendSuper` 的第一個參數是空指針對象,因此這裏須要加取地址符來把結構體地址賦值給指針對象 jh_msgSendSuper(&superStruct, _cmd, newValue); // 調用完父類的 `setter` 以後,從關聯對象中取出存儲了自定義的對象數組 NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); // 循環遍歷自定義的對象 for (JHKVOInfo *info in observerArr) { // 若是 `keyPath` 匹配則進入下一步 if ([info.keyPath isEqualToString:keyPath]) { // 基於線程安全的考慮,使用 `GCD` 的全局隊列異步執行下面的操做 dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 初始化一個通知字典 NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; // 判斷存儲的觀察策略,若是是新值,則在通知字典中設置新值 if (info.options & JHKeyValueObservingOptionNew) { [change setObject:newValue forKey:NSKeyValueChangeNewKey]; } // 若是是舊值,在通知字典中設置舊值 if (info.options & JHKeyValueObservingOptionOld) { [change setObject:@"" forKey:NSKeyValueChangeOldKey]; if (oldValue) { [change setObject:oldValue forKey:NSKeyValueChangeOldKey]; } } // 取得通知觀察者方法的 `SEL` SEL observerSEL = @selector(jh_observeValueForKeyPath:ofObject:change:context:); // 經過 `objc_msgSend` 手動發送消息,達到觀察者收到回調的效果 ((void(*)(id, SEL, id, id, NSMutableDictionary *, void *))objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL); }); } } } 複製代碼
getterForSetter
實現以下:static NSString *getterForSetter(NSString *setter){ // 判斷傳入的 `setter` 字符串長度是否大於 0,以及是否有 `set` 的前綴和 `:` 的後綴 if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;} // 排除掉 `setter` 字符串中的 `set:` 部分以取得 getter 字符串 NSRange range = NSMakeRange(3, setter.length-4); NSString *getter = [setter substringWithRange:range]; // 對 getter 字符串首字母小寫處理 NSString *firstString = [[getter substringToIndex:1] lowercaseString]; return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString]; } 複製代碼
咱們接着開始自定義移除觀察者,首先,咱們須要把 isa
指回原來的類,而後須要對關聯對象中存儲的自定義對象數組對應的觀察者移除掉。
- (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context { // 從關聯對象中取出數組 NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); // 若是數組中沒有內容,說明沒有添加過觀察者,那麼直接返回 if (observerArr.count<=0) { return; } // 遍歷取出的全部自定義對象 for (JHKVOInfo *info in observerArr) { // 若是 `keyPath` 匹配上了 則從數組中移除響應對象,而後存儲最新的數組到關聯對象上 if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; } } // 要將 `isa` 指回原來的類的前提條件是,被觀察屬性的對象已經沒有任何觀察者在觀察了,那麼就須要指回去 if (observerArr.count<=0) { Class superClass = [self class]; object_setClass(self, superClass); } } 複製代碼
如今咱們自定義的 KVO
已經能夠實現簡單的通知觀察者新值和舊值的變化了,但其實對於 api
的使用者來講,仍是要嚴格的執行 addObserver
和 removeObserver
的配套操做,不免有些繁瑣。雖然通常來講爲了方便起見,都是在觀察者的 dealloc
方法中去手動調用 removeObserver
方法,但仍是太麻煩了。所以,咱們能夠藉助 methodSwizzling
的技術來替換默認 dealloc
方法的實現,直接上代碼:
+ (BOOL)jh_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL { // 獲取 Class 對象 Class cls = self; // 經過 `SEL` 獲取原始方法 Method oriMethod = class_getInstanceMethod(cls, oriSEL); // 經過 `SEL` 獲取要替換的方法 Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); // 若是要替換的方法不存在,返回 NO if (!swiMethod) { return NO; } // 若是原始方法不存在,那麼就直接在 Class 上添加要替換的方法,注意,添加的方法實現爲要替換的方法,可是方法 `SEL` 仍是原始方法的 `SEL` if (!oriMethod) { class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ })); } // 判斷是否添加成功 BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); if (didAddMethod) { // 若是成功,說明 Class 上已經存在了要替換的方法的實現,那麼就把原始方法實現替換掉 `swizzledSEL` 對應的方法實現 class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{ // 若是不成功,說明原始方法已經存在,則直接交換方法實現 method_exchangeImplementations(oriMethod, swiMethod); } return YES; } + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self jh_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)]; }); } - (void)myDealloc{ Class superClass = [self class]; object_setClass(self, superClass); // 這裏並不會形成循環引用的遞歸,由於 `myDealloc` 的方法實現是真正的原始 `dealloc` [self myDealloc]; } 複製代碼
經過實現自動移除觀察者,api
的使用者能夠徹底放心的只使用 addObserver
來添加觀察者以及 observeValueForKeyPath
來接收回調。
咱們雖然已經實現了自動的移除觀察者,可是從函數式編程思想來看,如今的設計還不是很完美,對同一個屬性的觀察的代碼散落在不一樣的地方,若是業務一旦增多,對於可讀性和可維護性都有很大的影響。因此,咱們能夠把如今這種回調的形式重構爲 Block
的方式。
// NSObject+JHBlockKVO.h typedef void(^JHKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue); @interface NSObject (JHBlockKVO) - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JHKVOBlock)block; - (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; @end // NSObject+JHBlockKVO.m @interface JHBlockKVOInfo : NSObject @property (nonatomic, weak) NSObject *observer; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, copy) JHKVOBlock handleBlock; @end @implementation JHBlockKVOInfo - (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(JHKVOBlock)block{ if (self=[super init]) { _observer = observer; _keyPath = keyPath; _handleBlock = block; } return self; } @end @implementation NSObject (JHBlockKVO) - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JHKVOBlock)block{ // 1.判斷 getter 是否存在 SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSString *reason = [NSString stringWithFormat:@"對象 %@ 的 key %@ 沒有 setter 實現", self, keyPath]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; return; } // 2.動態建立中間子類 Class newClass = [self createChildClassWithKeyPath:keyPath]; // 3.將對象的isa指向爲新的中間子類 object_setClass(self, newClass); // 4.保存觀察者 JHBlockKVOInfo *info = [[JHBlockKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block]; NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); if (!mArray) { mArray = [NSMutableArray arrayWithCapacity:1]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [mArray addObject:info]; } 複製代碼
這裏咱們直接經過傳入分類一個 block
,而後存儲在對應的自定義觀察對象中,而後咱們還須要在重寫 setter
方法中作出修改,原來是直接經過發送消息來實現回調,如今須要改爲 block
回調
static void jh_setter(id self,SEL _cmd,id newValue){ NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; void (*jh_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper; struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; jh_msgSendSuper(&superStruct,_cmd,newValue); NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); for (JHBlockKVOInfo *info in mArray) { if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); } } } 複製代碼
通過探索 KVC
和 KVO
的底層,咱們能夠看到 KVO
是創建在 KVC
基礎之上的。KVO
做爲觀察者設計模式在 iOS
中的具體落地,其原理到實現咱們都探索完了。其實咱們能夠看出來在早期設計 api
的時候,原生的 KVO
其實並很差用,因此諸如 FaceBook
的庫 KVOController 會大受歡迎。固然本文的自定義 KVO
實現並不嚴謹,感興趣的讀者能夠查看這兩個代碼庫:
KVC
和 KVO
反彙編而成 DIS_KVC_KVOGNUStep
的 libs-base
gnustep/libs-base咱們的 iOS
底層探索系列接下來將會進入多線程篇章,敬請期待~
Key-Value Observing Programming Guide - Apple 官方文檔