用于多播的 RACMulticastConnection

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ì)有很多問題。

RACMulticastConnection

我們希望有一種模型能夠?qū)⒗湫盘?hào)轉(zhuǎn)變成熱信號(hào),并在合適的時(shí)間觸發(fā),向所有的訂閱者發(fā)送消息;而今天要介紹的 RACMulticastConnection 就是用于解決上述問題的。

RACMulticastConnection 簡介

RACMulticastConnection 封裝了將一個(gè)信號(hào)的訂閱分享給多個(gè)訂閱者的思想,它的每一個(gè)對(duì)象都持有兩個(gè) RACSignal

RACMulticastConnection-Interface

一個(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-And-Subscribe

每一次在 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ù)。

RACSignal-RACMulticastConnection-Connect

對(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-and-multicast

-publish 只是對(duì) -multicast: 方法的簡單封裝,它們都是通過 RACMulticastConnection 私有的初始化方法 -initWithSourceSignal:subject: 創(chuàng)建一個(gè)新的實(shí)例。

在使用 -multicast: 方法時(shí),傳入的信號(hào)其實(shí)就是用于廣播的信號(hào);這個(gè)信號(hào)必須是一個(gè) RACSubject 本身或者它的子類:

![RACSubject - Subclasses](http://img.draveness.me/2017-02-15-RACSubject - Subclasses.png)

傳入 -multicast: 方法的一般都是 RACSubject 或者 RACReplaySubject 對(duì)象。

訂閱源信號(hào)的時(shí)間點(diǎn)

訂閱 connection.signal 中的數(shù)據(jù)流時(shí),其實(shí)只是向多播對(duì)象中的熱信號(hào) RACSubject 持有的數(shù)組中加入訂閱者,而這時(shí)剛剛創(chuàng)建的 RACSubject 中并沒有任何的消息。

SubscribeNext-To-RACSubject-Before-Connect

只有在調(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 所有的訂閱者中。

Values-From-RACSignal-To-Subscribers

-connect 方法通過 -subscribe: 實(shí)際上建立了 RACSignalRACSubject 之間的連接,這種方式保證了 RACSignal 中的 didSubscribe 代碼塊只執(zhí)行了一次。

所有的訂閱者不再訂閱原信號(hào),而是訂閱 RACMulticastConnection 持有的熱信號(hào) RACSubject,實(shí)現(xiàn)對(duì)冷信號(hào)的一對(duì)多傳播。

RACMulticastConnection 中還有另一個(gè)用于連接 RACSignalRACSubject 信號(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ì)微的差別:

Difference-Between-Replay-Methods

相比于 -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)的方法。

RACMulticastConnection

在遇到冷信號(hào)中的行為有副作用后者非常昂貴時(shí),我們就可以使用這些方法將單播變成多播,提高執(zhí)行效率,減少副作用。

References

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/racconnection

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容