ReactiveCocoa 中的信號信號在默認(rèn)情況下都是冷的,每次有新的訂閱者訂閱信號時都會執(zhí)行信號創(chuàng)建時傳入的 block;這意味著對于任意一個訂閱者,所需要的數(shù)據(jù)都會重新計算,這在大多數(shù)情況下都是開發(fā)者想看到的情況,但是這在信號中的 block 有副作用或者較為昂貴時就會有很多問題。
我們希望有一種模型能夠?qū)⒗湫盘栟D(zhuǎn)變成熱信號,并在合適的時間觸發(fā),向所有的訂閱者發(fā)送消息;而今天要介紹的 RACMulticastConnection 就是用于解決上述問題的。
RACMulticastConnection 簡介
RACMulticastConnection 封裝了將一個信號的訂閱分享給多個訂閱者的思想,它的每一個對象都持有兩個 RACSignal:
一個是私有的源信號 sourceSignal,另一個是用于廣播的信號 signal,其實(shí)是一個 RACSubject 對象,不過對外只提供 RACSignal 接口,用于使用者通過 -subscribeNext: 等方法進(jìn)行訂閱。
RACMulticastConnection 的初始化
RACMulticastConnection 有一個非常簡單的初始化方法 -initWithSourceSignal:subject:,不過這個初始化方法是私有的:
-
(instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
self = [super init];_sourceSignal = source;
_serialDisposable = [[RACSerialDisposable alloc] init];
_signal = subject;return self;
}
在 RACMulticastConnection 的頭文件的注釋中,對它的初始化有這樣的說明:
Note that you shouldn't create RACMulticastConnection manually. Instead use -[RACSignal publish] or -[RACSignal multicast:].
我們不應(yīng)該直接使用 -initWithSourceSignal:subject: 來初始化一個對象,我們應(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;
}
這兩個方法 -publish 和 -multicast: 都是對初始化方法的封裝,并且都會返回一個 RACMulticastConnection 對象,傳入的 sourceSignal 就是當(dāng)前信號,subject 就是用于對外廣播的 RACSubject 對象。
RACSignal 和 RACMulticastConnection
網(wǎng)絡(luò)請求在客戶端其實(shí)是一個非常昂貴的操作,也算是多級緩存中最慢的一級,在使用 ReactiveCocoa 處理業(yè)務(wù)需求中經(jīng)常會遇到下面的情況:
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ò)請求的信號經(jīng)常會被多次訂閱,以滿足不同 UI 組件更新的需求,但是以上代碼卻有非常嚴(yán)重的問題。
每一次在 RACSignal 上執(zhí)行 -subscribeNext: 以及類似方法時,都會發(fā)起一次新的網(wǎng)絡(luò)請求,我們希望避免這種情況的發(fā)生。
為了解決上述問題,我們使用了 -publish 方法獲得一個多播對象 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];
在這個例子中,我們使用 -publish 方法生成實(shí)例,訂閱者不再訂閱源信號,而是訂閱 RACMulticastConnection 中的 RACSubject 熱信號,最后通過 -connect 方法觸發(fā)源信號中的任務(wù)。
我們再來看一下 -publish 和 -multicast: 這兩個方法的實(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)用時相當(dāng)于向 -multicast: 傳入了 RACSubject
-publish 只是對 -multicast: 方法的簡單封裝,它們都是通過 RACMulticastConnection 私有的初始化方法 -initWithSourceSignal:subject: 創(chuàng)建一個新的實(shí)例。
在使用 -multicast: 方法時,傳入的信號其實(shí)就是用于廣播的信號;這個信號必須是一個 RACSubject 本身或者它的子類:
傳入 -multicast: 方法的一般都是 RACSubject 或者 RACReplaySubject 對象。
訂閱源信號的時間點(diǎn)
訂閱 connection.signal 中的數(shù)據(jù)流時,其實(shí)只是向多播對象中的熱信號 RACSubject 持有的數(shù)組中加入訂閱者,而這時剛剛創(chuàng)建的 RACSubject 中并沒有任何的消息。
只有在調(diào)用 -connect 方法之后,RACSubject 才會訂閱源信號 sourceSignal。
- (RACDisposable *)connect {
self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
return self.serialDisposable;
}
這時源信號的 didSubscribe 代碼塊才會執(zhí)行,向 RACSubject 推送消息,消息向下繼續(xù)傳遞到 RACSubject 所有的訂閱者中。
-connect 方法通過 -subscribe: 實(shí)際上建立了 RACSignal 和 RACSubject 之間的連接,這種方式保證了 RACSignal 中的 didSubscribe 代碼塊只執(zhí)行了一次。
所有的訂閱者不再訂閱原信號,而是訂閱 RACMulticastConnection 持有的熱信號 RACSubject,實(shí)現(xiàn)對冷信號的一對多傳播。
在 RACMulticastConnection 中還有另一個用于連接 RACSignal 和 RACSubject 信號的 -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 方法返回的對象被第一次訂閱時,就會建立源信號與熱信號之間的連接。
使用 RACReplaySubject 訂閱源信號
雖然使用 -publish 方法已經(jīng)能夠解決大部分問題了,但是在 -connect 方法調(diào)用之后才訂閱的訂閱者并不能收到消息。
如何才能保存 didSubscribe 執(zhí)行過程中發(fā)送的消息,并在 -connect 調(diào)用之后也可以收到消息?這時,我們就要使用 -multicast: 方法和 RACReplaySubject 來完成這個需求了。
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);
}];
除了使用上述的代碼,也有一個更簡單的方式創(chuàng)建包含 RACReplaySubject 對象的 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)部封裝的熱信號不同,并在方法調(dià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;