神奇的 BlocksKit(1):原始碼分析

發表於2016-05-03

高能預警:本篇文章非常長,因為 BlocksKit 的實現還是比較複雜和有意的。這篇文章不是為了剖析 iOS 開發中的 block 的實現以及它是如何組成甚至使用的,如果你想通過這篇文章來了解 block 的實現,它並不能幫到你。

Block 到底是什麼?這可能是困擾很多 iOS 初學者的一個問題。如果你在 Google 上搜尋類似的問題時,可以查詢到幾十萬條結果,block 在 iOS 開發中有著非常重要的地位,而且它的作用也越來越重要。


概述

這篇文章僅對 BlocksKit v2.2.5 的原始碼進行分析,從框架的內部理解下面的功能是如何實現的:

  • 為 NSArray、 NSDictionary 和 NSSet 等集合型別以及對應的可變集合型別 NSMutableArray、NSMutableDictionary 和 NSMutableSet 新增 bk_each: 等方法完成對集合中元素的快速遍歷
  • 使用 block 對 NSObject 物件 KVO
  • 為 UIView 物件新增 bk_whenTapped: 等方法快速新增手勢
  • 使用 block 替換 UIKit 中的 delegate ,涉及到核心模組 DynamicDelegate。

BlocksKit 框架中包括但不僅限於上述的功能,這篇文章是對 v2.2.5 版本原始碼的分析,其它版本的功能不會在本篇文章中具體討論。

如何提供簡潔的遍歷方法

BlocksKit 實現的最簡單的功能就是為集合型別新增方法遍歷集合中的元素。

這段程式碼非常簡單,我們可以使用 enumerateObjectsUsingBlock: 方法替代 bk_each: 方法:

這部分程式碼的實現也沒什麼難度:

它在 block 執行前會判斷傳進來的 block 是否為空,然後就是呼叫遍歷方法,把陣列中的每一個 obj 傳給 block。

BlocksKit 在這些集合類中所新增的一些方法在 Ruby、Haskell 等語言中也同樣存在。如果你接觸過上面的語言,理解這裡方法的功能也就更容易了,不過這不是這篇文章關注的主要內容。

NSObject 上的魔法

NSObject 是 iOS 中的『上帝類』。

在 NSObject 上新增的方法幾乎會新增到 Cocoa Touch 中的所有類上,關於 NSObject 的討論和總共分為以下三部分進行:

  1. AssociatedObject
  2. BlockExecution
  3. BlockObservation

新增 AssociatedObject

經常跟 runtime 打交道的人不可能不知道 AssociatedObject ,當我們想要為一個已經存在的類新增屬性時,就需要用到 AssociatedObject 為類新增屬性,而 BlocksKit 提供了更簡單的方法來實現,不需要新建一個分類。

這裡我們使用了 bk_associateValue:withKey: 和 bk_associatedValueForKey: 兩個方法設定和獲取 name 對應的值Draveness.

這裡的 OBJC_ASSOCIATION_RETAIN_NONATOMIC 表示當前屬性為 retain nonatomic 的,還有其它的引數如下:

上面的這個 NS_ENUM 也沒什麼好說的,需要注意的是這裡沒有 weak 屬性。

BlocksKit 通過另一種方式實現了『弱屬性』:

在這裡先獲取了一個 _BKWeakAssociatedObject 物件 assoc,然後更新這個物件的屬性 value。

因為直接使用 AssociatedObject 不能為物件新增弱屬性,所以在這裡新增了一個物件,然後讓這個物件持有一個弱屬性:

這就是 BlocksKit 實現弱屬性的方法,我覺得這個實現的方法還是比較簡潔的。

getter 方法的實現也非常類似:

在任意物件上執行 block

通過這個類提供的一些介面,可以在任意物件上快速執行執行緒安全、非同步的 block,而且這些 block 也可以在執行之前取消。

判斷 block 是否為空在這裡都是細枝末節,這個方法中最關鍵的也就是它返回了一個可以取消的 block,而這個 block 就是用靜態函式 BKDispatchCancellableBlock 生成的。

這個函式首先會執行 BKSupportsDispatchCancellation 來判斷當前平臺和版本是否支援使用 GCD 取消 block,當然一般都是支援的:

  • 函式返回的是 YES,那麼在 block 被派發到指定佇列之後就會返回這個 dispatch_block_t 型別的 block
  • 函式返回的是 NO,那麼就會就會手動包裝一個可以取消的 block,具體實現的部分如下:

上面這部分程式碼就先建立一個 wrapper block,然後派發到指定佇列,派發到指定佇列的這個 block 是一定會執行的,但是怎麼取消這個 block 呢?

如果當前 block 沒有執行,我們在外面呼叫一次 wrapper(YES) 時,block 內部的 cancelled 變數就會被設定為 YES,所以 block 就不會執行。

  1. dispatch_after — cancelled = NO
  2. wrapper(YES) — cancelled = YES
  3. wrapper(NO) — cancelled = YES block 不會執行

這是實現取消的關鍵部分:

  • GCD 支援取消 block,那麼直接呼叫 dispatch_block_cancel 函式取消 block
  • GCD 不支援取消 block 那麼呼叫一次 wrapper(YES)

使用 Block 封裝 KVO

BlocksKit 對 KVO 的封裝由兩部分組成:

  1. NSObject 的分類負責提供便利方法
  2. 私有類 _BKObserver 具體實現原生的 KVO 功能

提供介面並在 dealloc 時停止 BlockObservation

NSObject+BKBlockObservation 這個分類中的大部分介面都會呼叫這個方法:

我們不會在這裡討論 #1、#3 部分,再詳細閱讀 #2 部分程式碼之前,先來看一下這個省略了絕大部分細節的核心方法。

使用傳入方法的引數建立了一個 _BKObserver 物件,然後呼叫 startObservingWithOptions: 方法開始 KVO 觀測相應的屬性,然後以 {identifier,obeserver} 的形式存到字典中儲存。

這裡實在沒什麼新意,我們在下一小節中會介紹 startObservingWithOptions: 這一方法。

在分類中調劑 dealloc 方法

這個問題我覺得是非常值得討論的一個問題,也是我最近在寫框架時遇到很棘手的一個問題。

當我們在分類中註冊一些通知或者使用 KVO 時,很有可能會找不到登出這些通知的時機。

因為在分類中是無法直接實現 dealloc 方法的。 在 iOS8 以及之前的版本,如果某個物件被釋放了,但是剛物件的註冊的通知沒有被移除,那麼當事件再次發生,就會向已經釋放的物件發出通知,整個程式就會崩潰。

這裡解決的辦法就十分的巧妙:

這部分程式碼的執行順序如下:

  1. 首先呼叫 bk_observedClassesHash 類方法獲取所有修改過 dealloc 方法的類的集合 classes
  2. 使用 @synchronized (classes) 保證互斥,避免同時修改 classes 集合的類過多出現意料之外的結果
  3. 判斷即將調劑方法的類 classToSwizzle 是否調劑過 dealloc 方法
  4. 如果 dealloc 方法沒有調劑過,就會通過 sel_registerName(“dealloc”) 方法獲取選擇子,這行程式碼並不會真正註冊dealloc 選擇子而是會獲取 dealloc 的選擇子,具體原因可以看這個方法的實現 sel_registerName
  5. 在新的 dealloc 中新增移除 Observer 的方法, 再呼叫原有的 dealloc
    1. 呼叫 bk_removeAllBlockObservers 方法移除所有觀察者,也就是這段程式碼的最終目的
    2. 根據 originalDealloc 是否為空,決定是向父類傳送訊息,還是直接呼叫 originalDealloc 並傳入 objSelf,deallocSelector 作為引數
  6. 在我們獲得了新 dealloc 方法的選擇子和 IMP 時,就要改變原有的 dealloc 的實現了
    1. 呼叫 class_addMethod 方法為當前類新增選擇子為 dealloc 的方法(當然 99.99% 的可能不會成功)
    2. 獲取原有的 dealloc 例項方法
    3. 將原有的實現儲存到 originalDealloc 中,防止使用 method_setImplementation 重新設定該方法的過程中呼叫dealloc 導致無方法可用
    4. 重新設定 dealloc 方法的實現。同樣,將實現儲存到 originalDealloc 中防止實現改變

關於在分類中調劑 dealloc 方法的這部分到這裡就結束了,下一節將繼續分析私有類 _BKObserver。

私有類 _BKObserver

_BKObserver 是用來觀測屬性的物件,它在介面中定義了 4 個屬性:

上面四個屬性的具體作用在這裡不說了,上面的 bk_addObserverForKeyPaths:identifier:options:context: 方法中呼叫_BKObserver 的初始化方法 initWithObservee:keyPaths:context:task: 太簡單了也不說了。

上面的第一行程式碼生成一個 observer 例項之後立刻呼叫了 startObservingWithOptions: 方法開始觀測對應的 keyPath:

startObservingWithOptions: 方法最重要的就是第 #1 部分:

遍歷自己的 keyPaths 然後讓 _BKObserver 作觀察者觀察自己,然後傳入對應的 keyPath。

關於 _stopObservingLocked 方法的實現也十分的相似,這裡就不說了。

到目前為止,我們還沒有看到實現 KVO 所必須的方法 observeValueForKeyPath:ofObject:change:context,這個方法就是每次屬性改變之後的回撥:

這個方法的實現也很簡單,根據傳入的 context 值,對 task 型別轉換,並傳入具體的值。

這個模組倒著就介紹完了,在下一節會介紹 BlocksKit 對 UIKit 元件一些簡單的改造。

改造 UIKit

在這個小結會具體介紹 BlocksKit 是如何對一些簡單的控制元件進行改造的,本節大約有兩部分內容:

  • UIGestureRecongizer + UIBarButtonItem + UIControl
  • UIView

改造 UIGestureRecongizer,UIBarButtonItem 和 UIControl

先來看一個 UITapGestureRecognizer 使用的例子

程式碼中的 bk_recognizerWithHandler:delay: 方法在最後都會呼叫初始化方法 bk_initWithHandler:delay: 生成一個UIGestureRecongizer 的例項

它會在這個方法中傳入 target 和 selector。 其中 target 就是 self,而 selector 也會在這個分類中實現:

因為在初始化方法 bk_initWithHandler:delay: 中儲存了當前手勢的 bk_handler,所以直接呼叫在 Block Execution 一節中提到過的 bk_performAfterDelay:usingBlock: 方法,將 block 派發到指定的佇列中,最終完成對 block 的呼叫。

封裝 block 並控制 block 是否可以執行

這部分程式碼和前面的部分有些相似,因為這裡也用到了一個屬性 bk_shouldHandleAction 來控制 block 是否會被執行:

同樣 UIBarButtonItem 和 UIControl 也是用了幾乎相同的機制,把 target 設定為 self,讓後在分類的方法中呼叫指定的 block。

UIControlWrapper

稍微有些不同的是 UIControl。因為 UIControl 有多種 UIControlEvents,所以使用另一個類 BKControlWrapper 來封裝handler 和 controlEvents

其中 UIControlWrapper 物件以 {controlEvents,wrapper} 的形式作為 UIControl 的屬性存入字典。

改造 UIView

因為在上面已經改造過了 UIGestureRecognizer,在這裡改造 UIView 就變得很容易了:

UIView 分類只有這一個核心方法,其它的方法都是向這個方法傳入不同的引數,這裡需要注意的就是。它會遍歷所有的gestureRecognizers,然後把對所有有衝突的手勢呼叫 requireGestureRecognizerToFail: 方法,保證新增的手勢能夠正常的執行。

相關文章