iOS 底層探索 - KVO

iOS 底層探索系列html

iOS 查漏補缺系列git

Objective-CCocoa 中,有許多事件之間進行通訊的方式,而且每一個都有不一樣程度的形式和耦合: NSNotification & NSNotificationCenter 提供了一箇中央樞紐,一個應用的任何部分均可能通知或者被通知應用的其餘部分的變化。惟一須要作的是要知道在尋找什麼,主要是通知的名字。例如,UIApplicationDidReceiveMemoryWarningNotification 是給應用發了一個內存不足的信號。 Key-Value Observing 鍵值觀察經過偵聽特定鍵路徑上的更改,能夠在特定對象實例之間進行特殊的事件自省。例如:一個 ProgressView 能夠觀察 網絡請求的 numberOfBytesRead 來更新它本身的 progress 屬性。 Delegate 是一個流行的傳遞事件的設計模式,經過定義一系列的方法來傳遞給指定的處理對象。例如:UIScrollView 每次它的 scroll offset 改變的時候都會發送 scrollViewDidScroll: 到它的代理 Callbacks 無論是像 NSOperation 裏的 completionBlock(當 isFinished==YES 的時候會觸發),仍是 C 裏邊的函數指針,傳遞一個函數鉤子好比 SCNetworkReachabilitySetCallback(3)github

1、KVO 初探

根據蘋果官方文檔的定義,KVO (Key Value Observing) 鍵值觀察是創建在 KVC 基礎之上的,因此若是對 KVC 不是很瞭解的讀者能夠查看上一篇 KVC 底層探索的文章。編程

我相信大多數開發者應該對於 KVO 都能熟練掌握,不過咱們仍是回顧一下官網對於 KVO 的解釋吧。設計模式

1.1 什麼是 KVO?

KVO 提供了一種當其餘對象的屬性發生變化就會通知觀察者對象的機制。根據官網的定義,屬性的分類能夠分爲下列三種:api

  • Attributes: 簡單屬性,好比基本數據類型,字符串和布爾值,而諸如 NSNumber 和其它一些不可變類型好比 NSColor 也能夠被認爲是簡單屬性
  • To-one relationships: 這些是具備本身屬性的可變對象屬性。即對象的屬性能夠更改,而無需更改對象自己。例如,一個 Account 對象可能具備一個 owner 屬性,該屬性是 Person 對象的實例,而 Person 對象自己具備 address 屬性。owner 的地址能夠更改,但卻而無需更改 Account 持有的 owner 屬性。也就是說 Accountowner 屬性未被更改,只是 address 被更改了。
  • To-many relationships: 這些是集合對象屬性。儘管也可使用自定義集合類,可是一般使用 NSArrayNSSet 的實例來持有此集合。

KVO 對於這三種屬性都能適用。下面舉一個例子:數組

如上所示,Person 對象有一個 Account 屬性,而 Account 對象又有 balanceinterestRate 兩個屬性。而且這兩個屬性對於 Person 對象來講都是可讀寫的。若是想實現一個功能:當餘額或利率變化的時候須要通知到用戶。通常來講可使用輪詢的方式,Person 對象按期從 Account 屬性中取出 balanceinterestRate。但這種方式是效率低下且不切實際的,更好的方式是使用 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 機制的最大好處你不須要本身去實現一個機制來獲取對象屬性什麼時候改變以及改變後的結果。

1.2 KVO 三大流程解析

1.2.1 觀察者註冊

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context
複製代碼

observer:註冊 KVO 通知的對象。觀察者必須實現 key-value observing 方法 observeValueForKeyPath:ofObject:change:context:keyPath:被觀察者的屬性的 keypath,相對於接受者,值不能是 niloptions: NSKeyValueObservingOptions 的組合,它指定了觀察通知中包含了什麼 context:在 observeValueForKeyPath:ofObject:change:context: 傳給 observer 參數的上下文

前兩個參數很好理解,而 optionscontext 參數則須要額外注意。

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 後觸發了兩個回調,而且一次是在屬性值變化前,一次是在屬性值變化後。同時而且沒有新值和舊值返回,咱們加一個 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld:

在咱們加上新值和舊值的枚舉以後,新值在兩次回調後被返回,可是第一次的新值實際上是最開始的屬性值,第二次纔是改變以後的屬性值,而舊值在第二次真正屬性值被改變後返回。

  • 指定 NSKeyValueObservingOptionPrior

能夠看到,NSKeyValueObservingOptionPrior 枚舉值是在屬性值發生變化後觸發了兩次回調,同時也沒有新值和舊值的返回,咱們加一個 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld:

能夠看到,在第一次回調裏沒有新值,第二次纔有,而舊值在兩次回調裏面都有。


  • keyPath 字符串問題

咱們在註冊觀察者的時候,要求傳入的 keyPath 是字符串類型,若是咱們拼寫錯誤的話,編譯器是不能幫咱們檢查出來的,全部最佳實踐應該是使用 NSStringFromSelector(SEL aSelector),好比咱們要觀察 tableViewcontentSize 屬性,咱們能夠這樣使用:

NSStringFromSelector(@selector(contentSize))
複製代碼

1.2.2 觀察者接收通知

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
複製代碼

這個方法就是觀察者接收通知的地方,除了 change 參數以外,其餘三個參數都與觀察者註冊的時候傳入的三個參數一一對應。

  • 不一樣對象監聽相同的 keypath

默認狀況下,咱們在 addObserver:forKeyPath:options:context: 方法的最後一個參數傳入的是 NULL,由於這個方法簽名中最後一個參數 contextvoid *,因此須要傳入一個空指針,而根據下圖咱們可知,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) {
    
  }
  
}

複製代碼
  • 須要本身處理 superclassobserve 事務

對於 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];
   }
}
複製代碼

1.2.3 取消註冊

- (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) {
        
    }
}
複製代碼

1.3 "自動擋" 和 "手動擋"

默認狀況下,咱們只須要按照前面說的 三步曲 的方式來實現對屬性的鍵值觀察,不過這屬因而 「自動擋」,什麼意思呢?就是說屬性值變化徹底是由系統控制,咱們只須要告訴系統監聽什麼屬性,而後就直接等系統告訴咱們就完事了。而實際上,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,它指定 NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement,受影響的對象的索引做爲 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"];
}
複製代碼

1.4 註冊從屬關係的 KVO

所謂從屬關係,指的是一個對象的某個屬性的值取決於另外一個對象的一個或多個屬性。對於不一樣類型的屬性,有不一樣的方式來實現。

  • 一對一關係

要觸發 一對一 類型屬性的自動 KVO,有兩種方式。一種是重寫 keyPathsForValuesAffectingValueForKey 方法,一種是實現一個合適的方法。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
複製代碼

好比上面的代碼,fullNamefirstNamelastName 組成,因此重寫 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 發佈的相關變動通知。

2、KVO 原理探究

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 指針,顧名思義,指向的是對象所屬的類,這個類維護了一個哈希表。這個哈希表基本上存儲的是方法的 SELIMP 的鍵值對。

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 方法來肯定對象實例所屬的類。

2.1 中間類

根據官網文檔的內容,咱們初步判斷,在 KVO 底層實現中,會有一個所謂的中間類生成。而這個中間類會讓對象的 isa 指針發生變化。咱們不妨測試一下:

如上圖所示,person 對象和 personForTest 對象都是屬於 JHPerson 類的,而 person 對象又實現了 KVO,可是在控制檯打印結果裏面能夠看到它們兩者的類都是 JHPerson 類。不是說會有一箇中間類生成嗎?難道是這個中間類生成又被幹掉了?咱們直接LLDB 大法測試一下:

Bingo~,所謂的中間類 NSKVONotifying_JHPerson 被咱們找出來了。那麼其實這裏顯然,系統是重寫了中間類 NSKVONotifying_JHPersonclass 方法,讓咱們覺得對象的 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 的子類的角色。

2.2 KVO 觀察的是什麼?

KVO 所關注的是屬性值的變化,而屬性值本質上是成員變量+getter+settergetter 是用來獲取值的,而顯然只有 setter 和成員變量賦值兩種方式能夠改變屬性值。咱們測試一下這兩種方式:

// JHPerson.h
@interface JHPerson : NSObject {
    @public
    NSString *_nickName;
}
@property (nonatomic, copy) NSString *name;
@end
複製代碼

如上圖所示,setter 方法對屬性 name 作了修改被 KVO 監聽到了,而成員變量 _nickName 的修改並無被監聽到,說明 KVO 底層其實觀察的是 setter 方法。

2.3 中間類重寫了哪些方法?

咱們能夠經過打印原始類和中間類的方法列表來驗證:

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:classdealloc_isKVOA 方法。

由咱們上一小節的測試結果可知,中間類重寫的 class 方法結果仍然是返回的是原始類,顯然系統這樣作的目的就是隱藏中間類的存在,讓調用者調用 class 方法結果先後一致。

2.4 KVO 中間類什麼時候指回去?

咱們推斷 KVO 註冊觀察者到移除觀察者這一個流程裏面,被觀察對象的 isa 指針纔會指向中間類,咱們用代碼測試一下:

由上圖可知,觀察者的 dealloc 方法中的移除觀察者以後,對象的 isa 指針已經指回了原始的類。那麼是否是此時中間類就被銷燬了呢,咱們不妨打印一下此時原始類的全部子類信息:

結果代表中間類仍然存在,也就是說移除觀察者並不會致使中間類銷燬,顯然這樣對於屢次添加和移除觀察者來講性能上更好。

2.5 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: 內部必然是調用了 observerobserveValueForKeyPath:ofObject:change:context:方法。

3、自定義 KVO 如何實現

咱們已經初步瞭解了 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
複製代碼

這裏爲了不與系統的方法衝突,因此添加了一個方法前綴。同時對於觀察策略,爲了簡化實現,這裏只聲明瞭新值和舊值兩種策略。

3.1 自定義觀察者註冊

在開始以前,咱們回憶下自定義 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];
}
複製代碼

3.2 自定義移除觀察者

咱們接着開始自定義移除觀察者,首先,咱們須要把 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);
    }
}
複製代碼

3.3 實現自動移除觀察者

如今咱們自定義的 KVO 已經能夠實現簡單的通知觀察者新值和舊值的變化了,但其實對於 api 的使用者來講,仍是要嚴格的執行 addObserverremoveObserver 的配套操做,不免有些繁瑣。雖然通常來講爲了方便起見,都是在觀察者的 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 來接收回調。

3.4 函數式編程思想重構

咱們雖然已經實現了自動的移除觀察者,可是從函數式編程思想來看,如今的設計還不是很完美,對同一個屬性的觀察的代碼散落在不一樣的地方,若是業務一旦增多,對於可讀性和可維護性都有很大的影響。因此,咱們能夠把如今這種回調的形式重構爲 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);
        }
    }
}
複製代碼

4、總結

通過探索 KVCKVO 的底層,咱們能夠看到 KVO 是創建在 KVC 基礎之上的。KVO 做爲觀察者設計模式在 iOS 中的具體落地,其原理到實現咱們都探索完了。其實咱們能夠看出來在早期設計 api 的時候,原生的 KVO 其實並很差用,因此諸如 FaceBook 的庫 KVOController 會大受歡迎。固然本文的自定義 KVO 實現並不嚴謹,感興趣的讀者能夠查看這兩個代碼庫:

咱們的 iOS 底層探索系列接下來將會進入多線程篇章,敬請期待~

參考資料

Key-Value Observing Programming Guide - Apple 官方文檔

nil/Nil/Null/NSNull - NSHipster

Key-Value Observing - NSHipster

相關文章
相關標籤/搜索