ReactiveCocoa
是一個 iOS
中的函數式響應式編程框架,它受 Functional Reactive Programming 的啟發,是 Justin Spahr-Summers 和 Josh Abernathy 在開發 GitHub for Mac 過程中的一個副產品,它提供了一系列用來組合和轉換值流的 API
。
Mattt Thompson 大神是這樣評價 ReactiveCocoa
的:
Breaking from a tradition of covering Apple APIs exclusively, this edition of NSHipster will look at an open source project that exemplifies this brave new era for Objective-C.
他認為 ReactiveCocoa
打破了蘋果 API
排他性的束縛,勇敢地開創了 Objective-C
的新紀元,具有劃時代的意義。不得不說,這對於一個第三方框架來說,已經是非常高的評價了。
關於 ReactiveCocoa
的版本演進歷程,簡單介紹如下:
<= v2.5
:Objective-C
;
v3.x
:Swift 1.2
;
v4.x
:Swift 2.x
。
注:本文所介紹的均為 ReactiveCocoa v2.5
版本中的內容,這是 Objective-C
最新的穩定版本。另外,本文的目錄結構如下:
簡介
信號源
RACStream
RACSignal
RACSubject
RACSequence
訂閱者
RACSubscriber
RACMulticastConnection
調度器
RACScheduler
清潔工
RACDisposable
總結
參考鏈接
ReactiveCocoa
是一個非常復雜的框架,在正式開始介紹它的核心組件前,我們先來看看它的類圖,以便從宏觀上了解它的層次結構:
從上面的類圖中,我們可以看出,ReactiveCocoa
主要由以下四大核心組件構成:
信號源:RACStream
及其子類;
訂閱者:RACSubscriber
的實現類及其子類;
調度器:RACScheduler
及其子類;
清潔工:RACDisposable
及其子類。
其中,信號源又是最核心的部分,其他組件都是圍繞它運作的。
對於一個應用來說,絕大部分的時間都是在等待某些事件的發生或響應某些狀態的變化,比如用戶的觸摸事件、應用進入後台、網絡請求成功刷新界面等等,而維護這些狀態的變化,常常會使代碼變得非常復雜,難以擴展。而 ReactiveCocoa
給出了一種非常好的解決方案,它使用信號來代表這些異步事件,提供了一種統一的方式來處理所有異步的行為,包括代理方法、block
回調、target-action
機制、通知、KVO
等:
// 代理方法 [[self rac_signalForSelector:@selector(webViewDidStartLoad:) fromProtocol:@protocol(UIWebViewDelegate)] subscribeNext:^(id x) { // 實現 webViewDidStartLoad: 代理方法 }]; // target-action [[self.avatarButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *avatarButton) { // avatarButton 被點擊了 } // 通知 [[[NSNotificationCenter defaultCenter] rac_addObserverForName:kReachabilityChangedNotification object:nil] subscribeNext:^(NSNotification *notification) { // 收到 kReachabilityChangedNotification 通知 }]; // KVO [RACObserve(self, username) subscribeNext:^(NSString *username) { // 用戶名發生了變化 }];
然而,這些還只是 ReactiveCocoa
的冰山一角,它真正強大的地方在於我們可以對這些不同的信號進行任意地組合和鏈式操作,從最原始的輸入 input
開始直至得到最終的輸出 output
為止:
[[[RACSignal combineLatest:@[ RACObserve(self, username), RACObserve(self, password) ] reduce:^(NSString *username, NSString *password) { return @(username.length > 0 && password.length > 0); }] distinctUntilChanged] subscribeNext:^(NSNumber *valid) { if (valid.boolValue) { // 用戶名和密碼合法,登錄按鈕可用 } else { // 用戶名或密碼不合法,登錄按鈕不可用 } }];
因此,對於 ReactiveCocoa
來說,我們可以毫不誇張地說,阻礙它發揮的瓶頸就只剩下你的想象力了。
在 ReactiveCocoa
中,信號源代表的是隨著時間而改變的值流,這是對 ReactiveCocoa
最精准的概括,訂閱者可以通過訂閱信號源來獲取這些值:
Streams of values over time.
你可以把它想象成水龍頭中的水,當你打開水龍頭時,水源源不斷地流出來;你也可以把它想象成電,當你插上插頭時,電靜靜地充到你的手機上;你還可以把它想象成運送玻璃珠的管道,當你打開閥門時,珠子一個接一個地到達。這裡的水、電、玻璃珠就是我們所需要的值,而打開水龍頭、插上插頭、打開閥門就是訂閱它們的過程。
RACStream
是 ReactiveCocoa
中最核心的類,代表的是任意的值流,它是整個 ReactiveCocoa
得以建立的基石,下面是它的繼承結構圖:
事實上,RACStream
是一個抽象類,通常情況下,我們並不會去實例化它,而是直接使用它的兩個子類 RACSignal
和 RACSequence
。那麼,問題來了,為什麼 RACStream
會被設計成一個抽象類?或者說它的抽象過程是以什麼作為依據的呢?
是的,沒錯,看過我上一篇文章 《Functor、Applicative 和 Monad》 的同學,應該已經知道了,RACStream
就是以 Monad
的概念為依據進行設計的,它代表的就是一個 Monad
:
/// An abstract class representing any stream of values. /// /// This class represents a monad, upon which many stream-based operations can /// be built. /// /// When subclassing RACStream, only the methods in the main @interface body need /// to be overridden. @interface RACStream : NSObject /// Lifts `value` into the stream monad. /// /// Returns a stream containing only the given value. + (instancetype)return:(id)value; /// Lazily binds a block to the values in the receiver. /// /// This should only be used if you need to terminate the bind early, or close /// over some state. -flattenMap: is more appropriate for all other cases. /// /// block - A block returning a RACStreamBindBlock. This block will be invoked /// each time the bound stream is re-evaluated. This block must not be /// nil or return nil. /// /// Returns a new stream which represents the combined result of all lazy /// applications of `block`. - (instancetype)bind:(RACStreamBindBlock (^)(void))block; @end
有了 Monad
作為基石後,許多基於流的操作就可以被建立起來了,比如 map
、filter
、zip
等。
RACSignal
代表的是未來將會被傳送的值,它是一種 push-driven
的流。RACSignal
可以向訂閱者發送三種不同類型的事件:
next
:RACSignal
通過 next
事件向訂閱者傳送新的值,並且這個值可以為 nil
;
error
:RACSignal
通過 error
事件向訂閱者表明信號在正常結束前發生了錯誤;
completed
:RACSignal
通過 completed
事件向訂閱者表明信號已經正常結束,不會再有後續的值傳送給訂閱者。
注意,ReactiveCocoa
中的值流只包含正常的值,即通過 next
事件傳送的值,並不包括 error
和 completed
事件,它們需要被特殊處理。通常情況下,一個信號的生命周期是由任意個 next
事件和一個 error
事件或一個 completed
事件組成的。
從前面的類圖中,我們可以看出,RACSignal
並非只有一個類,事實上,它的一系列功能是通過類簇來實現的。除去我們將在下節介紹的 RACSubject
及其子類外,RACSignal
還有五個用來實現不同功能的私有子類:
RACEmptySignal
:空信號,用來實現 RACSignal
的 +empty
方法;
RACReturnSignal
:一元信號,用來實現 RACSignal
的 +return:
方法;
RACDynamicSignal
:動態信號,使用一個 block
來實現訂閱行為,我們在使用 RACSignal
的 +createSignal:
方法時創建的就是該類的實例;
RACErrorSignal
:錯誤信號,用來實現 RACSignal
的 +error:
方法;
RACChannelTerminal
:通道終端,代表 RACChannel
的一個終端,用來實現雙向綁定。
對於 RACSignal
類簇來說,最核心的方法莫過於 -subscribe:
了,這個方法封裝了訂閱者對信號源的一次訂閱過程,它是訂閱者與信號源產生聯系的唯一入口。因此,對於 RACSignal
的所有子類來說,這個方法的實現邏輯就代表了該子類的具體訂閱行為,是區分不同子類的關鍵所在。同時,這也是為什麼 RACSignal
中的 -subscribe:
方法是一個抽象方法,並且必須要讓子類實現的原因:
- (RACDisposable *)subscribe:(id)subscriber { NSCAssert(NO, @"This method must be overridden by subclasses"); return nil; }
RACSubject
RACSubject
代表的是可以手動控制的信號,我們可以把它看作是 RACSignal
的可變版本,就好比 NSMutableArray
是 NSArray
的可變版本一樣。RACSubject
繼承自 RACSignal
,所以它可以作為信號源被訂閱者訂閱,同時,它又實現了 RACSubscriber
協議,所以它也可以作為訂閱者訂閱其他信號源,這個就是 RACSubject
為什麼可以手動控制的原因:
根據官方的 Design Guidelines 中的說法,我們應該盡可能少地使用它。因為它太過靈活,我們可以在任何時候任何地方操作它,所以一旦過度使用,就會使代碼變得非常復雜,難以理解。
根據我的實際使用經驗,在 MVVM
中使用 RACSubject
可以非常方便地實現統一的錯誤處理邏輯。比如,我們可以在 viewModel
的基類中聲明一個 RACSubject
類型的屬性 errors
,然後在 viewController
的基類中編寫統一的錯誤處理邏輯:
[self.viewModel.errors subscribeNext:^(NSError *error) { // 錯誤處理邏輯 }
此時,假設在某個界面的 viewModel
中有三個用來請求遠程數據的命令,分別是 requestReadmeMarkdownCommand
、requestBlobCommand
和 requestReadmeHTMLCommand
,那麼這個界面的錯誤處理邏輯就可以這麼寫:
[[RACSignal merge:@[ self.requestReadmeMarkdownCommand.errors, self.requestBlobCommand.errors, self.requestReadmeHTMLCommand.errors ]] subscribe:self.errors];
另外,RACSubject
也有三個用來實現不同功能的子類:
RACGroupedSignal
:分組信號,用來實現 RACSignal
的分組功能;
RACBehaviorSubject
:重演最後值的信號,當被訂閱時,會向訂閱者發送它最後接收到的值;
RACReplaySubject
:重演信號,保存發送過的值,當被訂閱時,會向訂閱者重新發送這些值。
RACSubject
的功能非常強大,但是太過靈活,也正是因為如此,我們只有在迫不得已的情況下才會使用它。
RACSequence
代表的是一個不可變的值的序列,與 RACSignal
不同,它是 pull-driven
類型的流。從嚴格意義上講,RACSequence
並不能算作是信號源,因為它並不能像 RACSignal
那樣,可以被訂閱者訂閱,但是它與 RACSignal
之間可以非常方便地進行轉換。
從理論上說,一個 RACSequence
由兩部分組成:
head
:指的是序列中的第一個對象,如果序列為空,則為 nil
;
tail
:指的是序列中除第一個對象外的其它所有對象,同樣的,如果序列為空,則為 nil
。
事實上,一個序列的 tail
仍然是一個序列,如果我們將序列看作是一條毛毛蟲,那麼 head
和 tail
可表示如下:
同樣的,一個序列的 tail
也可以看作是由 head
和 tail
組成,而這個新的 tail
又可以繼續看作是由 head
和 tail
組成,這個過程可以一直進行下去。而這個就是 RACSequence
得以建立的理論基礎,所以一個 RACSequence
子類的最小實現就是 head
和 tail
:
/// Represents an immutable sequence of values. Unless otherwise specified, the /// sequences' values are evaluated lazily on demand. Like Cocoa collections, /// sequences cannot contain nil. /// /// Most inherited RACStream methods that accept a block will execute the block /// _at most_ once for each value that is evaluated in the returned sequence. /// Side effects are subject to the behavior described in /// +sequenceWithHeadBlock:tailBlock:. /// /// Implemented as a class cluster. A minimal implementation for a subclass /// consists simply of -head and -tail. @interface RACSequence : RACStream /// The first object in the sequence, or nil if the sequence is empty. /// /// Subclasses must provide an implementation of this method. @property (nonatomic, strong, readonly) id head; /// All but the first object in the sequence, or nil if the sequence is empty. /// /// Subclasses must provide an implementation of this method. @property (nonatomic, strong, readonly) RACSequence *tail; @end
總的來說,RACSequence
存在的最大意義就是為了簡化 Objective-C
中的集合操作:
Simplifying Collection Transformations: Higher-order functions like map, filter, fold/reduce are sorely missing from Foundation.
比如下面的代碼:
NSMutableArray *results = [NSMutableArray array]; for (NSString *str in strings) { if (str.length < 2) { continue; } NSString *newString = [str stringByAppendingString:@"foobar"]; [results addObject:newString]; }
可以用 RACSequence
來優雅地實現:
RACSequence *results = [[strings.rac_sequence filter:^ BOOL (NSString *str) { return str.length >= 2; }] map:^(NSString *str) { return [str stringByAppendingString:@"foobar"]; }];
因此,我們可以非常方便地使用 RACSequence
來實現集合的鏈式操作,直到得到你想要的最終結果為止。除此之外,使用 RACSequence
的另外一個主要好處是,RACSequence
中包含的值在默認情況下是懶計算的,即只有在真正用到的時候才會被計算,並且只會計算一次。也就是說,如果我們只用到了一個 RACSequence
中的部分值的時候,它就在不知不覺中提高了我們應用的性能。
同樣的,RACSequence
的一系列功能也是通過類簇來實現的,它共有九個用來實現不同功能的私有子類:
RACUnarySequence
:一元序列,用來實現 RACSequence
的 +return:
方法;
RACIndexSetSequence
:用來遍歷索引集;
RACEmptySequence
:空序列,用來實現 RACSequence
的 +empty
方法;
RACDynamicSequence
:動態序列,使用 blocks
來動態地實現一個序列;
RACSignalSequence
:用來遍歷信號中的值;
RACArraySequence
:用來遍歷數組中的元素;
RACEagerSequence
:非懶計算的序列,在初始化時立即計算所有的值;
RACStringSequence
:用來遍歷字符串中的字符;
RACTupleSequence
:用來遍歷元組中的元素。
RACSequence
為類簇提供了統一的對外接口,對於使用它的客戶端代碼來說,完全不需要知道私有子類的存在,很好地隱藏了實現細節。另外,值得一提的是,RACSequence
實現了快速枚舉的協議 NSFastEnumeration
,在這個協議中只聲明了一個看上去非常抽筋的方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;
有興趣的同學,可以看看 RACSequence
中的相關實現,我們將會在後續的文章中進行介紹。因此,我們也可以直接使用 for in
來遍歷一個 RACSequence
。
現在,我們已經知道信號源是什麼了,為了獲取信號源中的值,我們需要對信號源進行訂閱。在 ReactiveCocoa
中,訂閱者是一個抽象的概念,所有實現了 RACSubscriber
協議的類都可以作為信號源的訂閱者。
在 RACSubscriber
協議中,聲明了四個必須實現的方法:
/// Represents any object which can directly receive values from a RACSignal. /// /// You generally shouldn't need to implement this protocol. +[RACSignal /// createSignal:], RACSignal's subscription methods, or RACSubject should work /// for most uses. /// /// Implementors of this protocol may receive messages and values from multiple /// threads simultaneously, and so should be thread-safe. Subscribers will also /// be weakly referenced so implementations must allow that. @protocol RACSubscriber @required /// Sends the next value to subscribers. /// /// value - The value to send. This can be `nil`. - (void)sendNext:(id)value; /// Sends the error to subscribers. /// /// error - The error to send. This can be `nil`. /// /// This terminates the subscription, and invalidates the subscriber (such that /// it cannot subscribe to anything else in the future). - (void)sendError:(NSError *)error; /// Sends completed to subscribers. /// /// This terminates the subscription, and invalidates the subscriber (such that /// it cannot subscribe to anything else in the future). - (void)sendCompleted; /// Sends the subscriber a disposable that represents one of its subscriptions. /// /// A subscriber may receive multiple disposables if it gets subscribed to /// multiple signals; however, any error or completed events must terminate _all_ /// subscriptions. - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable; @end
其中 -sendNext:
、-sendError:
和 -sendCompleted
分別用來從 RACSignal
接收 next
、error
和 completed
事件,而 -didSubscribeWithDisposable:
則用來接收代表某次訂閱的 disposable
對象。
訂閱者對信號源的一次訂閱過程可以抽象為:通過 RACSignal
的 -subscribe:
方法傳入一個訂閱者,並最終返回一個 RACDisposable
對象的過程:
注意:在 ReactiveCocoa
中並沒有專門的類 RACSubscription
來代表一次訂閱,而間接地使用 RACDisposable
來充當這一角色。因此,一個 RACDisposable
對象就代表著一次訂閱,並且我們可以用它來取消這次訂閱,詳細內容將會在下面的章節中進行介紹。
除了 RACSignal
的子類外,還有兩個實現了 RACSubscriber
協議的類,如下圖所示:
其中,RACSubscriber
類的名字與 RACSubscriber
協議的名字相同,這跟 Objective-C
中的 NSObject
類的名字與 NSObject
協議的名字相同是一樣一樣的,除了名字相同外,然並卵。通常來說,RACSubscriber
類充當的角色就是信號源的真正訂閱者,它老老實實地實現了 RACSubscriber
協議。
既然 RACSubscriber
類就是真正的訂閱者,那麼 RACPassthroughSubscriber
類又是干嘛用的呢?原來,在 ReactiveCocoa
中,一個訂閱者是可以訂閱多個信號源的,也就是說它會擁有多個 RACDisposable
對象,並且它可以隨時取消其中任何一個訂閱。為了實現這個功能,ReactiveCocoa
就引入了 RACPassthroughSubscriber
類,它是 RACSubscriber
類的一個裝飾器,封裝了一個真正的訂閱者 RACSubscriber
對象,它負責轉發所有事件給這個真正的訂閱者,而當此次訂閱被取消時,它就會停止轉發:
- (void)sendNext:(id)value { if (self.disposable.disposed) return; [self.innerSubscriber sendNext:value]; } - (void)sendError:(NSError *)error { if (self.disposable.disposed) return; [self.innerSubscriber sendError:error]; } - (void)sendCompleted { if (self.disposable.disposed) return; [self.innerSubscriber sendCompleted]; }
事實上,在 ReactiveCocoa
中,我們傾向於隱藏訂閱者,因為外界根本不需要知道訂閱者的存在,這是內部的實現細節。這樣做的主要目的是進一步簡化信號源的訂閱邏輯,客戶端代碼只需要關心它所需要的值就可以了,根本不需要關心內部的訂閱過程。
通常來說,我們在訂閱一個信號源的過程中可能會產生副作用或者消耗比較大的資源,比如修改全局變量、發送網絡請求等。這個時候,我們往往需要讓多個訂閱者之間共享一次訂閱,就好比我們讀高中時,多個好朋友一起訂閱一份英語周報,然後只要出一份錢,是一個道理。這就是 ReactiveCocoa
中引入 RACMulticastConnection
類的原因。
RACMulticastConnection
通過一個標志 _hasConnected
來保證只對 sourceSignal
訂閱一次,然後對外暴露一個 RACSubject
類型的 signal
供外部訂閱者訂閱。這樣一來,不管外部訂閱者對 signal
訂閱多少次,我們對 sourceSignal
的訂閱至多只會有一次:
注:了解 RACMulticastConnection
的實現原理,對於我們後面理解 -replay
、replayLast
和 replayLazily
等方法非常有幫助。
有了信號源和訂閱者,我們還需要由調度器來統一調度訂閱者訂閱信號源的過程中所涉及到的任務,這樣才能保證所有的任務都能夠合理有序地執行。
RACScheduler
在 ReactiveCocoa
中就是扮演著調度器的角色,本質上,它就是用 GCD
的串行隊列來實現的,並且支持取消操作。是的,在 ReactiveCocoa
中,並沒有使用到 NSOperationQueue
和 NSRunloop
等技術,RACScheduler
也只是對 GCD
的簡單封裝而已。
同樣的,RACScheduler
的一系列功能也是通過類簇來實現的,除了用來測試的子類外,總共還有四個私有子類:
咋看之下,RACScheduler
的兒子貌似還不少,但是真正出力干活的卻真心不多,主要就是 RACTargetQueueScheduler
子類:
RACImmediateScheduler
:立即執行調度的任務,這是唯一一個支持同步執行的調度器;
RACQueueScheduler
:一個抽象的隊列調度器,在一個 GCD
串行列隊中異步調度所有任務;
RACTargetQueueScheduler
:繼承自 RACQueueScheduler
,在一個以一個任意的 GCD
隊列為 target
的串行隊列中異步調度所有任務;
RACSubscriptionScheduler
:一個只用來調度訂閱的調度器。
值得一提的是,在 RACScheduler
中有一個非常特殊的方法:
- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock;
這個方法的作用非常有意思,它可以將遞歸調用轉換成迭代調用,這樣做的目的是為了解決深層次的遞歸調用可能會帶來的堆棧溢出問題。
正如我們前面所說的,在訂閱者訂閱信號源的過程中,可能會產生副作用或者消耗一定的資源,所以當我們在取消訂閱或者完成訂閱時,我們就需要做一些資源回收和垃圾清理的工作。
RACDisposable
在 ReactiveCocoa
中就充當著清潔工的角色,它封裝了取消和清理一次訂閱所必需的工作。它有一個核心的方法 -dispose
,調用這個方法就會執行相應的清理工作,這有點類似於 NSObject
的 -dealloc
方法。RACDisposable
總共有四個子類,它的繼承結構圖如下:
RACSerialDisposable
:作為 disposable
的容器使用,可以包含一個 disposable
對象,並且允許將這個 disposable
對象通過原子操作交換出來;
RACKVOTrampoline
:代表一次 KVO
觀察,並且可以用來停止觀察;
RACCompoundDisposable
:跟 RACSerialDisposable
一樣,RACCompoundDisposable
也是作為 disposable
的容器使用。不同的是,它可以包含多個 disposable
對象,並且支持手動添加和移除 disposable
對象,有點類似於可變數組 NSMutableArray
。而當一個 RACCompoundDisposable
對象被 disposed
時,它會調用其所包含的所有 disposable
對象的 -dispose
方法,有點類似於 autoreleasepool
的作用;
RACScopedDisposable
:當它被 dealloc
的時候調用本身的 -dispose
方法。
咋看之下,RACDisposable
的邏輯似乎有些復雜,不過換湯不換藥,不管它們怎麼換著花樣玩,最終都只是為了能夠在合適的時機調用 disposable
對象的 -dispose
方法,執行清理工作而已。
至此,我們介紹完了 ReactiveCocoa
的四大核心組件,對它的架構有了宏觀上的認識。它建立於 Monad
的概念之上,然後圍繞其搭建了一系列完整的配套組件,它們共同支撐了 ReactiveCocoa
的強大功能。盡管,ReactiveCocoa
是一個重型的函數式響應式框架,但是它並不會對我們現有的代碼構成侵略性,我們完全可以在一個單獨的類中使用它,哪怕只是簡單的一行代碼,也是沒有問題的。所以,如果你對 ReactiveCocoa
感興趣的話,不妨就從現在開始嘗試吧,Let’s go !
PS:MVVMReactiveCocoa 是我用 MVVM
+ RAC
編寫的一個開源應用,如果你有興趣的話不妨 clone
下來看看 ReactiveCocoa
的具體實踐吧。
https://github.com/ReactiveCocoa/ReactiveCocoa/tree/v2.5
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/Documentation/FrameworkOverview.md
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/Documentation/DesignGuidelines.md#avoid-using-subjects-when-possible
http://nshipster.com/reactivecocoa/
http://nathanli.cn/2015/08/27/reactivecocoa2-%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90/
http://blog.devtang.com/blog/2014/02/11/reactivecocoa-introduction/
http://m.oschina.net/blog/294178