用于多播的 RACMulticastConnection
ReactiveCocoa 中的信號信號在默認情況下都是冷的,每次有新的訂閱者訂閱信號時都會執行信號創建時傳入的 block;這意味著對于任意一個訂閱者,所需要的數據都會 重新計算 ,這在大多數情況下都是開發者想看到的情況,但是這在信號中的 block 有副作用或者較為昂貴時就會有很多問題。
RACMulticastConnection
我們希望有一種模型能夠將冷信號轉變成熱信號,并在合適的時間觸發,向所有的訂閱者發送消息;而今天要介紹的 RACMulticastConnection 就是用于解決上述問題的。
RACMulticastConnection 簡介
RACMulticastConnection 封裝了將一個信號的訂閱分享給多個訂閱者的思想,它的每一個對象都持有兩個 RACSignal :
RACMulticastConnection-Interface
一個是私有的源信號 sourceSignal ,另一個是用于廣播的信號 signal ,其實是一個 RACSubject 對象,不過對外只提供 RACSignal 接口,用于使用者通過 -subscribeNext: 等方法進行訂閱。
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:].
我們不應該直接使用 -initWithSourceSignal:subject: 來初始化一個對象,我們應該通過 RACSignal 的實例方法初始化 RACMulticastConnection 實例。
- (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 就是當前信號, subject 就是用于對外廣播的 RACSubject 對象。
RACSignal 和 RACMulticastConnection
網絡請求在客戶端其實是一個非常昂貴的操作,也算是多級緩存中最慢的一級,在使用 ReactiveCocoa 處理業務需求中經常會遇到下面的情況:
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);
}];
通過訂閱發出網絡請求的信號經常會被多次訂閱,以滿足不同 UI 組件更新的需求,但是以上代碼卻有非常嚴重的問題。
RACSignal-And-Subscribe
每一次在 RACSignal 上執行 -subscribeNext: 以及類似方法時,都會發起一次新的網絡請求,我們希望避免這種情況的發生。
為了解決上述問題,我們使用了 -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 方法生成實例,訂閱者不再訂閱源信號,而是訂閱 RACMulticastConnection 中的 RACSubject 熱信號,最后通過 -connect 方法觸發源信號中的任務。
RACSignal-RACMulticastConnection-Connect
對于熱信號不了解的讀者,可以閱讀這篇文章 『可變』的熱信號 RACSubject 。
publish 和 multicast 方法
我們再來看一下 -publish 和 -multicast: 這兩個方法的實現:
- (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: 傳入了 RACSubject 。
publish-and-multicast
-publish 只是對 -multicast: 方法的簡單封裝,它們都是通過 RACMulticastConnection 私有的初始化方法 -initWithSourceSignal:subject: 創建一個新的實例。
在使用 -multicast: 方法時,傳入的信號其實就是用于廣播的信號;這個信號必須是一個 RACSubject 本身或者它的子類:
RACSubject - Subclasses
傳入 -multicast: 方法的一般都是 RACSubject 或者 RACReplaySubject 對象。
訂閱源信號的時間點
訂閱 connection.signal 中的數據流時,其實只是向多播對象中的熱信號 RACSubject 持有的數組中加入訂閱者,而這時剛剛創建的 RACSubject 中并沒有任何的消息。
SubscribeNext-To-RACSubject-Before-Connect
只有在調用 -connect 方法之后, RACSubject 才會 訂閱 源信號 sourceSignal 。
- (RACDisposable *)connect {
self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
return self.serialDisposable;
}
這時源信號的 didSubscribe 代碼塊才會執行,向 RACSubject 推送消息,消息向下繼續傳遞到 RACSubject 所有的訂閱者中。
Values-From-RACSignal-To-Subscribers
-connect 方法通過 -subscribe: 實際上建立了 RACSignal 和 RACSubject 之間的連接,這種方式保證了 RACSignal 中的 didSubscribe 代碼塊只執行了一次。
所有的訂閱者不再訂閱原信號,而是訂閱 RACMulticastConnection 持有的熱信號 RACSubject ,實現對冷信號的一對多傳播。
在 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 方法已經能夠解決大部分問題了,但是在 -connect 方法調用之后才訂閱的訂閱者并不能收到消息。
如何才能保存 didSubscribe 執行過程中發送的消息,并在 -connect 調用之后也可以收到消息?這時,我們就要使用 -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);
}];
除了使用上述的代碼,也有一個更簡單的方式創建包含 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 差不多,只是內部封裝的熱信號不同,并在方法調用時就連接原信號:
- (RACSignal *)replay {
RACReplaySubject *subject = [RACReplaySubject subject];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
除了 -replay 方法, RACSignal 中還定義了與 RACMulticastConnection 中相關的其它 -replay 方法:
- (RACSignal<ValueType> *)replay;
- (RACSignal<ValueType> *)replayLast;
- (RACSignal<ValueType> *)replayLazily;
三個方法都會在 RACMulticastConnection 初始化時傳入一個 RACReplaySubject 對象,不過卻有一點細微的差別:
Difference-Between-Replay-Methods
相比于 -replay 方法, -replayLast 方法生成的 RACMulticastConnection 中熱信號的容量為 1 :
- (RACSignal *)replayLast {
RACReplaySubject *subject = [RACReplaySubject replaySubjectWithCapacity:1];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
而 replayLazily 會在返回的信號被 第一次訂閱 時,才會執行 -connect 方法:
- (RACSignal *)replayLazily {
RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];
return [RACSignal
defer:^{
[connection connect];
return connection.signal;
}];
}
總結
RACMulticastConnection 在處理冷熱信號相互轉換時非常好用,在 RACSignal 中也提供了很多將原有的冷信號通過 RACMulticastConnection 轉換成熱信號的方法。
RACMulticastConnection
在遇到冷信號中的行為有副作用后者非常昂貴時,我們就可以使用這些方法將單播變成多播,提高執行效率,減少副作用。
References
來自:http://www.jianshu.com/p/b94a0454e582