『狀態』驅動的世界:ReactiveCocoa

這篇以及以後的文章主要會對 ReactiveObjc v2.5 的實現進行分析,從最簡單的例子中瞭解 ReactiveCocoa 的工做原理以及概念,也是筆者我的對於 RAC 學習的總結與理解。本文主要會圍繞 RAC 中核心概念 RACSignal 展開,詳細瞭解其底層實現。react

狀態驅動

2015 年的夏天的時候,作了幾個簡單的開源框架,想作點其它更有意思的框架卻沒什麼思路,就開始看一些跟編程沒有太大關係的書籍。git

out-of-contro

其中一本叫作《失控》給了我很大的啓發,其中有一則故事是這樣的:github

布魯克斯開展了一個雄心勃勃的研究生課題項目,研發更接近昆蟲而非恐龍的機器人。編程

布魯克斯的設想在一個叫「成吉思」的機巧裝置上成形。成吉思有橄欖球大小,像只蟑螂似的。布魯克斯把他的精簡理念發揮到了極致。小成吉思有 6 條腿卻沒有一丁點兒能夠稱爲「腦」的東西。全部 12 個電機和 21 個傳感器分佈在沒有中央處理器的可解耦網絡上。然而這 12 個充當肌肉的電機和 21 個傳感器之間的交互做用竟然產生了使人驚歎的複雜性和相似生命體的行爲。設計模式

成吉思的每條小細腿都在自顧自地工做,和其他的腿毫無關係。每條腿都經過本身的一組神經元——一個微型處理器——來控制其動做。每條腿只需管好本身!對成吉思來講,走路是一個團隊合做項目,至少有六個小頭腦在工做。它體內其他更微小的腦力則負責腿與腿之間的通信。昆蟲學家說這正是螞蟻和蟑螂的解決之道——這些爬行昆蟲的足肢上的神經元負責爲該足肢進行思考。數組

------ 《失控》第三章·第二節 快速、廉價、失控安全

書中對於機器人的介紹比較冗長,在這裏就簡單總結一下:機器人的每一條腿都單獨進行工做,經過傳感器感應的狀態作出響應:網絡

  • 若是腿擡起來了,那麼它要落下去;
  • 若是腿在向前動,要讓另外五條腿距離它遠一點;

這種去中心化的方式,簡化了整個系統的構造,使得各個組件只須要關心狀態,以及狀態對應的動做;再也不須要一箇中樞系統來組織、管理其它的組件,並負責大多數的業務邏輯。這種自底向下的、狀態驅動的構建方式可以使用多個較小的組件,減小臃腫的中樞出現的可能性,從而下降系統的複雜度。框架

ReactiveCocoa 與信號

ReactiveCocoa 對於狀態的理解與《失控》一書中十分相似,將原有的各類設計模式,包括代理、Target/Action、block、通知中心以及觀察者模式各類『輸入』,都抽象成了數據流或者信號(也能夠理解爲狀態流)讓單一的組件可以對本身的響應動做進行控制,簡化了視圖控制器的負擔。函數

在 ReactiveCocoa 中最重要的信號,也就是 RACSignal 對象是這一篇文章介紹的核心;文章中主要會介紹下面的代碼片斷出現的內容:

RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendNext:@2];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"dispose");
    }];
}];
[signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];複製代碼

在上述代碼執行時,會在控制檯中打印出如下內容:

1
2
dispose複製代碼

代碼片斷基本都是圍繞 RACSignal 類進行的,文章會分四部分對上面的代碼片斷的工做流程進行簡單的介紹:

  • 簡單瞭解 RACSignal
  • 信號的建立
  • 信號的訂閱與發送
  • 訂閱的回收過程

RACSignal 簡介

RACSignal 實際上是抽象類 RACStream 的子類,在整個 ReactiveObjc 工程中有另外一個類 RACSequence 也繼承自抽象類 RACStream

RACSignal-Hierachy

RACSignal 能夠說是 ReactiveCocoa 中的核心類,也是最重要的概念,整個框架圍繞着 RACSignal 的概念進行組織,對 RACSignal 最簡單的理解就是它表示一連串的狀態:

What-is-RACSigna

在狀態改變時,對應的訂閱者 RACSubscriber 就會收到通知執行相應的指令,在 ReactiveCocoa 的世界中全部的消息都是經過信號的方式來傳遞的,原有的設計模式都會簡化爲一種模型,這篇文章做爲 ReactiveCocoa 系列的第一篇文章並不會對這些問題進行詳細的展開和介紹,只會對 RACSignal 使用過程的原理進行簡單的分析。

這一小節會對 RACStream 以及 RACSignal 中與 RACStream 相關的部分進行簡單的介紹。

RACStream

RACStream 做爲抽象類自己不提供方法的實現,其實現內部原生提供的而方法都是抽象方法,會在調用時直接拋出異常:

+ (__kindof RACStream *)empty {
    NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)];
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil];
}

- (__kindof RACStream *)bind:(RACStreamBindBlock (^)(void))block;
+ (__kindof RACStream *)return:(id)value;
- (__kindof RACStream *)concat:(RACStream *)stream;
- (__kindof RACStream *)zipWith:(RACStream *)stream;複製代碼

RACStream-AbstractMethod

上面的這些抽象方法都須要子類覆寫,不過 RACStreamOperations 分類中使用上面的抽象方法提供了豐富的內容,好比說 -flattenMap: 方法:

- (__kindof RACStream *)flattenMap:(__kindof RACStream * (^)(id value))block {
    Class class = self.class;

    return [[self bind:^{
        return ^(id value, BOOL *stop) {
            id stream = block(value) ?: [class empty];
            NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream);

            return stream;
        };
    }] setNameWithFormat:@"[%@] -flattenMap:", self.name];
}複製代碼

其餘方法好比 -skip:-take:-ignore: 等等實例方法都構建在這些抽象方法之上,只要子類覆寫了全部抽象方法就能自動得到全部的 Operation 分類中的方法。

RACStream-Operation

RACSignal 與 Monad

若是你對 Monad 有所瞭解,那麼你應該知道 bindreturn 實際上是 Monad 中的概念,但 Monad 並非本篇文章所覆蓋的內容,並不會具體解釋它究竟是什麼。

ReactiveCocoa 框架中借鑑了不少其餘平臺甚至語言中的概念,包括微軟中的 Reactive Extension 以及 Haskell 中的 Monad,RACStream 提供的抽象方法中的 +return:-bind: 就與 Haskell 中 Monad 徹底同樣。

不少人都說 Monad 只是一個自函子範疇上的一個幺半羣而已;在筆者看來這種說法雖然是正確的,不過也很扯淡,這句話解釋了仍是跟沒解釋同樣,若是有人再跟你用這句話解釋 Monad,我以爲你最好的迴應就是買一本範疇論糊他一臉。若是真的想了解 Haskell 中的 Monad 究竟是什麼?能夠從代碼的角度入手,多寫一些代碼就明白了,這個概念理解起來其實根本沒什麼困難的,固然也能夠看一下 A Fistful of Monads,寫寫其中的代碼,會對 Monad 有本身的認知,固然,請不要再寫一篇解釋 Monad 的教程了(手動微笑)。

首先來看一下 +return 方法的 實現

+ (RACSignal *)return:(id)value {
    return [RACReturnSignal return:value];
}複製代碼

該方法接受一個 NSObject 對象,並返回一個 RACSignal 的實例,它會將一個 UIKit 世界的對象 NSObject 轉換成 ReactiveCocoa 中的 RACSignal

RACSignal-Return

RACReturnSignal 也僅僅是把 NSObject 對象包裝一下,並無作什麼複雜的事情:

+ (RACSignal *)return:(id)value {
    RACReturnSignal *signal = [[self alloc] init];
    signal->_value = value;
    return signal;
}複製代碼

可是 -bind: 方法的 實現 相比之下就十分複雜了:

- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACSignalBindBlock bindingBlock = block();
        return [self subscribeNext:^(id x) {
            BOOL stop = NO;
            id signal = bindingBlock(x, &stop);

            if (signal != nil) {
                [signal subscribeNext:^(id x) {
                    [subscriber sendNext:x];
                } error:^(NSError *error) {
                    [subscriber sendError:error];
                } completed:^{
                    [subscriber sendCompleted];
                }];
            }
            if (signal == nil || stop) {
                [subscriber sendCompleted];
            }
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            [subscriber sendCompleted];
        }];
    }] setNameWithFormat:@"[%@] -bind:", self.name];
}複製代碼

筆者在這裏對 -bind: 方法進行了大量的省略,省去了其中對各類 RACDisposable 的處理過程。

-bind: 方法會在原信號每次發出消息時,都執行 RACSignalBindBlock 對原有的信號中的消息進行變換生成一個新的信號:

RACSignal-Bind

在原有的 RACSignal 對象上調用 -bind: 方法傳入 RACSignalBindBlock,圖示中的右側就是具體的執行過程,原信號在變換以後變成了新的藍色的 RACSignal 對象。

RACSignalBindBlock 能夠簡單理解爲一個接受 NSObject 對象返回 RACSignal 對象的函數:

typedef RACSignal * _Nullable (^RACSignalBindBlock)(id _Nullable value, BOOL *stop);複製代碼

其函數簽名能夠理解爲 id -> RACSignal,然而這種函數是沒法直接對 RACSignal 對象進行變換的;不過經過 -bind: 方法就可使用這種函數操做 RACSignal,其實現以下:

  1. RACSignal 對象『解包』出 NSObject 對象;
  2. NSObject 傳入 RACSignalBindBlock 返回 RACSignal

若是在不考慮 RACSignal 會發出錯誤或者完成信號時,-bind: 能夠簡化爲更簡單的形式:

- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACSignalBindBlock bindingBlock = block();
        return [self subscribeNext:^(id x) {
            BOOL stop = NO;
            [bindingBlock(x, &stop) subscribeNext:^(id x) {
                [subscriber sendNext:x];
            }];
        }];
    }] setNameWithFormat:@"[%@] -bind:", self.name];
}複製代碼

調用 -subscribeNext: 方法訂閱當前信號,將信號中的狀態解包,而後將原信號中的狀態傳入 bindingBlock 中並訂閱返回的新的信號,將生成的新狀態 x 傳回原信號的訂閱者。

這裏經過兩個簡單的例子來了解 -bind: 方法的做用:

RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendNext:@2];
    [subscriber sendNext:@3];
    [subscriber sendNext:@4];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{
    return ^(NSNumber *value, BOOL *stop) {
        value = @(value.integerValue * value.integerValue);
        return [RACSignal return:value];
    };
}];
[signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"signal: %@", x);
}];
[bindSignal subscribeNext:^(id  _Nullable x) {
    NSLog(@"bindSignal: %@", x);
}];複製代碼

上面的代碼中直接使用了 +return: 方法將 value 打包成了 RACSignal * 對象:

Before-After-Bind-RACSigna

在 BindSignal 中的每個數字其實都是由一個 RACSignal 包裹的,這裏沒有畫出,在下一個例子中,讀者能夠清晰地看到其中的區別。

上圖簡要展現了變化先後的信號中包含的狀態,在運行上述代碼時,會在終端中打印出:

signal: 1
signal: 2
signal: 3
signal: 4
bindSignal: 1
bindSignal: 4
bindSignal: 9
bindSignal: 16複製代碼

這是一個最簡單的例子,直接使用 -return: 打包 NSObject 返回一個 RACSignal,接下來用一個更復雜的例子來幫助咱們更好的瞭解 -bind: 方法:

RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendNext:@2];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{
    return ^(NSNumber *value, BOOL *stop) {
        NSNumber *returnValue = @(value.integerValue * value.integerValue);
        return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            for (NSInteger i = 0; i < value.integerValue; i++) [subscriber sendNext:returnValue];
            [subscriber sendCompleted];
            return nil;
        }];
    };
}];
[bindSignal subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];複製代碼

下圖相比上面例子中的圖片更能精確的表現出 -bind: 方法都作了什麼:

Before-After-Bind-RACSignal-Complicated

信號中原有的狀態通過 -bind: 方法中傳入 RACSignalBindBlock 的處理實際上返回了多個 RACSignal

在源代碼的註釋中清楚地寫出了方法的實現過程:

  1. 訂閱原信號中的值;
  2. 將原信號發出的值傳入 RACSignalBindBlock 進行轉換;
  3. 若是 RACSignalBindBlock 返回一個信號,就會訂閱該信號並將信號中的全部值傳給訂閱者 subscriber
  4. 若是 RACSignalBindBlock 請求終止信號就會向信號發出 -sendCompleted 消息;
  5. 全部信號都完成時,會向訂閱者發送 -sendCompleted
  6. 不管什麼時候,若是信號發出錯誤,都會向訂閱者發送 -sendError: 消息。

若是想要了解 -bind: 方法在執行的過程當中是如何處理訂閱的清理和銷燬的,能夠閱讀文章最後的 -bind: 中對訂閱的銷燬 部分。

信號的建立

信號的建立過程十分簡單,-createSignal: 是推薦的建立信號的方法,方法其實只作了一次轉發:

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
    return [RACDynamicSignal createSignal:didSubscribe];
}

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
    RACDynamicSignal *signal = [[self alloc] init];
    signal->_didSubscribe = [didSubscribe copy];
    return [signal setNameWithFormat:@"+createSignal:"];
}複製代碼

該方法其實只是建立了一個 RACDynamicSignal 實例並保存了傳入的 didSubscribe 代碼塊,在每次有訂閱者訂閱當前信號時,都會執行一遍,向訂閱者發送消息。

RACSignal 類簇

雖然 -createSignal: 的方法簽名上返回的是 RACSignal 對象的實例,可是實際上這裏返回的是 RACDynamicSignal,也就是 RACSignal 的子類;一樣,在 ReactiveCocoa 中也有不少其餘的 RACSignal 子類。

使用類簇的方式設計的 RACSignal 在建立實例時可能會返回 RACDynamicSignalRACEmptySignalRACErrorSignalRACReturnSignal 對象:

RACSignal-Subclasses

其實這幾種子類並無對原有的 RACSignal 作出太大的改變,它們的建立過程也不是特別的複雜,只須要調用 RACSignal 不一樣的類方法:

RACSignal-Instantiate-Object

RACSignal 只是起到了一個代理的做用,最後的實現過程仍是會指向對應的子類:

+ (RACSignal *)error:(NSError *)error {
    return [RACErrorSignal error:error];
}

+ (RACSignal *)empty {
    return [RACEmptySignal empty];
}

+ (RACSignal *)return:(id)value {
    return [RACReturnSignal return:value];
}複製代碼

RACReturnSignal 的建立過程爲例:

+ (RACSignal *)return:(id)value {
    RACReturnSignal *signal = [[self alloc] init];
    signal->_value = value;
    return signal;
}複製代碼

這個信號的建立過程和 RACDynamicSignal 的初始化過程同樣,都很是簡單;只是將傳入的 value 簡單保存一下,在有其餘訂閱者 -subscribe: 時,向訂閱者發送 value

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return [RACScheduler.subscriptionScheduler schedule:^{
        [subscriber sendNext:self.value];
        [subscriber sendCompleted];
    }];
}複製代碼

RACEmptySignalRACErrorSignal 的建立過程也異常的簡單,只是對傳入的數據進行簡單的存儲,而後在訂閱時發送出來:

// RACEmptySignal
+ (RACSignal *)empty {
    return [[[self alloc] init] setNameWithFormat:@"+empty"];
}

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return [RACScheduler.subscriptionScheduler schedule:^{
        [subscriber sendCompleted];
    }];
}

// RACErrorSignal
+ (RACSignal *)error:(NSError *)error {
    RACErrorSignal *signal = [[self alloc] init];
    signal->_error = error;
    return signal;
}

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return [RACScheduler.subscriptionScheduler schedule:^{
        [subscriber sendError:self.error];
    }];
}複製代碼

這兩個建立過程的惟一區別就是一個發送的是『空值』,另外一個是 NSError 對象。

信號的訂閱與信息的發送

ReactiveCocoa 中信號的訂閱與信息的發送過程主要是由 RACSubscriber 類來處理的,而這也是信號的處理過程當中最重要的一部分,這一小節會先分析整個工做流程,以後會深刻代碼的實現。

RACSignal-Subcribe-Process

在信號建立以後調用 -subscribeNext: 方法返回一個 RACDisposable,然而這不是這一流程關心的重點,在訂閱過程當中生成了一個 RACSubscriber 對象,向這個對象發送消息 -sendNext: 時,就會向全部的訂閱者發送消息。

信號的訂閱

信號的訂閱與 -subscribe: 開頭的一系列方法有關:

RACSignal-Subscribe-Methods

訂閱者能夠選擇本身想要感興趣的信息類型 next/error/completed 進行關注,並在對應的信息發生時調用 block 進行處理回調。

全部的方法其實只是對 nextBlockcompletedBlock 以及 errorBlock 的組合,這裏以其中最長的 -subscribeNext:error:completed: 方法的實現爲例(也只須要介紹這一個方法):

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock {
    RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];
    return [self subscribe:o];
}複製代碼

方法中傳入的全部 block 參數都應該是非空的。

拿到了傳入的 block 以後,使用 +subscriberWithNext:error:completed: 初始化一個 RACSubscriber 對象的實例:

+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
    RACSubscriber *subscriber = [[self alloc] init];

    subscriber->_next = [next copy];
    subscriber->_error = [error copy];
    subscriber->_completed = [completed copy];

    return subscriber;
}複製代碼

在拿到這個對象以後,調用 RACSignal-subscribe: 方法傳入訂閱者對象:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    NSCAssert(NO, @"This method must be overridden by subclasses");
    return nil;
}複製代碼

RACSignal 類中其實並無實現這個實例方法,須要在上文提到的四個子類對這個方法進行覆寫,這裏僅分析 RACDynamicSignal 中的方法:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
    subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable];

    RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{
        RACDisposable *innerDisposable = self.didSubscribe(subscriber);
        [disposable addDisposable:innerDisposable];
    }];

    [disposable addDisposable:schedulingDisposable];

    return disposable;
}複製代碼

這裏暫時不須要關注與 RACDisposable 有關的任何內容,咱們會在下一節中詳細介紹。

RACPassthroughSubscriber 就像它的名字同樣,只是對上面建立的訂閱者對象進行簡單的包裝,將全部的消息轉發給內部的 innerSubscriber,也就是傳入的 RACSubscriber 對象:

- (instancetype)initWithSubscriber:(id<RACSubscriber>)subscriber signal:(RACSignal *)signal disposable:(RACCompoundDisposable *)disposable {
    self = [super init];

    _innerSubscriber = subscriber;
    _signal = signal;
    _disposable = disposable;

    [self.innerSubscriber didSubscribeWithDisposable:self.disposable];
    return self;
}複製代碼

若是直接簡化 -subscribe: 方法的實現,你能夠看到一個看起來極爲敷衍的代碼:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return self.didSubscribe(subscriber);
}複製代碼

方法只是執行了在建立信號時傳入的 RACSignalBindBlock

[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendNext:@2];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"dispose");
    }];
}];複製代碼

總而言之,信號的訂閱過程就是初始化 RACSubscriber 對象,而後執行 didSubscribe 代碼塊的過程。

Principle-of-Subscribing-Signals

信息的發送

RACSignalBindBlock 中,訂閱者能夠根據本身的興趣選擇本身想要訂閱哪一種消息;咱們也能夠按需發送三種消息:

RACSignal-Subcription-Messages-Sending

而如今只須要簡單看一下這三個方法的實現,就可以明白信息的發送過程了(真是沒啥好說的,不過爲了湊字數完整性):

- (void)sendNext:(id)value {
    @synchronized (self) {
        void (^nextBlock)(id) = [self.next copy];
        if (nextBlock == nil) return;

        nextBlock(value);
    }
}複製代碼

-sendNext: 只是將方法傳入的值傳入 nextBlock 再調用一次,並無什麼值得去分析的地方,而剩下的兩個方法實現也差很少,會調用對應的 block,在這裏就省略了。

訂閱的回收過程

在建立信號時,咱們向 -createSignal: 方法中傳入了 didSubscribe 信號,這個 block 在執行結束時會返回一個 RACDisposable 對象,用於在訂閱結束時進行必要的清理,一樣也能夠用於取消由於訂閱建立的正在執行的任務。

而處理這些事情的核心類就是 RACDisposable 以及它的子類:

RACDisposable-And-Subclasses

這篇文章中主要關注的是左側的三個子類,固然 RACDisposable 的子類不止這三個,還有用於處理 KVO 的 RACKVOTrampoline,不過在這裏咱們不會討論這個類的實現。

RACDisposable

在繼續分析討論訂閱的回收過程以前,筆者想先對 RACDisposable 進行簡要的剖析和介紹:

RACDisposable

RACDisposable 是以 _disposeBlock 爲核心進行組織的,幾乎全部的方法以及屬性其實都是對 _disposeBlock 進行的操做。

關於 _disposeBlock 中的 self

這一小節的內容是可選的,跳過不影響整篇文章閱讀的連貫性。

_disposeBlock 是一個私有的指針變量,當 void (^)(void) 類型的 block 被傳入以後都會轉換成 CoreFoundation 中的類型並以 void * 的形式存入 _disposeBlock 中:

+ (instancetype)disposableWithBlock:(void (^)(void))block {
    return [[self alloc] initWithBlock:block];
}

- (instancetype)initWithBlock:(void (^)(void))block {
    self = [super init];

    _disposeBlock = (void *)CFBridgingRetain([block copy]); 
    OSMemoryBarrier();

    return self;
}複製代碼

奇怪的是,_disposeBlock 中不止會存儲代碼塊 block,還有可能存儲橋接以後的 self

- (instancetype)init {
    self = [super init];

    _disposeBlock = (__bridge void *)self;
    OSMemoryBarrier();

    return self;
}複製代碼

這裏,剛開始看到可能會以爲比較奇怪,有兩個疑問須要解決:

  1. 爲何要提供一個 -init 方法來初始化 RACDisposable 對象?
  2. 爲何要向 _disposeBlock 中傳入當前對象?

對於 RACDisposable 來講,雖然一個不包含 _disposeBlock 的對象沒什麼太多的意義,可是對於 RACSerialDisposable 等子類來講,卻不徹底是這樣,由於 RACSerialDisposable-dispose 時,並不須要執行 disposeBlock,這樣就浪費了內存和 CPU 時間;可是同時咱們須要一個合理的方法準確地判斷當前對象的 isDisposed

- (BOOL)isDisposed {
    return _disposeBlock == NULL;
}複製代碼

因此,使用向 _disposeBlock 中傳入 NULL 的方式來判斷 isDisposed;在 -init 調用時傳入 self 而不是 NULL 防止狀態被誤判,這樣就在不引入其餘實例變量、增長對象的設計複雜度的同時,解決了這兩個問題。

若是仍然不理解上述的兩個問題,在這裏舉一個錯誤的例子,若是 _disposeBlock 在使用時只傳入 NULL 或者 block,那麼在 RACCompoundDisposable 初始化時,是應該向 _disposeBlock 中傳入什麼呢?

  • 傳入 NULL 會致使在初始化以後 isDisposed == YES,然而當前對象根本沒有被回收;
  • 傳入 block 會致使無用的 block 的執行,浪費內存以及 CPU 時間;

這也就是爲何要引入 self 來做爲 _disposeBlock 內容的緣由。

-dispose: 方法的實現

這個只有不到 20 行的 -dispose: 方法已是整個 RACDisposable 類中最複雜的方法了:

- (void)dispose {
    void (^disposeBlock)(void) = NULL;

    while (YES) {
        void *blockPtr = _disposeBlock;
        if (OSAtomicCompareAndSwapPtrBarrier(blockPtr, NULL, &_disposeBlock)) {
            if (blockPtr != (__bridge void *)self) {
                disposeBlock = CFBridgingRelease(blockPtr);
            }

            break;
        }
    }

    if (disposeBlock != nil) disposeBlock();
}複製代碼

可是其實它的實現也沒有複雜到哪裏去,從 _disposeBlock 實例變量中調用 CFBridgingRelease 取出一個 disposeBlock,而後執行這個 block,整個方法就結束了。

RACSerialDisposable

RACSerialDisposable 是一個用於持有 RACDisposable 的容器,它一次只能持有一個 RACDisposable 的實例,並能夠原子地換出容器中保存的對象:

- (RACDisposable *)swapInDisposable:(RACDisposable *)newDisposable {
    RACDisposable *existingDisposable;
    BOOL alreadyDisposed;

    pthread_mutex_lock(&_mutex);
    alreadyDisposed = _disposed;
    if (!alreadyDisposed) {
        existingDisposable = _disposable;
        _disposable = newDisposable;
    }
    pthread_mutex_unlock(&_mutex);

    if (alreadyDisposed) {
        [newDisposable dispose];
        return nil;
    }

    return existingDisposable;
}複製代碼

線程安全的 RACSerialDisposable 使用 pthred_mutex_t 互斥鎖來保證在訪問關鍵變量時不會出現線程競爭問題。

-dispose 方法的處理也十分簡單:

- (void)dispose {
    RACDisposable *existingDisposable;

    pthread_mutex_lock(&_mutex);
    if (!_disposed) {
        existingDisposable = _disposable;
        _disposed = YES;
        _disposable = nil;
    }
    pthread_mutex_unlock(&_mutex);

    [existingDisposable dispose];
}複製代碼

使用鎖保證線程安全,並在內部的 _disposable 換出以後在執行 -dispose 方法對訂閱進行處理。

RACCompoundDisposable

RACSerialDisposable 只負責一個 RACDisposable 對象的釋放不一樣;RACCompoundDisposable 同時負責多個 RACDisposable 對象的釋放。

相比於只管理一個 RACDisposable 對象的 RACSerialDisposableRACCompoundDisposable 因爲管理多個對象,其實現更加複雜,並且爲了性能和內存佔用之間的權衡,其實現方式是經過持有兩個實例變量:

@interface RACCompoundDisposable () {
    ...
    RACDisposable *_inlineDisposables[RACCompoundDisposableInlineCount];

    CFMutableArrayRef _disposables;
    ...
}複製代碼

在對象持有的 RACDisposable 不超過 RACCompoundDisposableInlineCount 時,都會存儲在 _inlineDisposables 數組中,而更多的實例都會存儲在 _disposables 中:

RACCompoundDisposable

RACCompoundDisposable 在使用 -initWithDisposables:初始化時,會初始化兩個 RACDisposable 的位置用於加速銷燬訂閱的過程,同時爲了避免浪費內存空間,在默認狀況下只佔用兩個位置:

- (instancetype)initWithDisposables:(NSArray *)otherDisposables {
    self = [self init];

    [otherDisposables enumerateObjectsUsingBlock:^(RACDisposable *disposable, NSUInteger index, BOOL *stop) {
        self->_inlineDisposables[index] = disposable;
        if (index == RACCompoundDisposableInlineCount - 1) *stop = YES;
    }];

    if (otherDisposables.count > RACCompoundDisposableInlineCount) {
        _disposables = RACCreateDisposablesArray();

        CFRange range = CFRangeMake(RACCompoundDisposableInlineCount, (CFIndex)otherDisposables.count - RACCompoundDisposableInlineCount);
        CFArrayAppendArray(_disposables, (__bridge CFArrayRef)otherDisposables, range);
    }

    return self;
}複製代碼

若是傳入的 otherDisposables 多於 RACCompoundDisposableInlineCount,就會建立一個新的 CFMutableArrayRef 引用,並將剩餘的 RACDisposable 所有傳入這個數組中。

RACCompoundDisposable 中另外一個值得注意的方法就是 -addDisposable:

- (void)addDisposable:(RACDisposable *)disposable {
    if (disposable == nil || disposable.disposed) return;

    BOOL shouldDispose = NO;

    pthread_mutex_lock(&_mutex);
    {
        if (_disposed) {
            shouldDispose = YES;
        } else {
            for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) {
                if (_inlineDisposables[i] == nil) {
                    _inlineDisposables[i] = disposable;
                    goto foundSlot;
                }
            }

            if (_disposables == NULL) _disposables = RACCreateDisposablesArray();
            CFArrayAppendValue(_disposables, (__bridge void *)disposable);
        foundSlot:;
        }
    }
    pthread_mutex_unlock(&_mutex);
    if (shouldDispose) [disposable dispose];
}複製代碼

在向 RACCompoundDisposable 中添加新的 RACDisposable 對象時,會先嚐試在 _inlineDisposables 數組中尋找空閒的位置,若是沒有找到,就會加入到 _disposables 中;可是,在添加 RACDisposable 的過程當中也不免遇到當前 RACCompoundDisposable 已經 dispose 的狀況,而這時就會直接 -dispose 剛剛加入的對象。

訂閱的銷燬過程

在瞭解了 ReactiveCocoa 中與訂閱銷燬相關的類,咱們就能夠繼續對 -bind: 方法的分析了,以前在分析該方法時省略了 -bind: 在執行過程當中是如何處理訂閱的清理和銷燬的,因此會省略對於正常值和錯誤的處理過程,首先來看一下簡化後的代碼:

- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACSignalBindBlock bindingBlock = block();
        __block volatile int32_t signalCount = 1;
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        void (^completeSignal)(RACDisposable *) = ...
        void (^addSignal)(RACSignal *) = ...

        RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init];
        [compoundDisposable addDisposable:selfDisposable];
        RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
            BOOL stop = NO;
            id signal = bindingBlock(x, &stop);

            if (signal != nil) addSignal(signal);
            if (signal == nil || stop) {
                [selfDisposable dispose];
                completeSignal(selfDisposable);
            }
        } completed:^{
            completeSignal(selfDisposable);
        }];
        selfDisposable.disposable = bindingDisposable;
        return compoundDisposable;
    }] setNameWithFormat:@"[%@] -bind:", self.name];
}複製代碼

在簡化的代碼中,訂閱的清理是由一個 RACCompoundDisposable 的實例負責的,向這個實例中添加 RACSerialDisposable 以及 RACDisposable 對象,並在 RACCompoundDisposable 銷燬時銷燬。

completeSignaladdSignal 兩個 block 主要負責處理新建立信號的清理工做:

void (^completeSignal)(RACDisposable *) = ^(RACDisposable *finishedDisposable) {
    if (OSAtomicDecrement32Barrier(&signalCount) == 0) {
        [subscriber sendCompleted];
        [compoundDisposable dispose];
    } else {
        [compoundDisposable removeDisposable:finishedDisposable];
    }
};

void (^addSignal)(RACSignal *) = ^(RACSignal *signal) {
    OSAtomicIncrement32Barrier(&signalCount);
    RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init];
    [compoundDisposable addDisposable:selfDisposable];
    RACDisposable *disposable = [signal completed:^{
        completeSignal(selfDisposable);
    }];
    selfDisposable.disposable = disposable;
};複製代碼

先經過一個例子來看一下 -bind: 方法調用以後,訂閱是如何被清理的:

RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendNext:@2];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"Original Signal Dispose.");
    }];
}];
RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{
    return ^(NSNumber *value, BOOL *stop) {
        NSNumber *returnValue = @(value.integerValue);
        return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            for (NSInteger i = 0; i < value.integerValue; i++) [subscriber sendNext:returnValue];
            [subscriber sendCompleted];
            return [RACDisposable disposableWithBlock:^{
                NSLog(@"Binding Signal Dispose.");
            }];
        }];
    };
}];
[bindSignal subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];複製代碼

在每一個訂閱建立以及全部的值發送以後,訂閱就會被就地銷燬,調用 disposeBlock,並從 RACCompoundDisposable 實例中移除:

1
Binding Signal Dispose.
2
2
Binding Signal Dispose.
Original Signal Dispose.複製代碼

原訂閱的銷燬時間以及綁定信號的控制是由 SignalCount 控制的,其表示 RACCompoundDisposable 中的 RACSerialDisposable 實例的個數,在每次有新的訂閱被建立時都會向 RACCompoundDisposable 加入一個新的 RACSerialDisposable,並在訂閱發送結束時從數組中移除,整個過程用圖示來表示比較清晰:

RACSignal-Bind-Disposable

紫色的 RACSerialDisposable 爲原訂閱建立的對象,灰色的爲新信號訂閱的對象。

總結

這是整個 ReactiveCocoa 源代碼分析系列文章的第一篇,想寫一個跟這個系列有關的代碼已經好久了,文章中對於 RACSignal 進行了一些簡單的介紹,項目中絕大多數的方法都是很簡潔的,行數並很少,代碼的組織方式也很易於理解。雖然沒有太多讓人意外的東西,不過整個工程仍是很值得閱讀的。

References

方法實現對照表

方法 實現
+return: RACSignal.m#L89-L91
-bind: RACSignal.m#L93-176

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: draveness.me/racsignal

相關文章
相關標籤/搜索