用于多播的 RACMulticastConnection

KazukoHume 7年前發布 | 7K 次閱讀 iOS開發 移動開發 ReactiveCocoa

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

RACMulticastConnection

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

References

 

來自:http://www.jianshu.com/p/b94a0454e582

 

 本文由用戶 KazukoHume 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!