前言
緊接著上篇的原始碼實現分析,繼續分析RACSignal的變換操作的底層實現。
目錄
- 1.高階訊號操作
- 2.同步操作
- 3.副作用操作
- 4.多執行緒操作
- 5.其他操作
一. 高階訊號操作
高階操作大部分的操作是針對高階訊號的,也就是說訊號裡面傳送的值還是一個訊號或者是一個高階訊號。可以類比陣列,這裡就是多維陣列,陣列裡面還是套的陣列。
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:操作之後,返回的新的訊號的每個值就是原訊號中每個訊號的值。
如果對訊號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 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();
}
}]];複製程式碼
如果當前最大可容納訊號的個數 > 0 ,且,activeDisposables陣列裡面已經裝滿到最大可容納訊號的個數,不能再裝新的訊號了。那麼就把當前的訊號快取到queuedSignals陣列中。
直到activeDisposables陣列裡面有空的位子可以加入新的訊號,那麼就呼叫subscribeToSignal( )閉包,開始訂閱這個新的訊號。
最後完成的時候標記變數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);
}];
};複製程式碼
- activeDisposables先新增當前高階訊號發出來的訊號的Disposable( 也就是入參訊號的Disposable)
- 這裡會對recur進行__strong,因為下面第6步會用到subscribeToSignal( )閉包,同樣也是為了防止出現迴圈引用。
- 訂閱入參訊號,給訂閱者傳送訊號。當傳送完畢後,activeDisposables中移除它對應的Disposable。
- 如果當前快取的queuedSignals陣列裡面沒有快取的訊號,那麼就呼叫completeIfAllowed( )閉包。
- 如果當前快取的queuedSignals陣列裡面有快取的訊號,那麼就取出第0個訊號,並在queuedSignals陣列移除它。
- 把第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了。
如果maxConcurrent = 1,會發生什麼?那麼flatten:就退化成concat了。
如果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];
}複製程式碼
經過對比可以發現,雖然最終變換出來的結果類似,但是針對的訊號的物件是不同的,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就會被取消訂閱,開始訂閱新的訊號。
一個高階訊號經過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中高階訊號的升階 / 降階操作:
升階操作:
- map( 把值map成一個訊號)
- [RACSignal return:signal]
降階操作:
- flatten(等效於flatten:0,+merge:)
- concat(等效於flatten:1)
- flatten:1
- switchToLatest
- 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 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個關於多執行緒的操作。
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中執行。
五. 其他操作
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的所有操作底層分析實現都已經分析完成。最後請大家多多指教。