ReactiveCocoa2 原始碼淺析

發表於2015-09-06

• 開車不需要知道離合器是怎麼工作的,但如果知道離合器原理,那麼車子可以開得更平穩。

ReactiveCocoa 是一個重型的 FRP 框架,內容十分豐富,它使用了大量內建的 block,這使得其有強大的功能的同時,內部原始碼也比較複雜。本文研究的版本是2.4.4,小版本間的差別不是太大,無需擔心此問題。 這裡只探究其核心 RACSignal 原始碼及其相關部分。本文不會詳細解釋裡面的程式碼,重點在於討論那些核心程式碼是 怎麼來 的。文字難免有不正確的地方,請不吝指教,非常感謝。

@protocol RACSubscriber

訊號是一個非同步資料流,即一個將要發生的以時間為序的事件序列,它能發射出三種不同的東西:valueerrorcompleted。我們們能非同步地捕獲這些事件:監聽訊號,針對其發出的三種東西進行操作。“監聽”資訊的行為叫做 訂閱(subscriber)。我們定義的操作就是觀察者,這個被“監聽”的訊號就是被觀察的主體(subject) 。其實,這正是“觀察者”設計模式!

RAC 針對這個訂閱行為定義了一個協議:RACSubscriber。RACSubscriber 協議是與 RACSignal 打交道的唯一方式。我們們先不探究 RACSignal 的內容,而是先研究下 RACSubscriber 是怎麼回事。

先來看下 RACSubscriber 的定義:

##1、NLSubscriber
我們們自己來實現這個協議看看(本文自定義的類都以 “NL” 開頭,以視區別):

現在我們們這個類只關心 sendNext:sendError:sendCompleted。本類的實現只是簡單的列印一些資料。那怎麼來使用這個訂閱者呢?RACSignal 類提供了介面來讓實現了 RACSubscriber 協議的訂閱者訂閱訊號:

用定時器訊號來試試看:

下面是輸出結果:

##2、改進NLSubscriber

現在的這個訂閱者類 NLSubscriber 除了列印打東西外,啥也幹不了,更別說複用了,如果針對所有的訊號都寫一個訂閱者那也太痛苦了,甚至是不太可能的事。

我們們來改進一下,做到如下幾點:
1. 實現 RACSubscriber 協議
2. 提供與 RACSubscriber對應的可選的可配的介面。

沒錯,這正是一個介面卡!

第2點的要求可不少,那怎麼才能做到這一點呢?還好,OC 中有 block !我們們可以將 RACSubscriber 協議中的三個方法轉為三個 block:

改進目標和改進方向都有了,那我們們來看看改進後的的樣子:

現在來試試看這個改進版,還是上面那個定時器的例子:

輸出結果如下:

輸出結果沒什麼變化,但是訂閱者的行為終於受到我們們的撐控了。再也不用為了一個訊號而去實現 RACSubscriber 協議了,只需要拿出 NLSubscriber 這個介面卡,再加上我們們想要的自定義的行為即可。如果對訊號發出的某個事件不感興趣,直接傳個 nil 可以了,例如上面例子的 error: ,要知道, RACSubscriber 協議中的所有方法都是 @required 的。NLSubscriber 大大方便了我們的工作。

那還以再改進嗎?

##3、RACSignal 類別之 Subscription

有沒有可能把 NLSubscriber 隱藏起來呢?畢竟作為一個訊號的消費者,需要了解的越少就越簡單,用起來也就越方便。我們們可以通過 OC 中的類別方式,給 RACSignal 加個類別(nl_Subscription),將訂閱操作封裝到這個訊號類中。這樣,對於使用這個類的客戶而言,甚至不知道訂閱者的存在。
nl_Subscription 類別程式碼如下:

在這個類別中,將訊號的 next:error:completed 以及這三個事件的組合都以 block 的形式封裝起來,從以上程式碼中可以看出,這些方法最終呼叫的還是 - (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock; 方法,而它則封裝了訂閱者 NLSubsciber

通過這麼個小小的封裝,客戶使用起來就極其方便了:

輸出如下:

本例並沒有採用之前的 “定時器訊號”,而是自己建立的訊號,當有訂閱者到來時,由這個訊號來決定在什麼時候傳送什麼事件。這個例子裡傳送的事件的邏輯請看程式碼裡的註釋。

看到這裡,是不是很熟悉了?有沒有想起 subscribeNext:,好吧,我就是在使用好多好多次它之後才慢慢入門的,誰讓 RAC 的大部分教程裡面第一個講的就是它呢!

到了這裡,是不是訂閱者這部分就完了呢?我相信你也注意到了,這裡有幾個不對勁的地方:

1. 無法隨時中斷訂閱操作。想想訂閱了一個無限次的定時器訊號,無法中斷訂閱操作的話,定時器就是永不停止的發下去。

2. 訂閱完成或錯誤時,沒有統一的地方做清理、掃尾等工作。比如現在有一個上傳檔案的訊號,當上傳完成或上傳錯誤時,你得斷開與檔案伺服器的網路連線,還得清空記憶體裡的檔案資料。

##4、Disposable
###RACDisposable
針對上述兩個問題,RACDisposable 應運而生。也就是說 Disposable 有兩個作用:

1. 中斷訂閱某訊號
2. 訂閱完成後,執行一些收尾任務(清理、回收等等)。

訂閱者與 Disposable 的關係:

1. 當 Disposable 有“清理”過,那麼訂閱者就不會再接收到這個被“清理”訂閱源的任何事件。舉例而言,就是訂閱者 subscriberX 訂閱了訊號 signalA 和 signalB 兩個訊號,其所對應的 Disposable 分別為 disposableA 和 disposableB,也就是說 subscriberX 會同時接收來自 signalA 和 signalB 的訊號。當我們手動強制 “清理” disposableA 後,subscriberX 就不會再接收來自 signalA 的任何事件;而來自 signalB 的事件則不受影響。

2. 當訂閱者 subscriberX 有接收來自任何一個訊號的 “error” 或 “completed” 事件時,則不會再接收任何事件了。

可以這麼說:Disposable 代表發生了訂閱行為

根據 Disposable 的作用和與訂閱者的關係,來總結它所需要提供的介面:

1. 包含清理任務的 block ;
2. 執行清理任務的方法:- (void)dispose ;
3. 一個用來表明是否已經 “清理” 過的布林變數:BOOL disposed 。

我們們為這個 Disposable 也整了一個類,如下:

從這個類提供的介面來看,顯然是做不到 “訂閱者與 Disposable 的關係” 中的第2條的。因為這條中所描述的是一個訂閱者訂閱多個訊號,且能手動中斷訂閱其中一個訊號的功能,而 NLDisposable 是單個訂閱關係所設計的。

###RACCompoundDisposable
那怎麼組織這“多個”的關係呢?陣列?Good,就是陣列。OK,我們們來相像一下這個方案的初步程式碼。每個訂閱者有一個 Disposable 陣列,訂閱一個一個訊號,則加入一個 Disposable;當手動拆除一個訂閱關係時,找到與之相關的 Disposable,傳送 dispose 訊息,將其從陣列中移除;當訂閱者不能再接收訊息時(接收過 errorcompleted 訊息),要 dispose 陣列中所有元素,接下來再加入元素時,直接給這個要加入的元素髮送 dispose 訊息;在多執行緒環境下,每一次加入或移除或其遍歷時,都得加鎖。。。(好吧,我編不下去了)

我** ,這麼複雜,看來直接用陣列來維護是不可行的了。有啥其它可行的法子沒?還好,GoF 對此有個方案,叫做“組合模式”:

組合模式 允許你將物件組合成樹形結構來表現 “整體/部分” 層次結構。組合能讓客戶以一致的方式處理個別物件以及物件組合。

使用組合結構,我們能把相同的操作應用在組合和個別物件上。換句話說,在大多數情況下,我們可以 忽略 物件組合和個別物件之間的差別。

本文畢竟不是來講模式的,關於這個模式更多的資訊,請自行 google。

RAC 中這個組合類叫 RACCompoundDisposable, 我們們的叫 NLCompoundDisposable,來看看我們們這個類的程式碼:

 

###RACScheduler 簡介

本文不打算研究 RACScheduler 原始碼,但其又是 RAC 中不可或缺的一個元件,在研究 RACSignal 的原始碼時不可避免地會遇到它,所以對其作下介紹還是有必要的。其實它的原始碼並不複雜,可自行研究。

ReactiveCocoa 中 RACSignal 傳送的所有事件的傳遞交給了一個特殊的框架元件——排程器,即 RACScheduler 類簇(類簇模式稍後介紹)。排程器是為了簡化 同步/非同步/延遲 事件傳遞 以及 取消預定的任務(scheduded actions) 這兩種 RAC 中常見的動作而提出來的。“事件傳遞” 簡單而言就是些 blocks,RACScheduler 所做的就是:排程這些 blocks (schedule blokcs,還是英文的意思準確些)。我們可以通過那些排程方法所返回的 RACDisposable 物件來取消那些 scheduling blocks。

正如前面所說,RACScheduler 是一個類簇。我們們來看看幾種具體的排程器:

####RACImmediateScheduler
這是 RAC 內部使用的私有排程器,只支援同步 scheduling。就是簡單的馬上執行 block。這個偵錯程式的延遲 scheduling 是通過呼叫 -[NSThread sleepUntilDate:] 來阻塞當前執行緒來達到目的的。顯然,這樣一個排程器,沒法取消 scheduling,所以它那些方法返回的 disposables 啥也不會做(實際上,它那些 scheduling 方法返回的是nil)。

####RACQueueScheduler
這個排程器使用 GCD 佇列來 scheduling blocks。如果你對 GCD 有所瞭解的話,你會發現這個排程器的功能很簡單,它只是在 GCD 佇列 dispatching blocks 上的簡單封裝罷了。

####RACSubscriptionScheduler
這是另一個內部使用的私有排程器。如果當前執行緒有排程器(排程器可以與執行緒相關聯起來:associated)那它就將 scheduling 轉發給這個執行緒的排程器;否則就轉發給預設的 background queue 偵錯程式。

####介面
偵錯程式有下面一些方法:

scheduling block 如下:

##5、Subscriber 和 Disposable

前面介紹了 Disposable 的來源,現在來研究下怎麼使用它。還記得嗎,訂閱者與訊號打交道的唯一方式是 RACSignal 中的一個方法:

自定義訊號所對應的類是 RACDynamicSignalRACSignal 採用的是類簇模式。除自定義訊號之外還有幾種其它的訊號,之後會研究到。OC 中的 NSNumber 用的就是類簇模式。類簇是Foundation框架中廣泛使用的設計模式。類簇將一些私有的、具體的子類組合在一個公共的、抽象的超類下面,以這種方法來組織類可以簡化一個物件導向框架的公開架構,而又不減少功能的豐富性。

我們們來研究一下自定義訊號裡的這個方法的實現。這個方法實現的難處在於:“一個訂閱者可以訂閱多個訊號,並可以手動拆除其中任何一個訂閱”。針對這個問題,提出了上節講到的 RACDisposable。也就是說,在每一次訂閱時,都會返回一個與這次訂閱相關的 Disposable,那怎麼做到這一點呢?

給訂閱者新增一個 CompoundDisposable 型別的屬性 (畢竟 CompoundDisposable 就是用來針對多個 Disposable 的統一管理而存在的),然後在每一次訂閱時,都加一個 Disposable 到這個屬性裡,行不行?但很可惜,訂閱者是一個協議 protocol RACSubscriber,而不是一個具體的類,我們們在使用到它時,都是別人實現了這個協議的類的物件,所以我們們不太可能做到說給這麼一個未知的類新增一個屬性。

事實上,RAC 中確實有 RACSubscriber 這麼一個私有類(它是我們們第一個自定義類 NLSubscriber 的原型),我們們叫它做 class RACSubscriber。嗯,class RACSusbscriber 實現了 protocol RACSubscriber 協議:@interface RACSubscriber : NSObject <RACSubscriber>。有沒有想到 class NSObjectprotocol NSObject ?雖然它們形式上確實很像,但千萬別混為一談。RAC 中的其它實現了 protocol RACSubscriber 協議的訂閱者類可沒有一個繼承自 class RACSubscriber 的。

我們們可以用裝飾模式來解決這個問題

裝飾模式。在不必改變原類檔案和使用繼承的情況下,動態地擴充套件一個物件的功能。

訂閱者裝飾器 RACPassthroughSubscriber

在訂閱者每一次訂閱訊號時產生一個 Disposable,並將其與此次訂閱關聯起來,這是通過裝飾器 RACPassthroughSubscriber 來做到的。這個裝飾器的功能:

1. 包裝真正的訂閱者,使自己成為訂閱者的替代者。

2. 將真正的訂閱者與一個訂閱時產生的 Disposable 關聯起來。

這正是一個裝飾器所應該做的。依之前的,我們們來模仿這個裝飾器,新建一個我們們的裝飾器:NLPassthroughSubscriber,來看下它的程式碼:

自定義訊號 RACDynamicSignal 的訂閱方法 subscribe

我們們來看看 RACDynamicSignal 是怎麼來使用 RACPassthroughSubscriber 的,這裡就不自己寫程式碼了,直接上它的程式碼:

可以看到,訂閱者裝飾器直接偽裝成真正的訂閱器,傳給 didSubscribe 這個 block 使用。在這個 block 中,會有一些事件傳送給訂閱者裝飾器,而這個訂閱者裝飾器則根據 disposable 的狀態來來決定是否轉發給真正的訂閱者。disposable 作為返回值,返回給外部,也就是說能夠從外部來取消這個訂閱了。

從這幾行程式碼中,我們可以看到,didSubscribe 這個 block 是處於 subscriptionScheduler 這個 scheduler 的排程中。RACSubscriptionScheduler 的排程是取決於當前所在的執行緒的,即 didSubscribe 可能會在不同的排程器中被執行。

假設當前 -(RACDisposable *)subscribe:(id<RACSubscriber>)subscriber 這個方法是在非同步環境下呼叫的,那麼在 disposable 返回後,在schedule block 還沒有來得及呼叫,此時 disposable 中包含 schedulingDisposable。如果我們此時給 disposable 傳送 dispose 訊息,那麼 schedulingDisposable 也會被 dispose,schedule block 就不會執行了;如果是在 schedule block 執行中或執行後給 disposable 傳送 dispose 訊息,那麼 innerDisposableschedulingDisposable 都會被 dispose。這些行為正是我們們所預期的。

##6、再次改進NLSubscriber
###1、didSubscribeWithDisposable

這個 RACSubscriber 協議中宣告的一個方法,在最開始的時候被我們特意給忽略,現在是時候回過頭來看看它了。對於一個訂閱者來說,nexterrorcompleted 三種事件分別對應協議裡的三種方法,那麼這個方法存在的意義是什麼呢?

RACSubscriber 協議中,可以看到,當一個訂閱者有收到過 errorcompleted 事件後,這個訂閱者就不能再接收任何事件了,換句話說,此時這個訂閱者會解除所有的訂閱關係,且無法再次訂閱。既然要解除所有訂閱,首先我得知道我訂閱過哪些訊號是不?而代表一個訂閱行為的就是 disposable ,告訴它就傳一個給它好了。所以這個方法就是告訴訂閱者:你發生了訂閱行為。

那為啥要 RACCompoundDisposable 型別作為引數呢?因為有些訂閱者會針對其附加一些操作,而只有這個型別的 disposable 才能動態加入一些操作。接下來我們就會看到的。

###2、NLSubscriber 結合 RACDisposable
這一次改進 NLSubscriber 的目的是讓其可以終結自己的訂閱能力的功能。同時實現 didSubscribeWithDisposable 方法。千言萬語不如實際程式碼,讓我們來一探究竟:

###3、改進類別 nl_Subscription
還記得麼?nl_Subscription 類別中的訂閱方法一旦訂閱,就無法停止了,這顯然有很大的問題。解決這個問題很簡單,直接將 disposable 返回即可:

 

RACSignal – Operations

本節主要研究這些操作(Operations) —— flattenMap:map:filter: ….

終於看到你想看的東西了?好吧,我承認,上節的東西很無趣,可能壓根不是你想看的東西。但如果沒弄清上面的內容的話,直接研究 Operations 可是會比較吃力的喲~

你以為我們們現在開始研究 Operations?哈哈,你又得失望了~ 我們得先看看這兩個類:RACEmptySignalRACReturnSignal

##1、兩個 RACSignal 的特殊子類 RACEmptySignal 和 RACReturnSignal
###1、RACEmptySignal
RACEmptySignal+[RACSignal empty] 的內部實現,一個私有 RACSignal 子類。它就是一個會立即 completed 的訊號。讓我們來看看它的 - subscribe: 方法:

 

這樣一個訂閱者一訂閱就會 completed 訊號有什麼用呢?稍後揭曉。

###2、RACReturnSignal
RACReturnSignal+[RACSignal return:] 的內部實現,也是一個私有 RACSignal 子類。它會同步傳送出一個值(即 next)給訂閱者,然後再傳送 completed 事件。 它比 RACEmptySignal 多了一點點東西,它是。直接看其實現:

純吐槽:為啥要叫 ReturnSignal 呢?不如直接 OneValueSignal 好了。O(∩_∩)O~~ 不過說真的,RAC 的命名真心不咋地。

那麼傳送一個 next 後又 completed 的訊號又有啥用呢?等下會知道地。

##2、concat: 練手
-[RACSignal concat:] 是原始碼較簡單,且使用頻率也較多的。那我們們就來拿它來練練手好了。

RACSerialDisposableRACDisposable 的子類,它包含一個 Disposable,能夠在執行時設定這個 Disposable。當設定新的 newDisposable時,老的 oldDisposable 會被 dispose。當 RACSerialDisposabledispose 時,其所包含的 Disposable 會被 dispose

基本上,對一個 RACSignal 的操作的返回值是一個新的 RACSignal 值時,其內部都是呼叫了 +[RACSignal createSignal:] 這個方法。這個建立訊號返回的實際是自定義訊號:RACDynamicSignal,針對它前文有所介紹。

這裡有一個小技巧。因為很多訊號的操作是針對該訊號本身 self 所傳送的值作的操作。那也就是說會訂閱 self,那我們們先找到這一句再說:self subscribe:self subscribeNext:...。嗯,找到了這幾行:

在訂閱了 self 後,將 nexterror 事件傳送給訂閱者 subscriber。當 self 傳送了 completed 事件事,再讓 subscriber 訂閱引數 signal。也就是當源訊號完成後訂閱 signal。怎麼樣,很簡單吧。

##3、zipWith:
再來一個練手的玩意。-[RACSignal zipWith:]-[RACSignal concat:] 稍微複雜點。它是將 self 和 引數 signal 兩個訊號傳送的值合併起來傳送給訂閱者。

 

同樣的,重點在 [self subscriberNext:][signal subscribeNext:] 處。這裡的實現是訂閱 selfsignal 訊號,然後將它們傳送出的值收集起來,當兩個都發出了值時,分別拿出兩個訊號最早發出的值,合併為一個 RACTuple,再傳送給訂閱者 subscriber。這個也很簡單吧,只是程式碼稍多點而已。

##4、bind:
###1、說明
訊號的很多 operations 的實現呼叫來呼叫去最後都是呼叫了這個 -[RACSignal bind:] 方法,比如 flattenMap:map:filter 等等。那我們們就來看看這個方法是哪路神仙?

這是在 RACStream 中宣告的抽象方法。來看看它的宣告:

  RACStreamBindBlock 是一個 block。它從一個 RACStream 中接收一個值,並且返回一個與該流相同型別的例項。如果將 stop 設為 YES,則會在返回一個例項後終結此次 bind。如果返回 nil 則會立即終結。

bind: 方法是將流中每一個值都放到 RACStreamBindBlock 中跑一下。來看看其引數:block。然而這有什麼卵用呢?好吧,我太笨,從它的說明來看,我真的不能理解它有什麼用。

###2、原始碼解讀
既然從方法說明了解不到,那直接來看其原始碼了。

我們一步一步來看。先從第 一 步開始,其步驟如下:
1. 訂閱 self
  2. 針對 self 發出的每一個值 x,經過 bindingBlock,獲取一個訊號:signal
    1. 如果 signal 不為 nil,就轉到第二步:addSignal
    2. 如果 signal 為 nil,或 stopYES,則轉到第三步:completedSignal
  3. 如果 self 發出 error 事件,則中斷訂閱;如果 self 發出 completed 事件則轉到第三步:completedSignal

第二步:addSignal:signal
  1. 先將 signal 新增到 signals
2. 訂閱 signal
    1. 將 signalnext 事件轉發給訂閱者 subscriber
    2. 如果 signal 傳送 error 事件則中斷訂閱
3. 如果 signal 傳送 complete 事件,則轉到第三步

第三步:completeSignal:signal:disposable
  1. 將 signalsignals 中移除
2. 如果 signals 中沒有了 signal,那麼訂閱就完成了

好了,來總結一下這個 -bind:
1. 訂閱原訊號 self 的 values。
2. 將 self 發出的任何一個值,都對其使用 bindingBlock 進行轉換。
3. 如果 bindingBlock 返回一個訊號,則訂閱它,將從它那接收到的每個值都傳遞給訂閱者 subscriber
4. 如果 bindingBlock 要求結束繫結,則 complete self 訊號。
5. 如果 所有 的訊號全都 complete,則給 subscriber 傳送 completed 事件.
6. 如果任何一個訊號發出 error,將其傳送給 subscriber

那從中可以玩出什麼花樣呢?

###3、示例
我們們先用用它,再看看能怎麼玩吧。
####示例1:結合 RACReturnSignal

輸出如下:

這個示例就是在 bind: 中簡單的返回值。那我們們將這個值變化一下如何?

####示例2:結合 RACReturnSignal、轉換 value

輸出如下:

哇哇,這就是個 map: 有木有? 現在,有感受到 RACReturnSignal 的魅力?RACReturnSignal-bind: 結合能轉換 value。

####示例3:結合 RACEmptySignal
  現在來換個玩法試試看,這回換 RACEmptySignal 來玩玩。

輸出如下:

這一次,“outer value” 比 “inner value” 少了一個,這就是 filter: 呀!RACEmptySignalbind: 結合能過濾 value。

####示例4:改進 bind:
  經過這幾個示例,我們可以發現,直接使用 bind: 是比較麻煩的。而一般情況下,我們們還真用不到 stop,那我們們就改進一下唄:

哈哈,這個就是 - flattenMap:了。不必過多解釋了吧~

##5、-map:
  嗯,這其實就是 -flattenMap:RACReturnSignal 的結合:

##6、-flatten
  訊號可以傳送任何型別的值,當然也包括 RACSignal 型別。例如,RACCommandexecutionSignals 這個訊號,它發出的值就是 RACSignal 型別的。對於這種發出的值是 RACSignal 型別的 RACSignal,叫做 signal of signals。這有點類似於 disposable of disposables。

既然這個訊號發出的就是 RACSignal,那在 -flattenMap:中,我們直接將 value 返回就好了。來看看示例:

輸出如下:

##7、小結
RACSignal 的 operations 實在太多,全部在這裡列出來不現實,也沒有這個必要。我相信,經過前面的解析,你現在再去看其它 的一個 operation 原始碼,也應該不是太大的難事。

#RAC() 巨集展開
RAC 的最大的魅力之一就是繫結:RAC(self, ...) = signal; 這應該是大家經常寫的一條語句。有沒有想過它是怎麼工作的呢?我們們來看點程式碼:

重點在 RAC(self, text) = signal; 這一行。先來看看將這個巨集展開是什麼樣子(RAC 對巨集的運用很是牛B,有興趣請看這篇文章):

看得更清楚一點:

跳到 RACSubscriptingAssignmentTrampoline 類的宣告,可以看到:

這個類使用了 clang 的特性,可以使用 []語法([] 的相關文章)。也就是說 assignment[@"text"] = signal;,實際上是這樣子的:

再看 - (void)setObject:(RACSignal *)signal forKeyedSubscript:(NSString *)keyPath; 這個方法的實現,我們發現,它其實呼叫的是 signal 的方法:- (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object nilValue:(id)nilValue,再像上面的方法一樣來分析這個方法,我們找到了關鍵點:

哦,原來它就是訂閱了 signal,並將 signal 發出的每一值都設定給 objectkeyPath 屬性而已。很簡單嘛~

#結束
本文研究了 RAC 中的一些基本元件,並沒有對一些高階內容進行深入研究,所以才叫“淺析”。但這些也是對高階內容深入研究的基礎,既然有“漁”,何懼無“魚”呢?

其實頗想繼續分享,但心有餘而力不足。

還可研究的主題:
1. Subjects 它也是 RACSignal 一些操作的基礎,值得研究。難度係數:2 (最高為5)
2. RACMulticastConnection 常用,值得研究。難度係數:3
3. Foundation、UIKit、KVO (給各系統類加的 rac_ 擴充套件),有研究價值。研究過後,你會對 runtime 會有很深入的瞭解,還會接觸到一些 OC 中少用的知識(如 NSProxy 等),能開拓視野。難度係數:5

相關文章