ReactiveCocoa 中的信號(hào)信號(hào)在默認(rèn)情況下都是冷的,每次有新的訂閱者訂閱信號(hào)時(shí)都會(huì)執(zhí)行信號(hào)創(chuàng)建時(shí)傳入的 block;這意味著對(duì)于任意一個(gè)訂閱者,所需要的數(shù)據(jù)都會(huì)重新計(jì)算,這在大多數(shù)情況下都是開發(fā)者想看到的情況,但是這在信號(hào)中的 block 有副作用或者較為昂貴時(shí)就會(huì)有很多問題。
我們希望有一種模型能夠?qū)⒗湫盘?hào)轉(zhuǎn)變成熱信號(hào),并在合適的時(shí)間觸發(fā),向所有的訂閱者發(fā)送消息;而今天要介紹的 RACMulticastConnection
就是用于解決上述問題的。
RACMulticastConnection 簡介
RACMulticastConnection
封裝了將一個(gè)信號(hào)的訂閱分享給多個(gè)訂閱者的思想,它的每一個(gè)對(duì)象都持有兩個(gè) RACSignal
:
一個(gè)是私有的源信號(hào) sourceSignal
,另一個(gè)是用于廣播的信號(hào) signal
,其實(shí)是一個(gè) RACSubject
對(duì)象,不過對(duì)外只提供 RACSignal
接口,用于使用者通過 -subscribeNext:
等方法進(jìn)行訂閱。
RACMulticastConnection 的初始化
RACMulticastConnection
有一個(gè)非常簡單的初始化方法 -initWithSourceSignal:subject:
,不過這個(gè)初始化方法是私有的:
- (instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
self = [super init];
_sourceSignal = source;
_serialDisposable = [[RACSerialDisposable alloc] init];
_signal = subject;
return self;
}
在 RACMulticastConnection
的頭文件的注釋中,對(duì)它的初始化有這樣的說明:
Note that you shouldn't create RACMulticastConnection manually. Instead use -[RACSignal publish] or -[RACSignal multicast:].
我們不應(yīng)該直接使用 -initWithSourceSignal:subject:
來初始化一個(gè)對(duì)象,我們應(yīng)該通過 RACSignal
的實(shí)例方法初始化 RACMulticastConnection
實(shí)例。
- (RACMulticastConnection *)publish {
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *connection = [self multicast:subject];
return connection;
}
- (RACMulticastConnection *)multicast:(RACSubject *)subject {
RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
return connection;
}
這兩個(gè)方法 -publish
和 -multicast:
都是對(duì)初始化方法的封裝,并且都會(huì)返回一個(gè) RACMulticastConnection
對(duì)象,傳入的 sourceSignal
就是當(dāng)前信號(hào),subject
就是用于對(duì)外廣播的 RACSubject
對(duì)象。
RACSignal 和 RACMulticastConnection
網(wǎng)絡(luò)請(qǐng)求在客戶端其實(shí)是一個(gè)非常昂貴的操作,也算是多級(jí)緩存中最慢的一級(jí),在使用 ReactiveCocoa 處理業(yè)務(wù)需求中經(jīng)常會(huì)遇到下面的情況:
RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"Send Request");
NSURL *url = [NSURL URLWithString:@"http://localhost:3000"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
NSString *URLString = [NSString stringWithFormat:@"/api/products/1"];
NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
[requestSignal subscribeNext:^(id _Nullable x) {
NSLog(@"product: %@", x);
}];
[requestSignal subscribeNext:^(id _Nullable x) {
NSNumber *productId = [x objectForKey:@"id"];
NSLog(@"productId: %@", productId);
}];
通過訂閱發(fā)出網(wǎng)絡(luò)請(qǐng)求的信號(hào)經(jīng)常會(huì)被多次訂閱,以滿足不同 UI 組件更新的需求,但是以上代碼卻有非常嚴(yán)重的問題。
每一次在 RACSignal
上執(zhí)行 -subscribeNext:
以及類似方法時(shí),都會(huì)發(fā)起一次新的網(wǎng)絡(luò)請(qǐng)求,我們希望避免這種情況的發(fā)生。
為了解決上述問題,我們使用了 -publish
方法獲得一個(gè)多播對(duì)象 RACMulticastConnection
,更改后的代碼如下:
RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"Send Request");
...
}] publish];
[connection.signal subscribeNext:^(id _Nullable x) {
NSLog(@"product: %@", x);
}];
[connection.signal subscribeNext:^(id _Nullable x) {
NSNumber *productId = [x objectForKey:@"id"];
NSLog(@"productId: %@", productId);
}];
[connection connect];
在這個(gè)例子中,我們使用 -publish
方法生成實(shí)例,訂閱者不再訂閱源信號(hào),而是訂閱 RACMulticastConnection
中的 RACSubject
熱信號(hào),最后通過 -connect
方法觸發(fā)源信號(hào)中的任務(wù)。
對(duì)于熱信號(hào)不了解的讀者,可以閱讀這篇文章 『可變』的熱信號(hào) RACSubject。
publish 和 multicast 方法
我們?cè)賮砜匆幌?-publish
和 -multicast:
這兩個(gè)方法的實(shí)現(xiàn):
- (RACMulticastConnection *)publish {
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *connection = [self multicast:subject];
return connection;
}
- (RACMulticastConnection *)multicast:(RACSubject *)subject {
RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
return connection;
}
當(dāng) -publish
方法調(diào)用時(shí)相當(dāng)于向 -multicast:
傳入了 RACSubject
。
-publish
只是對(duì) -multicast:
方法的簡單封裝,它們都是通過 RACMulticastConnection
私有的初始化方法 -initWithSourceSignal:subject:
創(chuàng)建一個(gè)新的實(shí)例。
在使用 -multicast:
方法時(shí),傳入的信號(hào)其實(shí)就是用于廣播的信號(hào);這個(gè)信號(hào)必須是一個(gè) RACSubject
本身或者它的子類:

傳入 -multicast:
方法的一般都是 RACSubject
或者 RACReplaySubject
對(duì)象。
訂閱源信號(hào)的時(shí)間點(diǎn)
訂閱 connection.signal
中的數(shù)據(jù)流時(shí),其實(shí)只是向多播對(duì)象中的熱信號(hào) RACSubject
持有的數(shù)組中加入訂閱者,而這時(shí)剛剛創(chuàng)建的 RACSubject
中并沒有任何的消息。
只有在調(diào)用 -connect
方法之后,RACSubject
才會(huì)訂閱源信號(hào) sourceSignal
。
- (RACDisposable *)connect {
self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
return self.serialDisposable;
}
這時(shí)源信號(hào)的 didSubscribe
代碼塊才會(huì)執(zhí)行,向 RACSubject
推送消息,消息向下繼續(xù)傳遞到 RACSubject
所有的訂閱者中。
-connect
方法通過 -subscribe:
實(shí)際上建立了 RACSignal
和 RACSubject
之間的連接,這種方式保證了 RACSignal
中的 didSubscribe
代碼塊只執(zhí)行了一次。
所有的訂閱者不再訂閱原信號(hào),而是訂閱 RACMulticastConnection
持有的熱信號(hào) RACSubject
,實(shí)現(xiàn)對(duì)冷信號(hào)的一對(duì)多傳播。
在 RACMulticastConnection
中還有另一個(gè)用于連接 RACSignal
和 RACSubject
信號(hào)的 -autoconnect
方法:
- (RACSignal *)autoconnect {
__block volatile int32_t subscriberCount = 0;
return [RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
OSAtomicIncrement32Barrier(&subscriberCount);
RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber];
RACDisposable *connectionDisposable = [self connect];
return [RACDisposable disposableWithBlock:^{
[subscriptionDisposable dispose];
if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) {
[connectionDisposable dispose];
}
}];
}];
}
它保證了在 -autoconnect
方法返回的對(duì)象被第一次訂閱時(shí),就會(huì)建立源信號(hào)與熱信號(hào)之間的連接。
使用 RACReplaySubject 訂閱源信號(hào)
雖然使用 -publish
方法已經(jīng)能夠解決大部分問題了,但是在 -connect
方法調(diào)用之后才訂閱的訂閱者并不能收到消息。
如何才能保存 didSubscribe
執(zhí)行過程中發(fā)送的消息,并在 -connect
調(diào)用之后也可以收到消息?這時(shí),我們就要使用 -multicast:
方法和 RACReplaySubject
來完成這個(gè)需求了。
RACSignal *sourceSignal = [RACSignal createSignal:...];
RACMulticastConnection *connection = [sourceSignal multicast:[RACReplaySubject subject]];
[connection.signal subscribeNext:^(id _Nullable x) {
NSLog(@"product: %@", x);
}];
[connection connect];
[connection.signal subscribeNext:^(id _Nullable x) {
NSNumber *productId = [x objectForKey:@"id"];
NSLog(@"productId: %@", productId);
}];
除了使用上述的代碼,也有一個(gè)更簡單的方式創(chuàng)建包含 RACReplaySubject
對(duì)象的 RACMulticastConnection
:
RACSignal *signal = [[RACSignal createSignal:...] replay];
[signal subscribeNext:^(id _Nullable x) {
NSLog(@"product: %@", x);
}];
[signal subscribeNext:^(id _Nullable x) {
NSNumber *productId = [x objectForKey:@"id"];
NSLog(@"productId: %@", productId);
}];
-replay
方法和 -publish
差不多,只是內(nèi)部封裝的熱信號(hào)不同,并在方法調(diào)用時(shí)就連接原信號(hào):
- (RACSignal *)replay {
RACReplaySubject *subject = [RACReplaySubject subject];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
除了 -replay
方法,RACSignal
中還定義了與 RACMulticastConnection
中相關(guān)的其它 -replay
方法:
- (RACSignal<ValueType> *)replay;
- (RACSignal<ValueType> *)replayLast;
- (RACSignal<ValueType> *)replayLazily;
三個(gè)方法都會(huì)在 RACMulticastConnection
初始化時(shí)傳入一個(gè) RACReplaySubject
對(duì)象,不過卻有一點(diǎn)細(xì)微的差別:
相比于 -replay
方法,-replayLast
方法生成的 RACMulticastConnection
中熱信號(hào)的容量為 1
:
- (RACSignal *)replayLast {
RACReplaySubject *subject = [RACReplaySubject replaySubjectWithCapacity:1];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
而 replayLazily
會(huì)在返回的信號(hào)被第一次訂閱時(shí),才會(huì)執(zhí)行 -connect
方法:
- (RACSignal *)replayLazily {
RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];
return [RACSignal
defer:^{
[connection connect];
return connection.signal;
}];
}
總結(jié)
RACMulticastConnection
在處理冷熱信號(hào)相互轉(zhuǎn)換時(shí)非常好用,在 RACSignal
中也提供了很多將原有的冷信號(hào)通過 RACMulticastConnection
轉(zhuǎn)換成熱信號(hào)的方法。
在遇到冷信號(hào)中的行為有副作用后者非常昂貴時(shí),我們就可以使用這些方法將單播變成多播,提高執(zhí)行效率,減少副作用。
References
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · GitHub