iOS 底層探索 - KVO

iOS 底層探索系列

Objective-CCocoa 中,有許多事件之間進(jìn)行通信的方式,并且每個(gè)都有不同程度的形式和耦合:
NSNotification & NSNotificationCenter 提供了一個(gè)中央樞紐虏冻,一個(gè)應(yīng)用的任何部分都可能通知或者被通知應(yīng)用的其他部分的變化擦剑。唯一需要做的是要知道在尋找什么,主要是通知的名字。例如返顺,UIApplicationDidReceiveMemoryWarningNotification 是給應(yīng)用發(fā)了一個(gè)內(nèi)存不足的信號(hào)鹅搪。
Key-Value Observing 鍵值觀察通過偵聽特定鍵路徑上的更改站绪,可以在特定對(duì)象實(shí)例之間進(jìn)行特殊的事件自省。例如:一個(gè) ProgressView 可以觀察 網(wǎng)絡(luò)請(qǐng)求的 numberOfBytesRead 來更新它自己的 progress 屬性丽柿。
Delegate 是一個(gè)流行的傳遞事件的設(shè)計(jì)模式恢准,通過定義一系列的方法來傳遞給指定的處理對(duì)象。例如:UIScrollView 每次它的 scroll offset 改變的時(shí)候都會(huì)發(fā)送 scrollViewDidScroll: 到它的代理
Callbacks 不管是像 NSOperation 里的 completionBlock(當(dāng) isFinished==YES 的時(shí)候會(huì)觸發(fā))甫题,還是 C 里邊的函數(shù)指針馁筐,傳遞一個(gè)函數(shù)鉤子比如 SCNetworkReachabilitySetCallback(3)

一坠非、KVO 初探

根據(jù)蘋果官方文檔的定義敏沉,KVO (Key Value Observing) 鍵值觀察是建立在 KVC 基礎(chǔ)之上的,所以如果對(duì) KVC 不是很了解的讀者可以查看上一篇 KVC 底層探索的文章麻顶。

我相信大多數(shù)開發(fā)者應(yīng)該對(duì)于 KVO 都能熟練掌握赦抖,不過我們還是回顧一下官網(wǎng)對(duì)于 KVO 的解釋吧。

1.1 什么是 KVO?

KVO 提供了一種當(dāng)其他對(duì)象的屬性發(fā)生變化就會(huì)通知觀察者對(duì)象的機(jī)制辅肾。根據(jù)官網(wǎng)的定義队萤,屬性的分類可以分為下列三種:

  • Attributes: 簡單屬性,比如基本數(shù)據(jù)類型矫钓,字符串和布爾值要尔,而諸如 NSNumber 和其它一些不可變類型比如 NSColor 也可以被認(rèn)為是簡單屬性
  • To-one relationships: 這些是具有自己屬性的可變對(duì)象屬性舍杜。即對(duì)象的屬性可以更改,而無需更改對(duì)象本身赵辕。例如既绩,一個(gè) Account 對(duì)象可能具有一個(gè) owner 屬性,該屬性是 Person 對(duì)象的實(shí)例还惠,而 Person 對(duì)象本身具有 address 屬性饲握。owner 的地址可以更改,但卻而無需更改 Account 持有的 owner 屬性蚕键。也就是說 Accountowner 屬性未被更改救欧,只是 address 被更改了。
  • To-many relationships: 這些是集合對(duì)象屬性锣光。盡管也可以使用自定義集合類笆怠,但是通常使用 NSArrayNSSet 的實(shí)例來持有此集合。

KVO 對(duì)于這三種屬性都能適用誊爹。下面舉一個(gè)例子:

image

如上所示蹬刷,Person 對(duì)象有一個(gè) Account 屬性,而 Account 對(duì)象又有 balanceinterestRate 兩個(gè)屬性频丘。并且這兩個(gè)屬性對(duì)于 Person 對(duì)象來說都是可讀寫的办成。如果想實(shí)現(xiàn)一個(gè)功能:當(dāng)余額或利率變化的時(shí)候需要通知到用戶。一般來說可以使用輪詢的方式椎镣,Person 對(duì)象定期從 Account 屬性中取出 balanceinterestRate诈火。但這種方式是效率低下且不切實(shí)際的,更好的方式是使用 KVO状答,類似于余額或利率變動(dòng)時(shí)冷守, Person 對(duì)象收到了通知一樣。

要實(shí)現(xiàn) KVO 的前提是要確保被觀察對(duì)象是符合 KVO 機(jī)制的惊科。一般來說拍摇,繼承于 NSObject 根類的對(duì)象及其屬性都自動(dòng)符合 KVO 這一機(jī)制。當(dāng)然也可以自己去實(shí)現(xiàn) KVO 符合馆截。也就是說實(shí)際上 KVO 機(jī)制分為自動(dòng)符合手動(dòng)符合充活。

一旦確定了對(duì)象和屬性是 KVO 符合的話,就需要?dú)v經(jīng)三個(gè)步驟:

  • 觀察者注冊(cè)
image

Person 對(duì)象需要將自己注冊(cè)到 Account 的某一個(gè)具體屬性上蜡娶。這個(gè)過程是通過
addObserver:forKeyPath:options:context: 實(shí)現(xiàn)的混卵,這個(gè)方法需要指定監(jiān)聽者(observer)、監(jiān)聽誰(keypath)窖张、監(jiān)聽策略(options)幕随、監(jiān)聽上下文(context)

  • 被觀察者觸發(fā)回調(diào)
image

Person 對(duì)象要接收 Account 被監(jiān)聽屬性改動(dòng)后發(fā)出的通知,需要自身實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法來接收通知宿接。

  • 觀察者取消注冊(cè)
image

在觀察者不需要再監(jiān)聽或自身生命周期結(jié)束的時(shí)候赘淮,需要取消注冊(cè)辕录。具體實(shí)現(xiàn)是通過向被觀察對(duì)象發(fā)出 removeObserver:forKeyPath: 消息。

KVO 機(jī)制的最大好處你不需要自己去實(shí)現(xiàn)一個(gè)機(jī)制來獲取對(duì)象屬性何時(shí)改變以及改變后的結(jié)果梢卸。

1.2 KVO 三大流程解析

1.2.1 觀察者注冊(cè)

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

observer:注冊(cè) KVO 通知的對(duì)象走诞。觀察者必須實(shí)現(xiàn) key-value observing 方法 observeValueForKeyPath:ofObject:change:context:
keyPath:被觀察者的屬性的 keypath蛤高,相對(duì)于接受者蚣旱,值不能是 nil
options: NSKeyValueObservingOptions 的組合戴陡,它指定了觀察通知中包含了什么
context:在 observeValueForKeyPath:ofObject:change:context: 傳給 observer 參數(shù)的上下文

前兩個(gè)參數(shù)很好理解姻锁,而 optionscontext 參數(shù)則需要額外注意。

options 代表 NSKeyValueObservingOptions 的位掩碼猜欺,需要注意 NSKeyValueObservingOptionNew & NSKeyValueObservingOptionOld,因?yàn)檫@些是你經(jīng)常要用到的拷窜,可以跳過 NSKeyValueObservingOptionInitial & NSKeyValueObservingOptionPrior:开皿。

NSKeyValueObservingOptionNew: 表明通知中的更改字典應(yīng)該提供新的屬性值,如何可以的話篮昧。
NSKeyValueObservingOptionOld: 表明通知中的更改字典應(yīng)該包含舊的屬性值赋荆,如何可以的話。
NSKeyValueObservingOptionInitial: 這個(gè)枚舉值比較特殊懊昨,如果指定了這個(gè)枚舉值窄潭,
在屬性發(fā)生變化后立即通知觀察者,這個(gè)過程甚至早于觀察者注冊(cè)酵颁。如果在注冊(cè)的時(shí)候配置了 NSKeyValueObservingOptionNew嫉你,那么在通知的更改字典中也會(huì)包含 NSKeyValueChangeNewKey,但是不會(huì)包括 NSKeyValueChangeOldKey躏惋。(在初始通知中幽污,觀察到的屬性值可能是舊的,但是對(duì)于觀察者來說是新的)其實(shí)簡單來說就是這個(gè)枚舉值會(huì)在屬性變化前先觸發(fā)一次 observeValueForKeyPath 回調(diào)簿姨。
NSKeyValueObservingOptionPrior: 這個(gè)枚舉值會(huì)先后連續(xù)出發(fā)兩次 observeValueForKeyPath 回調(diào)距误。同時(shí)在回調(diào)中的可變字典中會(huì)有一個(gè)布爾值的 key - notificationIsPrior 來標(biāo)識(shí)屬性值是變化前還是變化后的。如果是變化后的回調(diào)扁位,那么可變字典中就只有 new 的值了准潭,如果同時(shí)制定了 NSKeyValueObservingOptionNew 的話。如果你需要啟動(dòng)手動(dòng) KVO 的話域仇,你可以指定這個(gè)枚舉值然后通過 willChange 實(shí)例方法來觀察屬性值刑然。在出發(fā) observeValueForKeyPath 回調(diào)后再去調(diào)用 willChange 可能就太晚了。

這些選項(xiàng)允許一個(gè)對(duì)象在發(fā)生變化的前后獲取值殉簸。在實(shí)踐中闰集,這不是必須的沽讹,因?yàn)閺漠?dāng)前屬性值獲取的新值一般是可用的 也就是說 NSKeyValueObservingOptionInitial 對(duì)于在反饋 KVO 事件的時(shí)候減少代碼路徑是很有好處的。比如武鲁,如果你有一個(gè)方法爽雄,它能夠動(dòng)態(tài)的使一個(gè)基于 text 值的按鈕有效,傳 NSKeyValueObservingOptionInitial 可以使事件隨著它的初始化狀態(tài)觸發(fā)一旦觀察者被添加進(jìn)去的話沐鼠。

如何設(shè)置一個(gè)好的 context 值呢挚瘟?這里有個(gè)建議:

static void * XXContext = &XXContext;

就是這么簡單:一個(gè)靜態(tài)變量存著它自己的指針。這意味著它自己什么也沒有饲梭,使 <NSKeyValueObserving> 更完美乘盖。

我們簡單測(cè)試一下在注冊(cè)觀察者時(shí)指定不同的枚舉值會(huì)有怎么樣的結(jié)果:

  • 只指定 NSKeyValueObservingOptionNew
image
  • 只指定 NSKeyValueObservingOptionOld
image
  • 指定 NSKeyValueObservingOptionInitial
image

可以看到,只指定了 NSKeyValueObservingOptionInitial 后觸發(fā)了兩個(gè)回調(diào)憔涉,并且一次是在屬性值變化前订框,一次是在屬性值變化后。同時(shí)并且沒有新值和舊值返回兜叨,我們加一個(gè) NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld:

image

在我們加上新值和舊值的枚舉之后穿扳,新值在兩次回調(diào)后被返回,但是第一次的新值其實(shí)是最開始的屬性值国旷,第二次才是改變之后的屬性值矛物,而舊值在第二次真正屬性值被改變后返回。

  • 指定 NSKeyValueObservingOptionPrior
image

可以看到跪但,NSKeyValueObservingOptionPrior 枚舉值是在屬性值發(fā)生變化后觸發(fā)了兩次回調(diào)履羞,同時(shí)也沒有新值和舊值的返回,我們加一個(gè) NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld:

image

可以看到屡久,在第一次回調(diào)里沒有新值忆首,第二次才有,而舊值在兩次回調(diào)里面都有涂身。


  • keyPath 字符串問題

我們?cè)谧?cè)觀察者的時(shí)候雄卷,要求傳入的 keyPath 是字符串類型,如果我們拼寫錯(cuò)誤的話蛤售,編譯器是不能幫我們檢查出來的丁鹉,所有最佳實(shí)踐應(yīng)該是使用 NSStringFromSelector(SEL aSelector),比如我們要觀察 tableViewcontentSize 屬性悴能,我們可以這樣使用:

NSStringFromSelector(@selector(contentSize))

1.2.2 觀察者接收通知

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

這個(gè)方法就是觀察者接收通知的地方揣钦,除了 change 參數(shù)之外,其他三個(gè)參數(shù)都與觀察者注冊(cè)的時(shí)候傳入的三個(gè)參數(shù)一一對(duì)應(yīng)漠酿。

  • 不同對(duì)象監(jiān)聽相同的 keypath

默認(rèn)情況下冯凹,我們?cè)?addObserver:forKeyPath:options:context: 方法的最后一個(gè)參數(shù)傳入的是 NULL,因?yàn)檫@個(gè)方法簽名中最后一個(gè)參數(shù) contextvoid *,所以需要傳入一個(gè)空指針宇姚,而根據(jù)下圖我們可知缀壤,nil 只是一個(gè)對(duì)象的字面零值仪际,這里需要的是一個(gè)指針,所以需要傳 NULL

image

但是如果是不同的對(duì)象都監(jiān)聽同一屬性赴叹,我們就需要給 context 傳入一個(gè)可以區(qū)分不同對(duì)象的字符串指針:

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 事務(wù)

對(duì)于 Objective-C呛踊,很多時(shí)候 Runtime 系統(tǒng)都會(huì)自動(dòng)幫助處理 superclass 的方法萍诱。譬如對(duì)于 dealloc瘦麸,假設(shè)類 Father 繼承自 NSObject,而類 Son 繼承自Father蒜绽,創(chuàng)建一個(gè) Son 的實(shí)例 aSon镶骗,在 aSon 被釋放的時(shí)候,Runtime 會(huì)先調(diào)用 Son#dealloc躲雅,之后會(huì)自動(dòng)調(diào)用 Father#dealloc鼎姊,而無需在 Son#dealloc 中顯式執(zhí)行 [super dealloc];。但 KVO 不會(huì)這樣相赁,所以為了保證父類(父類可能也會(huì)自己 observe 事務(wù)要處理)的 observe 事務(wù)也能被處理此蜈。

- (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 取消注冊(cè)

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

取消注冊(cè)有兩個(gè)方法,不過建議還是跟注冊(cè)和通知兩個(gè)流程統(tǒng)一噪生,選用帶有 context 參數(shù)的方法。

  • 取消注冊(cè)與注冊(cè)是一對(duì)一的關(guān)系

一旦對(duì)某個(gè)對(duì)象上的屬性注冊(cè)了鍵值觀察东囚,可以選擇在收到屬性值變化后取消注冊(cè)跺嗽,也可以在觀察者聲明周期結(jié)束之前(比如:dealloc 方法) 取消注冊(cè),如果忘記調(diào)用取消注冊(cè)方法页藻,那么一旦觀察者被銷毀后桨嫁,KVO 機(jī)制會(huì)給一個(gè)不存在的對(duì)象發(fā)送變化回調(diào)消息導(dǎo)致野指針錯(cuò)誤。

  • 不能重復(fù)取消注冊(cè)

取消注冊(cè)也不能對(duì)同一個(gè)觀察者重復(fù)多次份帐,為了避免 crash璃吧,可以把取消注冊(cè)的代碼包裹在 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 "自動(dòng)擋" 和 "手動(dòng)擋"

默認(rèn)情況下,我們只需要按照前面說的 三步曲 的方式來實(shí)現(xiàn)對(duì)屬性的鍵值觀察废境,不過這屬于是 「自動(dòng)擋」畜挨,什么意思呢?就是說屬性值變化完全是由系統(tǒng)控制噩凹,我們只需要告訴系統(tǒng)監(jiān)聽什么屬性巴元,然后就直接等系統(tǒng)告訴我們就完事了。而實(shí)際上驮宴,KVO 還支持「手動(dòng)擋」逮刨。

要讓系統(tǒng)知道我們想開啟手動(dòng)擋,需要修改類方法 automaticallyNotifiesObserversForKey: 的返回值堵泽,這個(gè)方法如果返回 YES 就是自動(dòng)擋修己,返回 NO 就是手動(dòng)擋恢总。同時(shí)該類方法還能精準(zhǔn)實(shí)策,讓我們選擇對(duì)哪些屬性是自動(dòng)睬愤,哪些屬性是手動(dòng)片仿。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

同樣的,如上代碼所示戴涝,我們使用 automaticallyNotifiesObserversForKey 的最佳實(shí)踐仍然需要把我們需要手動(dòng)或自動(dòng)的代碼排除后去調(diào)用下父類的方法來確保不會(huì)有問題出現(xiàn)滋戳。

  • 自動(dòng) KVO 觸發(fā)方式
// 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];

如上代碼所示是自動(dòng) KVO 的觸發(fā)方式

  • 手動(dòng) KVO 觸發(fā)方式

其實(shí)手動(dòng) KVO 可以幫助我們將多個(gè)屬性值的更改合并成一個(gè),這樣在回調(diào)的時(shí)候就有一次了啥刻,同時(shí)也能最大程度地減少處于應(yīng)用程序特定原因而導(dǎo)致的通知發(fā)生奸鸯。

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

如上代碼所示,最樸素的手動(dòng) KVO 使用方法就是在屬性值改變前對(duì)觀察者發(fā)送 willChangeValueForKey 實(shí)例方法可帽,在屬性值改變之后對(duì)觀察者發(fā)送 didChangeValueForKey 實(shí)例方法娄涩,參數(shù)都是所觀察的鍵。
當(dāng)然映跟,上面這種方式不是最佳的蓄拣,為了性能最佳,可以在屬性的 setter 中判斷是否要執(zhí)行 will + did:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

但是努隙,如果對(duì)一個(gè)屬性的改變會(huì)影響到多個(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)系屬性,不僅必須指定已更改的鍵荸镊,還必須指定更改的類型和所涉及對(duì)象的索引咽斧。 更改的類型是 NSKeyValueChange,它指定 NSKeyValueChangeInsertion躬存,NSKeyValueChangeRemovalNSKeyValueChangeReplacement张惹,受影響的對(duì)象的索引作為 NSIndexSet 對(duì)象傳遞:

- (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 注冊(cè)從屬關(guān)系的 KVO

所謂從屬關(guān)系,指的是一個(gè)對(duì)象的某個(gè)屬性的值取決于另一個(gè)對(duì)象的一個(gè)或多個(gè)屬性岭洲。對(duì)于不同類型的屬性宛逗,有不同的方式來實(shí)現(xiàn)。

  • 一對(duì)一關(guān)系

要觸發(fā) 一對(duì)一 類型屬性的自動(dòng) KVO盾剩,有兩種方式雷激。一種是重寫 keyPathsForValuesAffectingValueForKey 方法,一種是實(shí)現(xiàn)一個(gè)合適的方法告私。

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

比如上面的代碼侥锦,fullNamefirstNamelastName 組成,所以重寫 fullName 屬性的 getter 方法德挣。這樣恭垦,不論是 firstName 還是 lastName 發(fā)生了改變,監(jiān)聽 fullName 屬性的觀察者都會(huì)收到通知。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

如上代碼所示番挺,通過實(shí)現(xiàn)類方法 keyPathsForValuesAffectingValueForKey 來返回一個(gè)集合唠帝。值得注意的是,這里需要先對(duì)父類發(fā)送 keyPathsForValuesAffectingValueForKey 消息玄柏,以免干擾父類中對(duì)此方法的重寫襟衰。

實(shí)際上還有一個(gè)便利的方法,就是 keyPathsForValuesAffecting<Key>粪摘,Key 是屬性的名稱(需要首字母大寫)瀑晒。這個(gè)方法的效果和 keyPathsForValuesAffectingValueForKey 是一樣的,但針對(duì)的某個(gè)具體屬性徘意。

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

相對(duì)來說苔悦,在分類中去使用 keyPathsForValuesAffectingFullName 更合理,因?yàn)榉诸愔惺遣辉试S重載方法的椎咧,所以 keyPathsForValuesAffectingValueForKey 方法肯定是不能在分類中使用的玖详。


  • 一對(duì)多關(guān)系

keyPathsForValuesAffectingValueForKey:方法不支持包含一對(duì)多關(guān)系的 Key Path。例如勤讽,假設(shè)你有一個(gè) Department對(duì)象蟋座,該對(duì)象與 Employee 有一對(duì)多關(guān)系(即 employees 屬性),而 Employee 具有 salary 屬性脚牍。 如果需要在 Department 對(duì)象上增加totalSalary 屬性向臀,而該屬性取決于關(guān)系中所有 Employees 的薪水。例如诸狭,您不能使用 keyPathsForValuesAffectingTotalSalary 和返回 employees.salary 作為鍵來執(zhí)行此操作飒硅。

- (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 實(shí)例對(duì)象注冊(cè)為觀察者作谚,然后觀察對(duì)象為 totalSalary 屬性,但是在通知回調(diào)中會(huì)手動(dòng)調(diào)用 totalSalary 屬性的 setter 方法庵芭,并且傳入值是通過 KVC 的集合運(yùn)算符的方式取出 employees 屬性所對(duì)應(yīng)的集合中所有 sum 值之和妹懒。然后在 totalSalary 屬性的 setter 方法中,會(huì)相應(yīng)的調(diào)用 willChangeValueForKey:didChangeValueForKey: 方法双吆。

如果使用的是 Core Data眨唬,你還可以把 Department 注冊(cè)到 NSNotificationCenter 中來作為托管對(duì)象上下文的觀察者。Department 應(yīng)以類似于觀察鍵值的方式響應(yīng) Employee 發(fā)布的相關(guān)變更通知好乐。

二匾竿、KVO 原理探究

Automatic key-value observing is implemented using a technique called isa-swizzling.
【譯】自動(dòng)的鍵值觀察的實(shí)現(xiàn)基于 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 指針蔚万,顧名思義岭妖,指向的是對(duì)象所屬的類,這個(gè)類維護(hù)了一個(gè)哈希表。這個(gè)哈希表基本上存儲(chǔ)的是方法的 SELIMP 的鍵值對(duì)昵慌。

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.

【譯】當(dāng)一個(gè)觀察者注冊(cè)了對(duì)一個(gè)對(duì)象的某個(gè)屬性鍵值觀察之后假夺,被觀察對(duì)象的 isa 指針?biāo)赶虻膬?nèi)容發(fā)生了變化,指向了一個(gè)中間類而不是真正的類斋攀。這也導(dǎo)致 isa 指針并不一定是指向?qū)嵗鶎俚恼嬲念悺?/p>

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)不應(yīng)依靠 isa 指針來確定類成員身份已卷。相反,你應(yīng)該使用 class 方法來確定對(duì)象實(shí)例所屬的類淳蔼。

2.1 中間類

根據(jù)官網(wǎng)文檔的內(nèi)容侧蘸,我們初步判斷,在 KVO 底層實(shí)現(xiàn)中鹉梨,會(huì)有一個(gè)所謂的中間類生成讳癌。而這個(gè)中間類會(huì)讓對(duì)象的 isa 指針發(fā)生變化。我們不妨測(cè)試一下:

image

如上圖所示俯画,person 對(duì)象和 personForTest 對(duì)象都是屬于 JHPerson 類的析桥,而 person 對(duì)象又實(shí)現(xiàn)了 KVO,但是在控制臺(tái)打印結(jié)果里面可以看到它們二者的類都是 JHPerson 類艰垂。不是說會(huì)有一個(gè)中間類生成嗎泡仗?難道是這個(gè)中間類生成又被干掉了?我們直接LLDB 大法測(cè)試一下:

image

Bingo~猜憎,所謂的中間類 NSKVONotifying_JHPerson 被我們找出來了娩怎。那么其實(shí)這里顯然,系統(tǒng)是重寫了中間類 NSKVONotifying_JHPersonclass 方法胰柑,讓我們以為對(duì)象的 isa 指針一直指向的都是 JHPerson 類截亦。那么這個(gè)中間類和原來的類是什么關(guān)系呢?我們可以測(cè)試一下:

image

其中 printClasses 實(shí)現(xiàn)如下:

- (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);
}

最終打印結(jié)果如下:

classes = (
    JHPerson
)
classes = (
    JHPerson,
    "NSKVONotifying_JHPerson"
)

結(jié)果很清晰,中間類 NSKVONotifying_JHPerson 是作為原始真正的類 JHPerson 的子類的角色柬讨。

2.2 KVO 觀察的是什么崩瓤?

KVO 所關(guān)注的是屬性值的變化,而屬性值本質(zhì)上是成員變量+getter+setter踩官,getter 是用來獲取值的却桶,而顯然只有 setter 和成員變量賦值兩種方式可以改變屬性值。我們測(cè)試一下這兩種方式:

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

如上圖所示蔗牡,setter 方法對(duì)屬性 name 做了修改被 KVO 監(jiān)聽到了颖系,而成員變量 _nickName 的修改并沒有被監(jiān)聽到,說明 KVO 底層其實(shí)觀察的是 setter 方法辩越。

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

我們可以通過打印原始類和中間類的方法列表來驗(yàn)證:

image

printClassAllMethod 方法實(shí)現(xiàn)如下:

- (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 方法黔攒。根據(jù)我們前面所探索的消息發(fā)送以及轉(zhuǎn)發(fā)流程趁啸,這里的中間類應(yīng)該是重寫了 setName: 强缘、classdealloc_isKVOA 方法莲绰。

由我們上一小節(jié)的測(cè)試結(jié)果可知欺旧,中間類重寫的 class 方法結(jié)果仍然是返回的是原始類,顯然系統(tǒng)這樣做的目的就是隱藏中間類的存在蛤签,讓調(diào)用者調(diào)用 class 方法結(jié)果前后一致辞友。

2.4 KVO 中間類何時(shí)指回去?

我們推斷 KVO 注冊(cè)觀察者到移除觀察者這一個(gè)流程里面震肮,被觀察對(duì)象的 isa 指針才會(huì)指向中間類称龙,我們用代碼測(cè)試一下:

image
image

由上圖可知,觀察者的 dealloc 方法中的移除觀察者之后戳晌,對(duì)象的 isa 指針已經(jīng)指回了原始的類鲫尊。那么是不是此時(shí)中間類就被銷毀了呢,我們不妨打印一下此時(shí)原始類的所有子類信息:

image

結(jié)果表明中間類仍然存在沦偎,也就是說移除觀察者并不會(huì)導(dǎo)致中間類銷毀疫向,顯然這樣對(duì)于多次添加和移除觀察者來說性能上更好。

2.5 KVO 調(diào)用順序

而我們前面說了豪嚎,有一個(gè)中間類的存在搔驼,既然要生成中間類,肯定是有意義的侈询,我們梳理一下整個(gè) KVO 的流程舌涨,從注冊(cè)觀察者到觀察者的回調(diào)通知,既然有回調(diào)通知扔字,那么肯定是在某個(gè)地方發(fā)出回調(diào)的囊嘉,而由于中間類是不能編譯的,所以我們對(duì)中間類的父類也就是 JHPerson 類革为,我們重寫一下相應(yīng)的 setter 方法扭粱,我們不妨測(cè)試一下:

// 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");
}

打印結(jié)果如下:

image

也就是說 KVO 的調(diào)用順序是:

  • 調(diào)用 willChangeValueForKey:
  • 調(diào)用原來的 setter 實(shí)現(xiàn)
  • 調(diào)用 didChangeValueForKey:

也就是說 didChangeValueForKey: 內(nèi)部必然是調(diào)用了 observerobserveValueForKeyPath:ofObject:change:context:方法。

三震檩、自定義 KVO 如何實(shí)現(xiàn)

我們已經(jīng)初步了解了 KVO 底層原理琢蛤,接下來我們嘗試自己簡單實(shí)現(xiàn)一下 KVO
我們直接跳轉(zhuǎn)到 addObserver:forKeyPath:options:context: 方法的聲明處:

image

可以看到恳蹲,跟 KVC 一樣,KVO 在底層也是以分類的形式加載的俩滥,這個(gè)分類叫做 NSKeyValueObserverRegistration嘉蕾。我們不妨也以這種方式來自定義實(shí)現(xiàn)一下 KVO

// NSObject+JHKVO.h
@interface NSObject (JHKVO)
// 觀察者注冊(cè)
- (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(nullable void *)context;
// 回調(diào)通知觀察者
- (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

這里為了避免與系統(tǒng)的方法沖突霜旧,所以添加了一個(gè)方法前綴错忱。同時(shí)對(duì)于觀察策略儡率,為了簡化實(shí)現(xiàn),這里只聲明了新值和舊值兩種策略以清。

3.1 自定義觀察者注冊(cè)

在開始之前儿普,我們回憶下自定義 KVC 的時(shí)候的第一個(gè)步驟就是判斷 key 或者 keyPath,那么 KVO 是否也需要進(jìn)行這樣的判斷呢?經(jīng)過筆者實(shí)際測(cè)試掷倔,如果觀察對(duì)象的一個(gè)不存在的屬性的話眉孩,并不會(huì)報(bào)錯(cuò),也不會(huì)來到 KVO 回調(diào)方法勒葱,由此可見浪汪,判斷 keyPath 是否存在并沒有必要。但是凛虽,我們回想一下上一節(jié) KVO 底層原理死遭,KVO 關(guān)注的是屬性的 setter 方法,那其實(shí)判斷對(duì)象所屬的類是否有這樣的 setter 就相當(dāng)于同時(shí)判斷了 keyPath 是否存在凯旋。接著我們就需要去動(dòng)態(tài)的創(chuàng)建子類呀潭,創(chuàng)建子類的過程中包括了重寫 setter 等一系列方法。然后就需要保存觀察者和 keyPath 等信息至非,這里我們借助關(guān)聯(lián)對(duì)象來實(shí)現(xiàn)钠署,我們把傳入的觀察者對(duì)象、keyPath和觀察策略封裝成一個(gè)新的對(duì)象存儲(chǔ)在關(guān)聯(lián)對(duì)象中睡蟋。因?yàn)橥粋€(gè)對(duì)象的屬性可以被不同的觀察者所觀察踏幻,所以這里實(shí)質(zhì)上是以對(duì)象數(shù)組的方式存儲(chǔ)在關(guān)聯(lián)對(duì)象里面。
話不多說戳杀,直接上代碼:

// 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 對(duì)象该面。

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:@"對(duì)象 %@ 的 key %@ 沒有 setter 實(shí)現(xiàn)", self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    // 2.動(dòng)態(tài)創(chuàng)建中間子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3.將對(duì)象的isa指向?yàn)樾碌闹虚g子類
    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);
    }
}

上面的代碼是完整的添加觀察者的流程:

  • 判斷對(duì)象所屬的類上是否有要觀察的 keyPath 對(duì)應(yīng)的 setter 方法

這里的 setterForGetter 實(shí)現(xiàn)如下:

static NSString * setterForGetter(NSString *getter)
{
   // 判斷 getter 是否為空字符串
   if (getter.length <= 0) {
       return nil;
   }
   // 取出 getter 字符串的第一個(gè)字母并轉(zhuǎn)大寫 
   NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
   // 取出剩下的字符串內(nèi)容
   NSString *remainingLetters = [getter substringFromIndex:1];
   // 將首字母大寫的字母與剩下的字母拼接起來得到 `set<KeyPath>` 格式的字符串
   NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
   return setter;
}
  • 如果存在相應(yīng)的 setter 方法,那么就創(chuàng)建有對(duì)應(yīng)前綴的中間子類

這里的 createChildClassWithKeyPath 實(shí)現(xiàn)如下:

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    // 獲得原始類的類名
    NSString *oldClassName = NSStringFromClass([self class]);
    // 在原始類名前添加中間子類的前綴來獲得中間子類名
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJHKVOPrefix,oldClassName];
    // 通過中間子類名來判斷是否創(chuàng)建過
    Class newClass = NSClassFromString(newClassName);
    // 如果創(chuàng)建過中間子類信卡,直接返回
    if (newClass) return newClass;
    // 如果沒有創(chuàng)建過隔缀,則需要?jiǎng)?chuàng)建一下, objc_allocateClassPair 方法的三個(gè)參數(shù)分別為: 1.父類 2.新類的名字 3.創(chuàng)建新類所需額外的空間
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 注冊(cè)中間子類
    objc_registerClassPair(newClass);
    // 從父類上拿到 `class` 方法的 `SEL` 以及類型編碼,然后在中間子類上添加一個(gè)新的子類實(shí)現(xiàn) `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` 以及類型編碼傍菇,然后在中間子類上添加一個(gè)新的子類實(shí)現(xiàn) `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 的實(shí)現(xiàn)如下:

Class jh_class(id self,SEL _cmd) {
   // 通過 class_getSuperclass 來返回父類的 `Class`猾瘸,達(dá)到對(duì)調(diào)用者隱藏中間子類的效果
   return class_getSuperclass(object_getClass(self));
}

jh_setter 的實(shí)現(xiàn)如下:

static void jh_setter(id self,SEL _cmd,id newValue){
    // 因?yàn)?`_cmd` 作為方法的第二個(gè)參數(shù)其實(shí)就是 `setter` 的 `SEL`,這里反向獲得對(duì)應(yīng) `getter` 字符串形式作為 `keyPath`丢习,然后通過 `KVC` 來獲取到舊的屬性值
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    
    // 因?yàn)槭侵貙懜割惖?`setter`牵触,所以還需要通過消息發(fā)送的方式手動(dòng)執(zhí)行以下父類的 `setter` 方法
    // 通過強(qiáng)轉(zhuǎn)的方式將 `objc_msgSendSuper` 轉(zhuǎn)成 `jh_msgSendSuper` 函數(shù)指針,同時(shí)咐低,由于 `objc_msgSendSuper` 要比我們常見的 `objc_msgSend` 多一個(gè)父類結(jié)構(gòu)體參數(shù)揽思,所以需要手動(dòng)構(gòu)建一下這個(gè)父類結(jié)構(gòu)體,結(jié)構(gòu)體有兩個(gè)屬性见擦,分別是實(shí)例對(duì)象以及實(shí)例對(duì)象的類的父類
    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)),
    };
    // 準(zhǔn)備工作完成后手動(dòng)調(diào)用 `jh_msgSendSuper`钉汗,因?yàn)?`superStruct` 是結(jié)構(gòu)體類型羹令,而 `jh_msgSendSuper` 的第一個(gè)參數(shù)是空指針對(duì)象,所以這里需要加取地址符來把結(jié)構(gòu)體地址賦值給指針對(duì)象
    jh_msgSendSuper(&superStruct, _cmd, newValue);
    // 調(diào)用完父類的 `setter` 之后损痰,從關(guān)聯(lián)對(duì)象中取出存儲(chǔ)了自定義的對(duì)象數(shù)組
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    // 循環(huán)遍歷自定義的對(duì)象
    for (JHKVOInfo *info in observerArr) {
    // 如果 `keyPath` 匹配則進(jìn)入下一步
        if ([info.keyPath isEqualToString:keyPath]) {
            // 基于線程安全的考慮福侈,使用 `GCD` 的全局隊(duì)列異步執(zhí)行下面的操作 
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                // 初始化一個(gè)通知字典
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 判斷存儲(chǔ)的觀察策略,如果是新值卢未,則在通知字典中設(shè)置新值  
                if (info.options & JHKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                // 如果是舊值肪凛,在通知字典中設(shè)置舊值
                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` 手動(dòng)發(fā)送消息,達(dá)到觀察者收到回調(diào)的效果
                ((void(*)(id, SEL, id, id, NSMutableDictionary *, void *))objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL);
            });
        }
    }
}

getterForSetter 實(shí)現(xiàn)如下:

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];
    // 對(duì) getter 字符串首字母小寫處理
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

3.2 自定義移除觀察者

我們接著開始自定義移除觀察者显拜,首先,我們需要把 isa 指回原來的類爹袁,然后需要對(duì)關(guān)聯(lián)對(duì)象中存儲(chǔ)的自定義對(duì)象數(shù)組對(duì)應(yīng)的觀察者移除掉远荠。

- (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context
{
    // 從關(guān)聯(lián)對(duì)象中取出數(shù)組
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    // 如果數(shù)組中沒有內(nèi)容,說明沒有添加過觀察者失息,那么直接返回
    if (observerArr.count<=0) {
        return;
    }
    
    // 遍歷取出的所有自定義對(duì)象
    for (JHKVOInfo *info in observerArr) {
        // 如果 `keyPath` 匹配上了 則從數(shù)組中移除響應(yīng)對(duì)象譬淳,然后存儲(chǔ)最新的數(shù)組到關(guān)聯(lián)對(duì)象上
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    
    // 要將 `isa` 指回原來的類的前提條件是,被觀察屬性的對(duì)象已經(jīng)沒有任何觀察者在觀察了盹兢,那么就需要指回去
    if (observerArr.count<=0) {
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

3.3 實(shí)現(xiàn)自動(dòng)移除觀察者

現(xiàn)在我們自定義的 KVO 已經(jīng)可以實(shí)現(xiàn)簡單的通知觀察者新值和舊值的變化了邻梆,但其實(shí)對(duì)于 api 的使用者來說,還是要嚴(yán)格的執(zhí)行 addObserverremoveObserver 的配套操作绎秒,難免有些繁瑣浦妄。雖然一般來說為了方便起見,都是在觀察者的 dealloc 方法中去手動(dòng)調(diào)用 removeObserver 方法见芹,但還是太麻煩了剂娄。因此,我們可以借助 methodSwizzling 的技術(shù)來替換默認(rèn) dealloc 方法的實(shí)現(xiàn)玄呛,直接上代碼:

+ (BOOL)jh_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
    // 獲取 Class 對(duì)象
    Class cls = self;
    // 通過 `SEL` 獲取原始方法
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    // 通過 `SEL` 獲取要替換的方法
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    // 如果要替換的方法不存在阅懦,返回 NO
    if (!swiMethod) {
        return NO;
    }
    // 如果原始方法不存在,那么就直接在 Class 上添加要替換的方法徘铝,注意耳胎,添加的方法實(shí)現(xiàn)為要替換的方法,但是方法 `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 上已經(jīng)存在了要替換的方法的實(shí)現(xiàn)怕午,那么就把原始方法實(shí)現(xiàn)替換掉 `swizzledSEL` 對(duì)應(yīng)的方法實(shí)現(xiàn)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
    // 如果不成功,說明原始方法已經(jīng)存在淹魄,則直接交換方法實(shí)現(xiàn)
        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);
    // 這里并不會(huì)造成循環(huán)引用的遞歸郁惜,因?yàn)?`myDealloc` 的方法實(shí)現(xiàn)是真正的原始 `dealloc`
    [self myDealloc];
}

通過實(shí)現(xiàn)自動(dòng)移除觀察者,api 的使用者可以完全放心的只使用 addObserver 來添加觀察者以及 observeValueForKeyPath 來接收回調(diào)揭北。

3.4 函數(shù)式編程思想重構(gòu)

我們雖然已經(jīng)實(shí)現(xiàn)了自動(dòng)的移除觀察者扳炬,但是從函數(shù)式編程思想來看,現(xiàn)在的設(shè)計(jì)還不是很完美搔体,對(duì)同一個(gè)屬性的觀察的代碼散落在不同的地方恨樟,如果業(yè)務(wù)一旦增多,對(duì)于可讀性和可維護(hù)性都有很大的影響疚俱。所以劝术,我們可以把現(xiàn)在這種回調(diào)的形式重構(gòu)為 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:@"對(duì)象 %@ 的 key %@ 沒有 setter 實(shí)現(xiàn)", self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    // 2.動(dòng)態(tài)創(chuàng)建中間子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3.將對(duì)象的isa指向?yàn)樾碌闹虚g子類
    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];
} 

這里我們直接通過傳入分類一個(gè) block呆奕,然后存儲(chǔ)在對(duì)應(yīng)的自定義觀察對(duì)象中养晋,然后我們還需要在重寫 setter 方法中做出修改,原來是直接通過發(fā)送消息來實(shí)現(xiàn)回調(diào)梁钾,現(xiàn)在需要改成 block 回調(diào)

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);
        }
    }
}

四绳泉、總結(jié)

經(jīng)過探索 KVCKVO 的底層,我們可以看到 KVO 是建立在 KVC 基礎(chǔ)之上的姆泻。KVO 作為觀察者設(shè)計(jì)模式在 iOS 中的具體落地零酪,其原理到實(shí)現(xiàn)我們都探索完了。其實(shí)我們可以看出來在早期設(shè)計(jì) api 的時(shí)候拇勃,原生的 KVO 其實(shí)并不好用四苇,所以諸如 FaceBook 的庫 KVOController 會(huì)大受歡迎。當(dāng)然本文的自定義 KVO 實(shí)現(xiàn)并不嚴(yán)謹(jǐn)方咆,感興趣的讀者可以查看這兩個(gè)代碼庫:

我們的 iOS 底層探索系列接下來將會(huì)進(jìn)入多線程篇章月腋,敬請(qǐng)期待~

參考資料

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

nil/Nil/Null/NSNull - NSHipster

Key-Value Observing - NSHipster

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瓣赂,隨后出現(xiàn)的幾起案子榆骚,更是在濱河造成了極大的恐慌,老刑警劉巖钩述,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寨躁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡牙勘,警方通過查閱死者的電腦和手機(jī)职恳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來方面,“玉大人放钦,你說我怎么就攤上這事」Ы穑” “怎么了操禀?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長横腿。 經(jīng)常有香客問我颓屑,道長斤寂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任揪惦,我火速辦了婚禮遍搞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘器腋。我一直安慰自己溪猿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布纫塌。 她就那樣靜靜地躺著诊县,像睡著了一般。 火紅的嫁衣襯著肌膚如雪措左。 梳的紋絲不亂的頭發(fā)上依痊,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音怎披,去河邊找鬼抗悍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛钳枕,可吹牛的內(nèi)容都是我干的缴渊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼鱼炒,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼衔沼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起昔瞧,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤指蚁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后自晰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凝化,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年酬荞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搓劫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡混巧,死狀恐怖枪向,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咧党,我是刑警寧澤秘蛔,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響深员,放射性物質(zhì)發(fā)生泄漏负蠕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一倦畅、第九天 我趴在偏房一處隱蔽的房頂上張望虐急。 院中可真熱鬧,春花似錦滔迈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盼理,卻和暖如春谈山,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宏怔。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工奏路, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臊诊。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓鸽粉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抓艳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子触机,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容