ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

一縷殤流化隱半邊冰霜發表於2019-03-01

前言

緊接著上篇的原始碼實現分析,繼續分析RACSignal的變換操作的底層實現。

目錄

  • 1.高階訊號操作
  • 2.同步操作
  • 3.副作用操作
  • 4.多執行緒操作
  • 5.其他操作

一. 高階訊號操作

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

高階操作大部分的操作是針對高階訊號的,也就是說訊號裡面傳送的值還是一個訊號或者是一個高階訊號。可以類比陣列,這裡就是多維陣列,陣列裡面還是套的陣列。

1. flattenMap: (在父類RACStream中定義的)

flattenMap:在整個RAC中具有很重要的地位,很多訊號變換都是可以用flattenMap:來實現的。

map:,flatten,filter,sequenceMany:這4個操作都是用flattenMap:來實現的。然而其他變換操作實現裡面用到map:,flatten,filter又有很多。

回顧一下map:的實現:


- (instancetype)map:(id (^)(id value))block {
    NSCParameterAssert(block != nil);

    Class class = self.class;
    return [[self flattenMap:^(id value) {
        return [class return:block(value)];
    }] setNameWithFormat:@"[%@] -map:", self.name];
}複製程式碼

map:的操作其實就是直接原訊號進行的 flattenMap:的操作,變換出來的新的訊號的值是block(value)。

flatten的實現接下去會具體分析,這裡先略過。

filter的實現:


- (instancetype)filter:(BOOL (^)(id value))block {
    NSCParameterAssert(block != nil);

    Class class = self.class;
    return [[self flattenMap:^ id (id value) {
        block(value) ? return [class return:value] :  return class.empty;
    }] setNameWithFormat:@"[%@] -filter:", self.name];
}複製程式碼

filter的實現和map:有點類似,也是對原訊號進行 flattenMap:的操作,只不過block(value)不是作為返回值,而是作為判斷條件,滿足這個閉包的條件,變換出來的新的訊號返回值就是value,不滿足的就返回empty訊號

接下去要分析的高階操作裡面,switchToLatest,try:,tryMap:的實現中也將會使用到flattenMap:。

flattenMap:的原始碼實現:


- (instancetype)flattenMap:(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];
}複製程式碼

flattenMap:的實現是呼叫了bind函式,對原訊號進行變換,並返回block(value)的新訊號。關於bind操作的具體流程這篇文章裡面已經分析過了,這裡不再贅述。

從flattenMap:的原始碼可以看到,它是可以支援類似Promise的序列非同步操作的,並且flattenMap:是滿足Monad中bind部分定義的。flattenMap:沒法去實現takeUntil:和take:的操作。

然而,bind操作可以實現take:的操作,bind是完全滿足Monad中bind部分定義的。

2. flatten (在父類RACStream中定義的)

flatten的原始碼實現:


- (instancetype)flatten {
    __weak RACStream *stream __attribute__((unused)) = self;
    return [[self flattenMap:^(id value) {
        return value;
    }] setNameWithFormat:@"[%@] -flatten", self.name];
}複製程式碼

flatten操作必須是對高階訊號進行操作,如果訊號裡面不是訊號,即不是高階訊號,那麼就會崩潰。崩潰資訊如下:


*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Value returned from -flattenMap: is not a stream複製程式碼

所以flatten是對高階訊號進行的降階操作。高階訊號每傳送一次訊號,經過flatten變換,由於flattenMap:操作之後,返回的新的訊號的每個值就是原訊號中每個訊號的值。

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

如果對訊號A,訊號B,訊號C進行merge:操作,可以達到和flatten一樣的效果。


    [RACSignal merge:@[signalA,signalB,signalC]];複製程式碼

merge:操作在上篇文章分析過,再來複習一下:


+ (RACSignal *)merge:(id<NSFastEnumeration>)signals {
    NSMutableArray *copiedSignals = [[NSMutableArray alloc] init];
    for (RACSignal *signal in signals) {
        [copiedSignals addObject:signal];
    }

    return [[[RACSignal
              createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
                  for (RACSignal *signal in copiedSignals) {
                      [subscriber sendNext:signal];
                  }

                  [subscriber sendCompleted];
                  return nil;
              }]
             flatten]
            setNameWithFormat:@"+merge: %@", copiedSignals];
}複製程式碼

現在在回來看這段程式碼,copiedSignals雖然是一個NSMutableArray,但是它近似合成了一個上圖中的高階訊號。然後這些訊號們每傳送出來一個訊號就發給訂閱者。整個操作如flatten的字面意思一樣,壓平。

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

另外,在ReactiveCocoa v2.5中,flatten預設就是flattenMap:這一種操作。


public func flatten(_ strategy: FlattenStrategy) -> Signal<Value.Value, Error> {
    switch strategy {
    case .merge:
        return self.merge()

    case .concat:
        return self.concat()

    case .latest:
        return self.switchToLatest()
    }
}複製程式碼

而在ReactiveCocoa v3.x,v4.x,v5.x中,flatten的操作是可以選擇3種操作選擇的。merge,concat,switchToLatest。

3. flatten:

flatten:操作也必須是對高階訊號進行操作,如果訊號裡面不是訊號,即不是高階訊號,那麼就會崩潰。

flatten:的實現比較複雜,一步步的來分析:


- (RACSignal *)flatten:(NSUInteger)maxConcurrent {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [[RACCompoundDisposable alloc] init];
        NSMutableArray *activeDisposables = [[NSMutableArray alloc] initWithCapacity:maxConcurrent];
        NSMutableArray *queuedSignals = [NSMutableArray array];

        __block BOOL selfCompleted = NO;
        __block void (^subscribeToSignal)(RACSignal *);
        __weak __block void (^recur)(RACSignal *);
        recur = subscribeToSignal = ^(RACSignal *signal) { // 暫時省略};

        void (^completeIfAllowed)(void) = ^{ // 暫時省略};

        [compoundDisposable addDisposable:[self subscribeNext:^(RACSignal *signal) {
            if (signal == nil) return;

            NSCAssert([signal isKindOfClass:RACSignal.class], @"Expected a RACSignal, got %@", signal);

            @synchronized (subscriber) {
                if (maxConcurrent > 0 && activeDisposables.count >= maxConcurrent) {
                    [queuedSignals addObject:signal];
                    return;
                }
            }

            subscribeToSignal(signal);
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            @synchronized (subscriber) {
                selfCompleted = YES;
                completeIfAllowed();
            }
        }]];

        return compoundDisposable;
    }] setNameWithFormat:@"[%@] -flatten: %lu", self.name, (unsigned long)maxConcurrent];
}複製程式碼

先來解釋一些變數,陣列的作用

activeDisposables裡面裝的是當前正在訂閱的訂閱者們的disposables訊號。

queuedSignals裡面裝的是被暫時快取起來的訊號,它們等待被訂閱。

selfCompleted表示高階訊號是否Completed。

subscribeToSignal閉包的作用是訂閱所給的訊號。這個閉包的入參引數就是一個訊號,在閉包內部訂閱這個訊號,並進行一些操作。

recur是對subscribeToSignal閉包的一個弱引用,防止strong-weak迴圈引用,在下面會分析subscribeToSignal閉包,就會明白為什麼recur要用weak修飾了。

completeIfAllowed的作用是在所有訊號都傳送完畢的時候,通知訂閱者,給訂閱者傳送completed。

入參maxConcurrent的意思是最大可容納同時被訂閱的訊號個數。

再來詳細分析一下具體訂閱的過程。

flatten:的內部,訂閱高階訊號發出來的訊號,這部分的程式碼比較簡單:



    [self subscribeNext:^(RACSignal *signal) {
        if (signal == nil) return;

        NSCAssert([signal isKindOfClass:RACSignal.class], @"Expected a RACSignal, got %@", signal);

        @synchronized (subscriber) {
            // 1
            if (maxConcurrent > 0 && activeDisposables.count >= maxConcurrent) {
                [queuedSignals addObject:signal];
                return;
            }
        }
        // 2
        subscribeToSignal(signal);
    } error:^(NSError *error) {
        [subscriber sendError:error];
    } completed:^{
        @synchronized (subscriber) {
            selfCompleted = YES;
            // 3
            completeIfAllowed();
        }
    }]];複製程式碼
  1. 如果當前最大可容納訊號的個數 > 0 ,且,activeDisposables陣列裡面已經裝滿到最大可容納訊號的個數,不能再裝新的訊號了。那麼就把當前的訊號快取到queuedSignals陣列中。

  2. 直到activeDisposables陣列裡面有空的位子可以加入新的訊號,那麼就呼叫subscribeToSignal( )閉包,開始訂閱這個新的訊號。

  3. 最後完成的時候標記變數selfCompleted為YES,並且呼叫completeIfAllowed( )閉包。


void (^completeIfAllowed)(void) = ^{
    if (selfCompleted && activeDisposables.count == 0) {
        [subscriber sendCompleted];
        subscribeToSignal = nil;
    }
};複製程式碼

當selfCompleted = YES 並且activeDisposables陣列裡面的訊號都傳送完畢,沒有可以傳送的訊號了,即activeDisposables.count = 0,那麼就給訂閱者sendCompleted。這裡值得一提的是,還需要把subscribeToSignal手動置為nil。因為在subscribeToSignal閉包中強引用了completeIfAllowed閉包,防止completeIfAllowed閉包被提早的銷燬掉了。所以在completeIfAllowed閉包執行完畢的時候,需要再把subscribeToSignal閉包置為nil。

那麼接下來需要看的重點就是subscribeToSignal( )閉包。


    recur = subscribeToSignal = ^(RACSignal *signal) {
        RACSerialDisposable *serialDisposable = [[RACSerialDisposable alloc] init];
        // 1
        @synchronized (subscriber) {
            [compoundDisposable addDisposable:serialDisposable];
            [activeDisposables addObject:serialDisposable];
        }

        serialDisposable.disposable = [signal subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            // 2
            __strong void (^subscribeToSignal)(RACSignal *) = recur;
            RACSignal *nextSignal;
            // 3
            @synchronized (subscriber) {
                [compoundDisposable removeDisposable:serialDisposable];
                [activeDisposables removeObjectIdenticalTo:serialDisposable];
                // 4
                if (queuedSignals.count == 0) {
                    completeIfAllowed();
                    return;
                }
                // 5
                nextSignal = queuedSignals[0];
                [queuedSignals removeObjectAtIndex:0];
            }
            // 6
            subscribeToSignal(nextSignal);
        }];
    };複製程式碼
  1. activeDisposables先新增當前高階訊號發出來的訊號的Disposable( 也就是入參訊號的Disposable)
  2. 這裡會對recur進行__strong,因為下面第6步會用到subscribeToSignal( )閉包,同樣也是為了防止出現迴圈引用。
  3. 訂閱入參訊號,給訂閱者傳送訊號。當傳送完畢後,activeDisposables中移除它對應的Disposable。
  4. 如果當前快取的queuedSignals陣列裡面沒有快取的訊號,那麼就呼叫completeIfAllowed( )閉包。
  5. 如果當前快取的queuedSignals陣列裡面有快取的訊號,那麼就取出第0個訊號,並在queuedSignals陣列移除它。
  6. 把第4步取出的訊號繼續訂閱,繼續呼叫subscribeToSignal( )閉包。

總結一下:高階訊號每傳送一個訊號值,判斷activeDisposables陣列裝的個數是否已經超過了maxConcurrent。如果裝不下了就快取進queuedSignals陣列中。如果還可以裝的下就開始呼叫subscribeToSignal( )閉包,訂閱當前訊號。

每傳送完一個訊號就判斷快取陣列queuedSignals的個數,如果快取陣列裡面已經沒有訊號了,那麼就結束原來高階訊號的傳送。如果快取陣列裡面還有訊號就繼續訂閱。如此迴圈,直到原高階訊號所有的訊號都傳送完畢。

整個flatten:的執行流程都分析清楚了,最後,關於入參maxConcurrent進行更進一步的解讀。

回看上面flatten:的實現中有這樣一句話:


if (maxConcurrent > 0 && activeDisposables.count >= maxConcurrent)複製程式碼

那麼maxConcurrent的值域就是最終決定flatten:表現行為。

如果maxConcurrent < 0,會發生什麼?程式會崩潰。因為在原始碼中有這樣一行的初始化的程式碼:


NSMutableArray *activeDisposables = [[NSMutableArray alloc] initWithCapacity:maxConcurrent];複製程式碼

activeDisposables在初始化的時候會初始化一個大小為maxConcurrent的NSMutableArray。如果maxConcurrent < 0,那麼這裡初始化就會崩潰。

如果maxConcurrent = 0,會發生什麼?那麼flatten:就退化成flatten了。

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

如果maxConcurrent = 1,會發生什麼?那麼flatten:就退化成concat了。

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

如果maxConcurrent > 1,會發生什麼?由於至今還沒有遇到能用到maxConcurrent > 1的需求情況,所以這裡暫時不展示圖解了。maxConcurrent > 1之後,flatten的行為還依照高階訊號的個數和maxConcurrent的關係。如果高階訊號的個數<=maxConcurrent的值,那麼flatten:又退化成flatten了。如果高階訊號的個數>maxConcurrent的值,那麼多的訊號就會進入queuedSignals快取陣列。

4. concat

這裡的concat實現是在RACSignal裡面定義的。


- (RACSignal *)concat {
    return [[self flatten:1] setNameWithFormat:@"[%@] -concat", self.name];
}複製程式碼

一看原始碼就知道了,concat其實就是flatten:1。

當然在RACSignal中定義了concat:方法,這個方法在之前的文章已經分析過了,這裡回顧對比一下:


- (RACSignal *)concat:(RACSignal *)signal {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACSerialDisposable *serialDisposable = [[RACSerialDisposable alloc] init];

        RACDisposable *sourceDisposable = [self subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            RACDisposable *concattedDisposable = [signal subscribe:subscriber];
            serialDisposable.disposable = concattedDisposable;
        }];

        serialDisposable.disposable = sourceDisposable;
        return serialDisposable;
    }] setNameWithFormat:@"[%@] -concat: %@", self.name, signal];
}複製程式碼

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

經過對比可以發現,雖然最終變換出來的結果類似,但是針對的訊號的物件是不同的,concat是針對高階訊號進行降階操作。concat:是把兩個訊號連線起來的操作。如果把高階訊號按照時間軸,從左往右,依次把每個訊號都concat:連線起來,那麼結果就是concat。

5. switchToLatest


- (RACSignal *)switchToLatest {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACMulticastConnection *connection = [self publish];

        RACDisposable *subscriptionDisposable = [[connection.signal
                                                  flattenMap:^(RACSignal *x) {
                                                      NSCAssert(x == nil || [x isKindOfClass:RACSignal.class], @"-switchToLatest requires that the source signal (%@) send signals. Instead we got: %@", self, x);
                                                      return [x takeUntil:[connection.signal concat:[RACSignal never]]];
                                                  }]
                                                 subscribe:subscriber];

        RACDisposable *connectionDisposable = [connection connect];
        return [RACDisposable disposableWithBlock:^{
            [subscriptionDisposable dispose];
            [connectionDisposable dispose];
        }];
    }] setNameWithFormat:@"[%@] -switchToLatest", self.name];
}複製程式碼

switchToLatest這個操作只能用在高階訊號上,如果原訊號裡面有不是訊號的值,那麼就會崩潰,崩潰資訊如下:


***** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-switchToLatest requires that the source signal (<RACDynamicSignal: 0x608000038ec0> name: ) send signals.複製程式碼

在switchToLatest操作中,先把原訊號轉換成熱訊號,connection.signal就是RACSubject型別的。對RACSubject進行flattenMap:變換。在flattenMap:變換中,connection.signal會先concat:一個never訊號。這裡concat:一個never訊號的原因是為了內部的訊號過早的結束而導致訂閱者收到complete訊號。

flattenMap:變換中x也是一個訊號,對x進行takeUntil:變換,效果就是下一個訊號到來之前,x會一直髮送訊號,一旦下一個訊號到來,x就會被取消訂閱,開始訂閱新的訊號。

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

一個高階訊號經過switchToLatest降階操作之後,能得到上圖中的訊號。

6. switch: cases: default:

switch: cases: default:原始碼實現如下:



+ (RACSignal *)switch:(RACSignal *)signal cases:(NSDictionary *)cases default:(RACSignal *)defaultSignal {
    NSCParameterAssert(signal != nil);
    NSCParameterAssert(cases != nil);

    for (id key in cases) {
        id value __attribute__((unused)) = cases[key];
        NSCAssert([value isKindOfClass:RACSignal.class], @"Expected all cases to be RACSignals, %@ isn't", value);
    }

    NSDictionary *copy = [cases copy];

    return [[[signal
              map:^(id key) {
                  if (key == nil) key = RACTupleNil.tupleNil;

                  RACSignal *signal = copy[key] ?: defaultSignal;
                  if (signal == nil) {
                      NSString *description = [NSString stringWithFormat:NSLocalizedString(@"No matching signal found for value %@", @""), key];
                      return [RACSignal error:[NSError errorWithDomain:RACSignalErrorDomain code:RACSignalErrorNoMatchingCase userInfo:@{ NSLocalizedDescriptionKey: description }]];
                  }

                  return signal;
              }]
             switchToLatest]
            setNameWithFormat:@"+switch: %@ cases: %@ default: %@", signal, cases, defaultSignal];
}複製程式碼

實現中有3個斷言,全部都是針對入參的要求。入參signal訊號和cases字典都不能是nil。其次,cases字典裡面所有key對應的value必須是RACSignal型別的。注意,defaultSignal是可以為nil的。

接下來的實現比較簡單,對入參傳進來的signal訊號進行map變換,這裡的變換是升階的變換。

signal每次傳送出來的一個值,就把這個值當做key值去cases字典裡面去查詢對應的value。當然value對應的是一個訊號。如果value對應的訊號不為空,就把signal傳送出來的這個值map成字典裡面對應的訊號。如果value對應為空,那麼就把原signal發出來的值map成defaultSignal訊號。

如果經過轉換之後,得到的訊號為nil,就會返回一個error訊號。如果得到的訊號不為nil,那麼原訊號完全轉換完成就會變成一個高階訊號,這個高階訊號裡面裝的都是訊號。最後再對這個高階訊號執行switchToLatest轉換。

7. if: then: else:

if: then: else:原始碼實現如下:



+ (RACSignal *)if:(RACSignal *)boolSignal then:(RACSignal *)trueSignal else:(RACSignal *)falseSignal {
    NSCParameterAssert(boolSignal != nil);
    NSCParameterAssert(trueSignal != nil);
    NSCParameterAssert(falseSignal != nil);

    return [[[boolSignal
              map:^(NSNumber *value) {
                  NSCAssert([value isKindOfClass:NSNumber.class], @"Expected %@ to send BOOLs, not %@", boolSignal, value);

                  return (value.boolValue ? trueSignal : falseSignal);
              }]
             switchToLatest]
            setNameWithFormat:@"+if: %@ then: %@ else: %@", boolSignal, trueSignal, falseSignal];
}複製程式碼

入參boolSignal,trueSignal,falseSignal三個訊號都不能為nil。

boolSignal裡面都必須裝的是NSNumber型別的值。

針對boolSignal進行map升階操作,boolSignal訊號裡面的值如果是YES,那麼就轉換成trueSignal訊號,如果為NO,就轉換成falseSignal。升階轉換完成之後,boolSignal就是一個高階訊號,然後再進行switchToLatest操作。

8. catch:

catch:的實現如下:



- (RACSignal *)catch:(RACSignal * (^)(NSError *error))catchBlock {
    NSCParameterAssert(catchBlock != NULL);

    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACSerialDisposable *catchDisposable = [[RACSerialDisposable alloc] init];

        RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            RACSignal *signal = catchBlock(error);
            NSCAssert(signal != nil, @"Expected non-nil signal from catch block on %@", self);
            catchDisposable.disposable = [signal subscribe:subscriber];
        } completed:^{
            [subscriber sendCompleted];
        }];

        return [RACDisposable disposableWithBlock:^{
            [catchDisposable dispose];
            [subscriptionDisposable dispose];
        }];
    }] setNameWithFormat:@"[%@] -catch:", self.name];
}複製程式碼

當對原訊號進行訂閱的時候,如果出現了錯誤,會去執行catchBlock( )閉包,入參為剛剛產生的error。catchBlock( )閉包產生的是一個新的RACSignal,並再次用訂閱者訂閱該訊號。

這裡之所以說是高階操作,是因為這裡原訊號發生錯誤之後,錯誤會升階成一個訊號。

9. catchTo:

catchTo:的實現如下:


- (RACSignal *)catchTo:(RACSignal *)signal {
    return [[self catch:^(NSError *error) {
        return signal;
    }] setNameWithFormat:@"[%@] -catchTo: %@", self.name, signal];
}複製程式碼

catchTo:的實現就是呼叫catch:方法,只不過原來catch:方法裡面的catchBlock( )閉包,永遠都只返回catchTo:的入參,signal訊號。

10. try:


- (RACSignal *)try:(BOOL (^)(id value, NSError **errorPtr))tryBlock {
    NSCParameterAssert(tryBlock != NULL);

    return [[self flattenMap:^(id value) {
        NSError *error = nil;
        BOOL passed = tryBlock(value, &error);
        return (passed ? [RACSignal return:value] : [RACSignal error:error]);
    }] setNameWithFormat:@"[%@] -try:", self.name];
}複製程式碼

try:也是一個高階操作。對原訊號進行flattenMap變換,對訊號發出來的每個值都呼叫一遍tryBlock( )閉包,如果這個閉包的返回值是YES,那麼就返回[RACSignal return:value],如果閉包的返回值是NO,那麼就返回error。原訊號中如果都是值,那麼經過try:操作之後,每個值都會變成RACSignal,於是原訊號也就變成了高階訊號了。

11. tryMap:


- (RACSignal *)tryMap:(id (^)(id value, NSError **errorPtr))mapBlock {
    NSCParameterAssert(mapBlock != NULL);

    return [[self flattenMap:^(id value) {
        NSError *error = nil;
        id mappedValue = mapBlock(value, &error);
        return (mappedValue == nil ? [RACSignal error:error] : [RACSignal return:mappedValue]);
    }] setNameWithFormat:@"[%@] -tryMap:", self.name];
}複製程式碼

tryMap:的實現和try:的實現基本一致,唯一不同的就是入參閉包的返回值不同。在tryMap:中呼叫mapBlock( )閉包,返回是一個物件,如果這個物件不為nil,就返回[RACSignal return:mappedValue]。如果返回的物件是nil,那麼就變換成error訊號。

12. timeout: onScheduler:



- (RACSignal *)timeout:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler {
    NSCParameterAssert(scheduler != nil);
    NSCParameterAssert(scheduler != RACScheduler.immediateScheduler);

    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];

        RACDisposable *timeoutDisposable = [scheduler afterDelay:interval schedule:^{
            [disposable dispose];
            [subscriber sendError:[NSError errorWithDomain:RACSignalErrorDomain code:RACSignalErrorTimedOut userInfo:nil]];
        }];

        [disposable addDisposable:timeoutDisposable];

        RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            [disposable dispose];
            [subscriber sendError:error];
        } completed:^{
            [disposable dispose];
            [subscriber sendCompleted];
        }];

        [disposable addDisposable:subscriptionDisposable];
        return disposable;
    }] setNameWithFormat:@"[%@] -timeout: %f onScheduler: %@", self.name, (double)interval, scheduler];
}複製程式碼

timeout: onScheduler:的實現很簡單,它比正常的訊號訂閱多了一個timeoutDisposable操作。它在訊號訂閱的內部開啟了一個scheduler,經過interval的時間之後,就會停止訂閱原訊號,並對訂閱者sendError。

這個操作的表意和方法名完全一致,經過interval的時間之後,就算timeout,那麼就停止訂閱原訊號,並sendError。

總結一下ReactiveCocoa v2.5中高階訊號的升階 / 降階操作:

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

升階操作

  1. map( 把值map成一個訊號)
  2. [RACSignal return:signal]

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

降階操作

  1. flatten(等效於flatten:0,+merge:)
  2. concat(等效於flatten:1)
  3. flatten:1
  4. switchToLatest
  5. flattenMap:

這5種操作能將高階訊號變為低階訊號,但是最終降階之後的效果就只有3種:switchToLatest,flatten,concat。具體的圖示見上面的分析。

二. 同步操作

在ReactiveCocoa中還包含一些同步的操作,這些操作一般我們很少使用,除非真的很確定這樣做了之後不會有什麼問題,否則胡亂使用會導致執行緒死鎖等一些嚴重的問題。

1. firstOrDefault: success: error:


- (id)firstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error {
    NSCondition *condition = [[NSCondition alloc] init];
    condition.name = [NSString stringWithFormat:@"[%@] -firstOrDefault: %@ success:error:", self.name, defaultValue];

    __block id value = defaultValue;
    __block BOOL done = NO;

    // Ensures that we don't pass values across thread boundaries by reference.
    __block NSError *localError;
    __block BOOL localSuccess;

    [[self take:1] subscribeNext:^(id x) {
        // 加鎖
        [condition lock];

        value = x;
        localSuccess = YES;

        done = YES;
        [condition broadcast];
        // 解鎖
        [condition unlock];
    } error:^(NSError *e) {
        // 加鎖
        [condition lock];

        if (!done) {
            localSuccess = NO;
            localError = e;

            done = YES;
            [condition broadcast];
        }
        // 解鎖
        [condition unlock];
    } completed:^{
        // 加鎖
        [condition lock];

        localSuccess = YES;

        done = YES;
        [condition broadcast];
        // 解鎖
        [condition unlock];
    }];
    // 加鎖
    [condition lock];
    while (!done) {
        [condition wait];
    }

    if (success != NULL) *success = localSuccess;
    if (error != NULL) *error = localError;
    // 解鎖
    [condition unlock];
    return value;
}複製程式碼

從原始碼上看,firstOrDefault: success: error:這種同步的方法很容易導致執行緒死鎖。它在subscribeNext,error,completed的閉包裡面都呼叫condition鎖先lock再unlock。如果一個訊號傳送值過來,都沒有執行subscribeNext,error,completed這3個操作裡面的任意一個,那麼就會執行[condition wait],等待。

由於對原訊號進行了take:1操作,所以只會對第一個值進行操作。執行完subscribeNext,error,completed這3個操作裡面的任意一個,又會加一次鎖,對外部傳進來的入參success和error進行賦值,已便外部可以拿到裡面的狀態。最終返回訊號是原訊號中第一個next裡面的值,如果原訊號第一個值沒有,比如直接error或者completed,那麼返回的是defaultValue。

done為YES表示已經成功執行了subscribeNext,error,completed這3個操作裡面的任意一個。反之為NO。

localSuccess為YES表示成功傳送值或者成功傳送完了原訊號的所有值,期間沒有發生錯誤。

condition的broadcast操作是喚醒其他執行緒的操作,相當於作業系統裡面互斥訊號量的signal操作。

入參defaultValue是給內部變數value的一個初始值。當原訊號傳送出一個值之後,value的值時刻都會與原訊號的值保持一致。

success和error是外部變數的地址,從外面可以監聽到裡面的狀態。在函式內部賦值,在函式外面拿到它們的值。

2. firstOrDefault:


- (id)firstOrDefault:(id)defaultValue {
    return [self firstOrDefault:defaultValue success:NULL error:NULL];
}複製程式碼

firstOrDefault:的實現就是呼叫了firstOrDefault: success: error:方法。只不過不需要傳success和error,不關心內部的狀態。最終返回訊號是原訊號中第一個next裡面的值,如果原訊號第一個值沒有,比如直接error或者completed,那麼返回的是defaultValue。

3. first


- (id)first {
    return [self firstOrDefault:nil];
}複製程式碼

first方法就更加省略,連defaultValue也不傳。最終返回訊號是原訊號中第一個next裡面的值,如果原訊號第一個值沒有,比如直接error或者completed,那麼返回的是nil。

4. waitUntilCompleted:



- (BOOL)waitUntilCompleted:(NSError **)error {
    BOOL success = NO;

    [[[self
       ignoreValues]
      setNameWithFormat:@"[%@] -waitUntilCompleted:", self.name]
     firstOrDefault:nil success:&success error:error];

    return success;
}複製程式碼

waitUntilCompleted:裡面還是呼叫firstOrDefault: success: error:方法。返回值是success。只要原訊號正常的傳送完訊號,success應該為YES,但是如果傳送過程中出現了error,success就為NO。success作為返回值,外部就可以監聽到是否傳送成功。

雖然這個方法可以監聽到傳送結束的狀態,但是也儘量不要使用,因為它的實現呼叫了firstOrDefault: success: error:方法,這個方法裡面有大量的鎖的操作,一不留神就會導致死鎖。

5. toArray


- (NSArray *)toArray {
    return [[[self collect] first] copy];
}複製程式碼

經過collect之後,原訊號所有的值都會被加到一個陣列裡面,取出訊號的第一個值就是一個陣列。所以執行完first之後第一個值就是原訊號所有值的陣列。

三. 副作用操作

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

ReactiveCocoa v2.5中還為我們提供了一些可以進行副作用操作的函式。

1. doNext:


- (RACSignal *)doNext:(void (^)(id x))block {
    NSCParameterAssert(block != NULL);

    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        return [self subscribeNext:^(id x) {
            block(x);
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            [subscriber sendCompleted];
        }];
    }] setNameWithFormat:@"[%@] -doNext:", self.name];
}複製程式碼

doNext:能讓我們在原訊號sendNext之前,能執行一個block閉包,在這個閉包中我們可以執行我們想要執行的副作用操作。

2. doError:


- (RACSignal *)doError:(void (^)(NSError *error))block {
    NSCParameterAssert(block != NULL);

    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        return [self subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            block(error);
            [subscriber sendError:error];
        } completed:^{
            [subscriber sendCompleted];
        }];
    }] setNameWithFormat:@"[%@] -doError:", self.name];
}複製程式碼

doError:能讓我們在原訊號sendError之前,能執行一個block閉包,在這個閉包中我們可以執行我們想要執行的副作用操作。

3. doCompleted:



- (RACSignal *)doCompleted:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        return [self subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            block();
            [subscriber sendCompleted];
        }];
    }] setNameWithFormat:@"[%@] -doCompleted:", self.name];
}複製程式碼

doCompleted:能讓我們在原訊號sendCompleted之前,能執行一個block閉包,在這個閉包中我們可以執行我們想要執行的副作用操作。

4. initially:


- (RACSignal *)initially:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    return [[RACSignal defer:^{
        block();
        return self;
    }] setNameWithFormat:@"[%@] -initially:", self.name];
}複製程式碼

initially:能讓我們在原訊號傳送之前,先呼叫了defer:操作,在return self之前先執行了一個閉包,在這個閉包中我們可以執行我們想要執行的副作用操作。

5. finally:


- (RACSignal *)finally:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    return [[[self
              doError:^(NSError *error) {
                  block();
              }]
             doCompleted:^{
                 block();
             }]
            setNameWithFormat:@"[%@] -finally:", self.name];
}複製程式碼

finally:操作呼叫了doError:和doCompleted:操作,依次在sendError之前,sendCompleted之前,插入一個block( )閉包。這樣當訊號因為錯誤而要終止取消訂閱,或者,傳送結束之前,都能執行一段我們想要執行的副作用操作。

四. 多執行緒操作

在RACSignal裡面有3個關於多執行緒的操作。

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

1. deliverOn:



- (RACSignal *)deliverOn:(RACScheduler *)scheduler {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        return [self subscribeNext:^(id x) {
            [scheduler schedule:^{
                [subscriber sendNext:x];
            }];
        } error:^(NSError *error) {
            [scheduler schedule:^{
                [subscriber sendError:error];
            }];
        } completed:^{
            [scheduler schedule:^{
                [subscriber sendCompleted];
            }];
        }];
    }] setNameWithFormat:@"[%@] -deliverOn: %@", self.name, scheduler];
}複製程式碼

deliverOn:的入參是一個scheduler,當原訊號subscribeNext,sendError,sendCompleted的時候,都去呼叫scheduler的schedule方法。



- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block];

    block();
    return nil;
}複製程式碼

在schedule的方法裡面會判斷當前currentScheduler是否為nil,如果是nil就呼叫backgroundScheduler去執行block( )閉包,如果不為nil,當前currentScheduler直接執行block( )閉包。



+ (instancetype)currentScheduler {
    RACScheduler *scheduler = NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey];
    if (scheduler != nil) return scheduler;
    if ([self.class isOnMainThread]) return RACScheduler.mainThreadScheduler;

    return nil;
}複製程式碼

判斷currentScheduler是否存在,看兩點,一是當前執行緒的字典裡面,是否存在RACSchedulerCurrentSchedulerKey( @"RACSchedulerCurrentSchedulerKey" ),如果存在對應的value,返回scheduler,二是看當前的類是不是在主執行緒,如果在主執行緒,返回mainThreadScheduler。如果兩個條件都不存在,那麼當前currentScheduler就不存在,返回nil。

deliverOn:操作的特點是原訊號傳送sendNext,sendError,sendCompleted所線上程是確定的。

2. subscribeOn:



- (RACSignal *)subscribeOn:(RACScheduler *)scheduler {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];

        RACDisposable *schedulingDisposable = [scheduler schedule:^{
            RACDisposable *subscriptionDisposable = [self subscribe:subscriber];

            [disposable addDisposable:subscriptionDisposable];
        }];

        [disposable addDisposable:schedulingDisposable];
        return disposable;
    }] setNameWithFormat:@"[%@] -subscribeOn: %@", self.name, scheduler];
}複製程式碼

subscribeOn:操作就是在傳入的scheduler的閉包內部訂閱原訊號的。它與deliverOn:操作就不同:

subscribeOn:操作能夠保證didSubscribe block( )閉包在入參scheduler中執行,但是不能保證原訊號subscribeNext,sendError,sendCompleted在哪個scheduler中執行。

deliverOn:與subscribeOn:正好反過來,能保證原訊號subscribeNext,sendError,sendCompleted在哪個scheduler中執行,但是不能保證didSubscribe block( )閉包在哪個scheduler中執行。

3. deliverOnMainThread



- (RACSignal *)deliverOnMainThread {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        __block volatile int32_t queueLength = 0;

        void (^performOnMainThread)(dispatch_block_t) = ^(dispatch_block_t block) { // 暫時省略};

        return [self subscribeNext:^(id x) {
            performOnMainThread(^{
                [subscriber sendNext:x];
            });
        } error:^(NSError *error) {
            performOnMainThread(^{
                [subscriber sendError:error];
            });
        } completed:^{
            performOnMainThread(^{
                [subscriber sendCompleted];
            });
        }];
    }] setNameWithFormat:@"[%@] -deliverOnMainThread", self.name];
}複製程式碼

對比deliverOn:的原始碼實現,發現兩者比較相似,只不過這裡deliverOnMainThread把sendNext,sendError,sendCompleted都包在了performOnMainThread閉包中執行。


        __block volatile int32_t queueLength = 0;

        void (^performOnMainThread)(dispatch_block_t) = ^(dispatch_block_t block) {
            int32_t queued = OSAtomicIncrement32(&queueLength);
            if (NSThread.isMainThread && queued == 1) {
                block();
                OSAtomicDecrement32(&queueLength);
            } else {
                dispatch_async(dispatch_get_main_queue(), ^{
                    block();
                    OSAtomicDecrement32(&queueLength);
                });
            }
        };複製程式碼

performOnMainThread閉包內部保證了入參block( )閉包一定是在主執行緒中執行。

OSAtomicIncrement32 和 OSAtomicDecrement32是原子操作,分別代表+1和-1。下面的if-else判斷裡面,不管是滿足哪一條,最終都還是在主執行緒中執行block( )閉包。

deliverOnMainThread能保證原訊號subscribeNext,sendError,sendCompleted都在主執行緒MainThread中執行。

五. 其他操作

ReactiveCocoa 中 RACSignal 所有變換操作底層實現分析(下)

1. setKeyPath: onObject: nilValue:

setKeyPath: onObject: nilValue: 的原始碼實現如下:


- (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object nilValue:(id)nilValue {
    NSCParameterAssert(keyPath != nil);
    NSCParameterAssert(object != nil);

    keyPath = [keyPath copy];

    RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];

    __block void * volatile objectPtr = (__bridge void *)object;

    RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) {
        // 1
        __strong NSObject *object __attribute__((objc_precise_lifetime)) = (__bridge __strong id)objectPtr;
        [object setValue:x ?: nilValue forKeyPath:keyPath];
    } error:^(NSError *error) {
        __strong NSObject *object __attribute__((objc_precise_lifetime)) = (__bridge __strong id)objectPtr;

        NSCAssert(NO, @"Received error from %@ in binding for key path \"%@\" on %@: %@", self, keyPath, object, error);
        NSLog(@"Received error from %@ in binding for key path \"%@\" on %@: %@", self, keyPath, object, error);

        [disposable dispose];
    } completed:^{
        [disposable dispose];
    }];

    [disposable addDisposable:subscriptionDisposable];

#if DEBUG
    static void *bindingsKey = &bindingsKey;
    NSMutableDictionary *bindings;

    @synchronized (object) {
        // 2
        bindings = objc_getAssociatedObject(object, bindingsKey);
        if (bindings == nil) {
            bindings = [NSMutableDictionary dictionary];
            objc_setAssociatedObject(object, bindingsKey, bindings, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }

    @synchronized (bindings) {
        NSCAssert(bindings[keyPath] == nil, @"Signal %@ is already bound to key path \"%@\" on object %@, adding signal %@ is undefined behavior", [bindings[keyPath] nonretainedObjectValue], keyPath, object, self);

        bindings[keyPath] = [NSValue valueWithNonretainedObject:self];
    }
#endif

    RACDisposable *clearPointerDisposable = [RACDisposable disposableWithBlock:^{
#if DEBUG
        @synchronized (bindings) {
            // 3
            [bindings removeObjectForKey:keyPath];
        }
#endif

        while (YES) {
            void *ptr = objectPtr;
            // 4
            if (OSAtomicCompareAndSwapPtrBarrier(ptr, NULL, &objectPtr)) {
                break;
            }
        }
    }];

    [disposable addDisposable:clearPointerDisposable];

    [object.rac_deallocDisposable addDisposable:disposable];

    RACCompoundDisposable *objectDisposable = object.rac_deallocDisposable;
    return [RACDisposable disposableWithBlock:^{
        [objectDisposable removeDisposable:disposable];
        [disposable dispose];
    }];
}複製程式碼

程式碼雖然有點長,但是逐行讀下來不是很難,需要注意的有4點地方,已經在上述程式碼裡面標明瞭。接下來一一分析。

1. objc_precise_lifetime的問題。

作者在這裡寫了一段註釋:

Possibly spec, possibly compiler bug, but this __bridge cast does not result in a retain here, effectively an invisible __unsafe_unretained qualifier. Using objc_precise_lifetime gives the __strong reference desired. The explicit use of __strong is strictly defensive.

作者懷疑是編譯器的一個bug,即使是顯示的呼叫了__strong,依舊沒法保證被強引用了,所以還需要用objc_precise_lifetime來保證強引用。

關於這個問題,筆者查詢了一下LLVM的文件,在6.3 precise lifetime semantics這一節中提到了這個問題。

通常上,凡是宣告瞭__strong的變數,都會有很確切的生命週期。ARC會維持這些__strong的變數在其生命週期中被retained。

但是自動儲存的區域性變數是沒有確切的生命週期的。這些變數僅僅只是簡單的持有一個強引用,強引用著retain物件的指標型別的值。這些值完全受控於本地控制者的如何優化。所以要想改變這些區域性變數的生命週期,是不可能的事情。因為有太多的優化,理論上都會導致區域性變數的生命週期減少,但是這些優化非常有用。

但是LLVM為我們提供了一個關鍵字objc_precise_lifetime,使用這個可以是區域性變數的生命週期變成確切的。這個關鍵字有時候還是非常有用的。甚至更加極端情況,該區域性變數都沒有被使用,但是它依舊可以保持一個確定的生命週期。

回到原始碼上來,接著程式碼會對入參object進行setValue: forKeyPath:


[object setValue:x ?: nilValue forKeyPath:keyPath];複製程式碼

如何x為nil就返回nilValue傳進來的值。

2. AssociatedObject關聯物件

如果bindings字典不存在,那麼就呼叫objc_setAssociatedObject對object進行關聯物件。引數是OBJC_ASSOCIATION_RETAIN_NONATOMIC。如果bindings字典存在,就用objc_getAssociatedObject取出字典。

在字典裡面重新更新繫結key-value值,key就是入參keyPath,value是原訊號。

3. 取消訂閱原訊號的時候

[bindings removeObjectForKey:keyPath];複製程式碼

當訊號取消訂閱的時候,移除所有的關聯值。

3. OSAtomicCompareAndSwapPtrBarrier

這個函式屬於OSAtomic原子操作,原型如下:


OSAtomicCompareAndSwapPtrBarrier(type __oldValue, type __newValue, volatile type *__theValue)複製程式碼

Compares a variable against the specified old value. If the two values are equal, this function assigns the specified new value to the variable; otherwise, it does nothing. The comparison and assignment are done as one atomic operation and the function returns a Boolean value indicating whether the swap actually occurred.

這個函式用於比較__oldValue是否與__theValue指標指向的記憶體位置的值匹配,如果匹配,則將__newValue的值儲存到__theValue指向的記憶體位置。整個函式的返回值就是交換是否成功的BOOL值。

    while (YES) {
    void *ptr = objectPtr;
    if (OSAtomicCompareAndSwapPtrBarrier(ptr, NULL, &objectPtr))   {
          break;
    }
  }複製程式碼

在這個while的死迴圈裡面只有當OSAtomicCompareAndSwapPtrBarrier返回值為YES,才能退出整個死迴圈。返回值為YES就代表&objectPtr被置為了NULL,這樣就確保了線上程安全的情況下,不存在野指標的問題了。

2. setKeyPath: onObject:


- (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object {
    return [self setKeyPath:keyPath onObject:object nilValue:nil];
}複製程式碼

setKeyPath: onObject:就是呼叫setKeyPath: onObject: nilValue:方法,只不過nilValue傳遞的是nil。

最後

關於RACSignal的所有操作底層分析實現都已經分析完成。最後請大家多多指教。

相關文章