RAC實踐採坑指北

Yang1492955186752發表於2017-12-25

由於新入職的團隊使用的是RAC,因此需要熟悉一下RAC的類圖和大致的實現。 類圖大致如下:

RAC實踐採坑指北

RACSequence

和Cocoa內建的集合物件(NSArray,NSSet)類似,內部不能包含nil,是RACStream(一個抽象類,用於表示為訊號流的值)的子類,RACSequence是拉力驅動(被動)的資料流,因此預設是惰性求值,並且當呼叫mapfalttenMap之類的方法時,block對內部的物件求值只會進行一次。 借用RAC官方Demo

 NSArray *strings = @[ @"A", @"B", @"C" ];
    RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
        NSLog(@"%@", str);
        return [str stringByAppendingString:@"_"];
    }];
    
    // Logs "A" during this call.
    NSString *concatA = sequence.head;
    
    // Logs "B" during this call.
    NSString *concatB = sequence.tail.head;
    
    // Does not log anything.
    NSString *concatB2 = sequence.tail.head;
    
    RACSequence *derivedSequence = [sequence map:^(NSString *str) {
        return [@"_" stringByAppendingString:str];
    }];
    //  Does not log anything, and concatA2 value is A_ ,NOT _A_
    NSString *concatA2 = sequence.head;
複製程式碼

RACSignal

RACSignal是專注於解決通過訂閱訊號來非同步進行事件傳輸 RAC是執行緒安全的,因此可以在任意執行緒進行signal傳送,但是一個訂閱者只能序列的處理一個訊號,而不能併發的處理多個訊號。 因此-subscribeNext:error:completed:block不需要進行synchronized

bind

利用一段程式碼來測試bind函式的呼叫順序,由於程式碼結構複雜,所以在bind模組對應的block都會標有數字,方便描述呼叫順序。

 RACSignal *sourceSig = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        //doSomething
        //...
        //...block1
        NSLog(@"\nbegin---\n%@\n---end",@"dosomething");
        [subscriber sendNext:@"hello world"];
//        [subscriber sendCompleted];
        return nil;
    }];
    
    RACSignal *bindSig = [sourceSig bind:^RACStreamBindBlock{
        //block2
        return ^(id value, BOOL *stop) {
            //block3
            //這裡對value進行處理
            return [RACSignal return:value];
        };
    }];
    
    [bindSig subscribeNext:^(id x) {
        //block4
        NSLog(@"\nbegin---\n%@\n---end",x);
    }];
複製程式碼

1.createSignal:的作用是將傳的:^RACDisposable *(id<RACSubscriber> subscriber)這個block存到sourceSigdidSubscribe欄位中(block1)

RAC實踐採坑指北

2.bind:通過呼叫createSignal:返回一個新的訊號bindSigbind: 的引數是一個沒有入參,返回值為RACStreamBindBlock的block(block2)。 RACStreamBindBlock入參和出參如下:

typedef RACSignal * _Nullable (^RACSignalBindBlock)(ValueType _Nullable value, BOOL *stop);
複製程式碼

通過改變傳入進來的Value(也就是改變block3的內部實現 ),從而實現了flattenMap:,skip:,takeUntilBlock:,distinctUntilChanged:等高階操作。

- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block {
    //返回bindSig,並將block儲存至didSubscribe
	return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
		//省略didSubscribe內部程式碼
	}] setNameWithFormat:@"[%@] -bind:", self.name];
}

複製程式碼

3.當bindSig 呼叫subscribeNext:,生成一個RACSubscriber,並將nextBlock儲存在_next中

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
	NSCParameterAssert(nextBlock != NULL);
	
	RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
	return [self subscribe:o];
}
複製程式碼

然後bindSig呼叫subscribe:,入參就是這個subscribe

4.在subcribe:中,呼叫bindSig儲存的didSubscribe ,執行一長串程式碼(block5)

return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
                //block5
		RACStreamBindBlock bindingBlock = block();
                 //這裡的self是sourceSig
		NSMutableArray *signals = [NSMutableArray arrayWithObject:self];
            
		RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];
        
		void (^completeSignal)(RACSignal *, RACDisposable *) = ^(RACSignal *signal, RACDisposable *finishedDisposable) {
		        //block6
			BOOL removeDisposable = NO;

			@synchronized (signals) {
				[signals removeObject:signal];

				if (signals.count == 0) {
					[subscriber sendCompleted];
					[compoundDisposable dispose];
				} else {
					removeDisposable = YES;
				}
			}

			if (removeDisposable) [compoundDisposable removeDisposable:finishedDisposable];
		};

		void (^addSignal)(RACSignal *) = ^(RACSignal *signal) {
	        	//block7
			@synchronized (signals) {
				[signals addObject:signal];
			}

			RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init];
			[compoundDisposable addDisposable:selfDisposable];
                        //4.訂閱newSig,然後將newSig的值傳給bindSig的訂閱者,執行block8
			RACDisposable *disposable = [signal subscribeNext:^(id x) {
			        //block8
			        //這裡是subscriber對應的是bindSig
				[subscriber sendNext:x];
				//5.然後執行block4
			} error:^(NSError *error) {
				[compoundDisposable dispose];
				[subscriber sendError:error];
			} completed:^{
				@autoreleasepool {
					completeSignal(signal, selfDisposable);
				}
			}];

			selfDisposable.disposable = disposable;
		};

		@autoreleasepool {
			RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init];
			[compoundDisposable addDisposable:selfDisposable];
                         //1.先執行block1,然後執行block9
			RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
				// Manually check disposal to handle synchronous errors.
				//block9
				if (compoundDisposable.disposed) return;

				BOOL stop = NO;
				//對sourceSig傳的值進行處理,再包裝在新值(可為nil)簡稱newSig
				//2.再執行block3
				id signal = bindingBlock(x, &stop);

				@autoreleasepool {
				    //3.假如block3返回的sig不為nil執行block7
					if (signal != nil) addSignal(signal);
				    //假如block3返回的sig為nil或者stop指標為YES,執行block6
					if (signal == nil || stop) {
						[selfDisposable dispose];
						completeSignal(self, selfDisposable);
					}
				}
			} error:^(NSError *error) {
				[compoundDisposable dispose];
				[subscriber sendError:error];
			} completed:^{
				@autoreleasepool {
					completeSignal(self, selfDisposable);
				}
			}];

			selfDisposable.disposable = bindingDisposable;
		}

		return compoundDisposable;
	}] setNameWithFormat:@"[%@] -bind:", self.name];
複製程式碼

總結一下bind的作用:生成一個新的訊號bindSig,訂閱源訊號sourceSig,當sourceSig傳送一個值時,bindSig通過訂閱收到這個值後,根據上層傳的RACStreamBindBlock轉換value,傳送給bindSig的subscriber。

ATTENTION

由於RACSignal是冷訊號,所以每次有新的訂閱都會觸發副作用(對應的block),這意味著 singal對應的block會執行多次。

__block int missilesToLaunch = 0;

// Signal that will have the side effect of changing `missilesToLaunch` on
// subscription.
RACSignal *processedSignal = [[RACSignal return:@"missiles"]
	map:^(id x) {
		missilesToLaunch++;
		return [NSString stringWithFormat:@"will launch %d %@", missilesToLaunch, x];
	}];

// This will print "First will launch 1 missiles"
[processedSignal subscribeNext:^(id x) {
	NSLog(@"First %@", x);
}];

// This will print "Second will launch 2 missiles"
[processedSignal subscribeNext:^(id x) {
	NSLog(@"Second %@", x);
}];
複製程式碼

假如想冷訊號執行一次,就得轉換成熱訊號。比如網路請求肯定只需要一次就好,所以在業務場景中通過multicast使用,可以避免冷訊號的的多次呼叫

// This signal starts a new request on each subscription.
RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    AFHTTPRequestOperation *operation = [client
        HTTPRequestOperationWithRequest:request
        success:^(AFHTTPRequestOperation *operation, id response) {
            [subscriber sendNext:response];
            [subscriber sendCompleted];
        }
        failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            [subscriber sendError:error];
        }];

    [client enqueueHTTPRequestOperation:operation];
    return [RACDisposable disposableWithBlock:^{
        [operation cancel];
    }];
}];

// Starts a single request, no matter how many subscriptions `connection.signal`
// gets. This is equivalent to the -replay operator, or similar to
// +startEagerlyWithScheduler:block:.
// single中除了Subject之外的都是冷訊號,Subject是熱訊號。
RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]];
[connection connect];

[connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber one: %@", response);
}];

[connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber two: %@", response);
}];
複製程式碼

當我們需要在nextBlock之前需要加一些副作用程式碼,就可以呼叫-doNext,這時候會先呼叫這裡的block,再呼叫subscribersendNext

UI事件

RAC(self.label,text,@"nil的值") = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        __block int i = 0;
        [[self.button rac_signalForControlEvents:UIControlEventTouchDown] subscribeNext:^(id x) {
            i ++;
            if (i > 3) {
                [subscriber sendNext:nil];
            }
            else {
                [subscriber sendNext:@"123"];
            }
            
        }];
        return nil;
        }];
複製程式碼

通知

當我們用RAC來改寫NSNotification的時候用rac_addObserverForName: 比如我們需要監聽網路狀態時

    //當網路發生變化後,RAC這個巨集會進行keypath繫結,會將self.NetWorkStatus 賦予新值,這時其他利用RACObserve會收到這個變化並作出對應改
    RAC(self, NetWorkStatus) = [[[[NSNotificationCenter defaultCenter]
                                   rac_addObserverForName:kRealReachabilityChangedNotification object:nil]
                                  map:^(NSNotification *notification) {
                                      return @([notification.object currentReachabilityStatus]);
                                  }]
                                distinctUntilChanged];

    //RACObserve接受新值並訂閱訊號
    [RACObserve(self , NetWorkStatus) subscribeNext:^(NSNumber *networkStatus) {
        
        @strongify(self);
        if (networkStatus.integerValue == RealStatusNotReachable || networkStatus.integerValue == RealStatusUnknown) {
            [self.viewModel showErrorView];
        }else{
            [self.viewModel request];
        }
    }];  

複製程式碼

協議

   @weakify(self);
    [[self
      rac_signalForSelector:@selector(webViewDidStartLoad:)
      fromProtocol:@protocol(WebViewDelegate)]
    	subscribeNext:^(RACTuple *tuple) {
            @strongify(self)
            if (tuple.first == self.webView){
                dispatch_main_async_safe(^{
                    [self showStatusWithMessage:@"Loading..."];
                });
            }
        }];
複製程式碼

網路事件(耗時事件)

 
    __block int callCount = 0;
    這裡因為訂閱了兩次,所以會呼叫兩次block,因此假如是io類操作,最好將networkSig包裝成RACSubject然後通過multicast廣播
    self.networkSig = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        __block int i = 0;
        callCount ++;
        //列印兩次
        NSLog(@"\nbegin---\n callCount ==%d\n---end",callCount );
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            i++;
            [subscriber sendNext:@(i)];
        });
        return nil;
    }];
    
    
    
    [self.networkSig subscribeNext:^(id x) {
        NSLog(@"\nbegin---\nfirst i ====  %@\n---end", x);
    }];
    
    [self.networkSig subscribeNext:^(id x) {
        NSLog(@"\nbegin---\nsecond i ====  %@\n---end", x);
    }];
複製程式碼

改進後:

    __block int callCount = 0;
    self.networkSig = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        __block int i = 0;
        callCount ++;
        //只會列印一次
        NSLog(@"\nbegin---\n callCount ==%d\n---end",callCount );
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            i++;
            [subscriber sendNext:@(i)];
        });
        return nil;
    }];
    
    RACSubject *subject = [RACSubject subject];
    RACMulticastConnection *multicastConnection = [self.networkSig multicast:subject];
    [multicastConnection connect];
    
    [multicastConnection.signal subscribeNext:^(id x) {
        NSLog(@"\nbegin---\nfirst i ====  %@\n---end", x);
    }];
    
    [multicastConnection.signal subscribeNext:^(id x) {
        NSLog(@"\nbegin---\nsecond i ====  %@\n---end", x);
    }];
複製程式碼

KVO

 //實現self.navigationItem.title 和 self.viewModel.title的單向繫結
 RAC(self.navigationItem,title) = RACObserve(self.viewModel, title);
複製程式碼

RACCommand

建立RACCommand的時候需要返回一個signal,當呼叫execute:,signal必須呼叫sendCompletedsendError:,command才能進行下次execute:

初學者可能會想當然如下寫程式碼

    //1.先繫結self.button的keypath:enable
    RAC(self.button,enabled) = [RACSignal combineLatest:@[self.userNameField.rac_textSignal,self.passwordField.rac_textSignal]
                                                 reduce:^id(NSString *userName,NSString *password){
                                                     return @(userName.length >= 8 && password.length >= 6);
                                                 }];
    //2.然後設定button的點選事件
    self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [self login];
    }];
複製程式碼

這時候執行程式的時候報錯

RAC實踐採坑指北
這是因為RAC()這個巨集和button.rac_command都會呼叫setKeyPath:onObject:nilValue:這個方法。 首次呼叫時,會通過objc_setAssociatedObject將keypath儲存起來,當重複呼叫相同的keypath的時候會觸發NSCAssert 正確的做法是

    RACSignal *buttonEnabled = [RACSignal combineLatest:@[self.userNameField.rac_textSignal,self.passwordField.rac_textSignal]
                                                 reduce:^id(NSString *userName,NSString *password){
                                                     return @(userName.length >= 8 && password.length >= 6);
                                                 }];
    self.button.rac_command = [[RACCommand alloc] initWithEnabled:buttonEnabled signalBlock:^RACSignal *(id input) {
        return [self login];
    }];
複製程式碼

相關文章