1. 程式人生 > >ReactiveCocoa 中 RACCommand底層實現分析

ReactiveCocoa 中 RACCommand底層實現分析

 111194012-4d5be0a2fbf5eeac

前言

在ReactiveCocoa 過程中,除去RACSignal和RACSubject這些訊號類以外,有些時候我們可能還需要封裝一些固定的操作集合。這些操作集合都是固定的,每次只要一觸發就會執行事先定義好的一個過程。在iOS開發過程中,按鈕的點選事件就可能有這種需求。那麼RACCommand就可以實現這種需求。

當然除了封裝一個操作集合以外,RACCommand還能集中處理錯誤等等功能。今天就來從底層來看看RACCommand是如何實現的。

目錄

  • 1.RACCommand的定義
  • 2.initWithEnabled: signalBlock: 底層實現分析
  • 3.execute:底層實現分析
  • 4.RACCommand的一些Category

一. RACCommand的定義

121194012-903431334c6f8f7b

首先說說RACCommand的作用。
RACCommand 在ReactiveCocoa 中是對一個動作的觸發條件以及它產生的觸發事件的封裝。

  • 觸發條件:初始化RACCommand的入參enabledSignal就決定了RACCommand是否能開始執行。入參enabledSignal就是觸發條件。舉個例子,一個按鈕是否能點選,是否能觸發點選事情,就由入參enabledSignal決定。
  • 觸發事件:初始化RACCommand的另外一個入參(RACSignal * (^)(id input))signalBlock就是對觸發事件的封裝。RACCommand每次執行都會呼叫一次signalBlock閉包。

RACCommand最常見的例子就是在註冊登入的時候,點選獲取驗證碼的按鈕,這個按鈕的點選事件和觸發條件就可以用RACCommand來封裝,觸發條件是一個訊號,它可以是驗證手機號,驗證郵箱,驗證身份證等一些驗證條件產生的enabledSignal。觸發事件就是按鈕點選之後執行的事件,可以是傳送驗證碼的網路請求。

RACCommand在ReactiveCocoa中算是很特別的一種存在,因為它的實現並不是FRP實現的,是OOP實現的。RACCommand的本質就是一個物件,在這個物件裡面封裝了4個訊號。

關於RACCommand的定義如下:

1234567891011121314 @interfaceRACCommand:NSObject@property(nonatomic,strong,readonly)RACSignal *executionSignals;@property(nonatomic,strong,readonly)RACSignal *executing;@property(nonatomic,strong,readonly)RACSignal *enabled;@property(nonatomic,strong,readonly)RACSignal *errors;@property(atomic,assign)BOOLallowsConcurrentExecution;volatile uint32_t _allowsConcurrentExecution;@property(atomic,copy,readonly)NSArray *activeExecutionSignals;NSMutableArray *_activeExecutionSignals;@property(nonatomic,strong,readonly)RACSignal *immediateEnabled;@property(nonatomic,copy,readonly)RACSignal *(^signalBlock)(id input);@end

RACCommand中4個最重要的訊號就是定義開頭的那4個訊號,executionSignals,executing,enabled,errors。需要注意的是,這4個訊號基本都是(並不是完全是)在主執行緒上執行的

1. RACSignal *executionSignals

executionSignals是一個高階訊號,所以在使用的時候需要進行降階操作,降價操作在前面分析過了,在ReactiveCocoa v2.5中只支援3種降階方式,flatten,switchToLatest,concat。降階的方式就根據需求來選取。

還有選擇原則是,如果在不允許Concurrent併發的RACCommand中一般使用switchToLatest。如果在允許Concurrent併發的RACCommand中一般使用flatten。

2. RACSignal *executing

executing這個訊號就表示了當前RACCommand是否在執行,訊號裡面的值都是BOOL型別的。YES表示的是RACCommand正在執行過程中,命名也說明的是正在進行時ing。NO表示的是RACCommand沒有被執行或者已經執行結束。

3. RACSignal *enabled

enabled訊號就是一個開關,RACCommand是否可用。這個訊號除去以下2種情況會返回NO:

  • RACCommand 初始化傳入的enabledSignal訊號,如果返回NO,那麼enabled訊號就返回NO。
  • RACCommand開始執行中,allowsConcurrentExecution為NO,那麼enabled訊號就返回NO。

除去以上2種情況以外,enabled訊號基本都是返回YES。

4. RACSignal *errors

errors訊號就是RACCommand執行過程中產生的錯誤訊號。這裡特別需要注意的是:在對RACCommand進行錯誤處理的時候,我們不應該使用subscribeError:對RACCommand的executionSignals
進行錯誤的訂閱
,因為executionSignals這個訊號是不會發送error事件的,那當RACCommand包裹的訊號傳送error事件時,我們要怎樣去訂閱到它呢?應該用subscribeNext:去訂閱錯誤訊號

123 [commandSignal.errors subscribeNext:^(NSError *x){NSLog(@"ERROR! --> %@",x);}];

5. BOOL allowsConcurrentExecution

131194012-4c6a13ff1ebeb787

allowsConcurrentExecution是一個BOOL變數,它是用來表示當前RACCommand是否允許併發執行。預設值是NO。

如果allowsConcurrentExecution為NO,那麼RACCommand在執行過程中,enabled訊號就一定都返回NO,不允許併發執行。如果allowsConcurrentExecution為YES,允許併發執行。

如果是允許併發執行的話,就會出現多個訊號就會出現一起傳送值的情況。那麼這種情況產生的高階訊號一般可以採取flatten(等效於flatten:0,+merge:)的方式進行降階。

這個變數在具體實現中是用的volatile原子的操作,在實現中重寫了它的get和set方法。

1234567891011121314151617 // 重寫 get方法-(BOOL)allowsConcurrentExecution{return_allowsConcurrentExecution!=0;}// 重寫 set方法-(void)setAllowsConcurrentExecution:(BOOL)allowed{[selfwillChangeValueForKey:@keypath(self.allowsConcurrentExecution)];if(allowed){OSAtomicOr32Barrier(1,&_allowsConcurrentExecution);}else{OSAtomicAnd32Barrier(0,&_allowsConcurrentExecution);}[selfdidChangeValueForKey:@keypath(self.allowsConcurrentExecution)];}

OSAtomicOr32Barrier是原子運算,它的意義是進行邏輯的“或”運算。通過原子性操作訪問被volatile修飾的_allowsConcurrentExecution物件即可保障函式只執行一次。相應的OSAtomicAnd32Barrier也是原子運算,它的意義是進行邏輯的“與”運算。

6. NSArray *activeExecutionSignals

這個NSArray數組裡面裝了一個個有序排列的,執行中的訊號。NSArray的陣列是可以被KVO監聽的。

12345 -(NSArray *)activeExecutionSignals{@synchronized(self){return[_activeExecutionSignals copy];}}

當然內部還有一個NSMutableArray的版本,NSArray陣列是它的copy版本,使用它的時候需要加上執行緒鎖,進行執行緒安全的保護。

在RACCommand內部,是對NSMutableArray陣列進行操作的,在這裡可變數組裡面進行增加和刪除的操作。

12345678910 -(void)addActiveExecutionSignal:(RACSignal *)signal{NSCParameterAssert([signal isKindOfClass:RACSignal.class]);@synchronized(self){NSIndexSet *indexes=[NSIndexSet indexSetWithIndex:_activeExecutionSignals.count];[selfwillChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];[_activeExecutionSignals addObject:signal];[selfdidChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];}}

在往數組裡面新增資料的時候是滿足KVO的,這裡對index進行了NSKeyValueChangeInsertion監聽。

123456789101112131415 -(void)removeActiveExecutionSignal:(RACSignal *)signal{NSCParameterAssert([signal isKindOfClass:RACSignal.class]);@synchronized(self){NSIndexSet *indexes=[_activeExecutionSignals indexesOfObjectsPassingTest:^BOOL(RACSignal *obj,NSUInteger index,BOOL*stop){returnobj==signal;}];if(indexes.count==0)return;[selfwillChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];[_activeExecutionSignals removeObjectsAtIndexes:indexes];[selfdidChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];}}

在移除數組裡面也是依照indexes來進行移除的。注意,增加和刪除的操作都必須包在@synchronized (self)中保證執行緒安全。

123 +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{returnNO;}

從上面增加和刪除的操作中我們可以看見了RAC的作者在手動傳送change notification,手動呼叫willChange: 和 didChange:方法。作者的目的在於防止一些不必要的swizzling可能會影響到增加和刪除的操作,所以這裡選擇的手動傳送通知的方式。

美團部落格上這篇ReactiveCocoa核心元素與訊號流文章裡面對activeExecutionSignals的變化引起的一些變化畫了一張資料流圖:

141194012-09f3c04492ebc2c7

除去沒有影響到enabled訊號,activeExecutionSignals的變化會影響到其他三個訊號。

7. RACSignal *immediateEnabled

151194012-f16ac5a7956fb4b5

這個訊號也是一個enabled訊號,但是和之前的enabled訊號不同的是,它並不能保證在main thread主執行緒上,它可以在任意一個執行緒上。

8. RACSignal * (^signalBlock)(id input)

這個閉包返回值是一個訊號,這個閉包是在初始化RACCommand的時候會用到,下面分析原始碼的時候會出現。

123 -(id)initWithSignalBlock:(RACSignal *(^)(id input))signalBlock;-(id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal *(^)(id input))signalBlock;-(RACSignal *)execute:(id)input;

RACCommand 暴露出來的就3個方法,2個初始化方法和1個execute:的方法,接下來就來分析一下這些方法的底層實現。

二. initWithEnabled: signalBlock: 底層實現分析

161194012-275a60e23a63f935

首先先來看看比較短的那個初始化方法。

123 -(id)initWithSignalBlock:(RACSignal *(^)(id input))signalBlock{return[selfinitWithEnabled:nil signalBlock:signalBlock];}

initWithSignalBlock:方法實際就是呼叫了initWithEnabled: signalBlock:方法。

123 -(id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal *(^)(id input))signalBlock{}

initWithSignalBlock:方法相當於第一個引數傳的是nil的initWithEnabled: signalBlock:方法。第一個引數是enabledSignal,第二個引數是signalBlock的閉包。enabledSignal如果傳的是nil,那麼就相當於是傳進了[RACSignal return:@YES]。

接下來詳細分析一下initWithEnabled: signalBlock:方法的實現。

這個方法的實現非常長,需要分段來分析。RACCommand的初始化就是對自己的4個訊號,executionSignals,executing,enabled,errors的初始化。

1. executionSignals訊號的初始化

1234567891011 RACSignal *newActiveExecutionSignals=[[[[[selfrac_valuesAndChangesForKeyPath:@keypath(self.activeExecutionSignals)options:NSKeyValueObservingOptionNew observer:nil]reduceEach:^(id_,NSDictionary *change){NSArray *signals=change[NSKeyValueChangeNewKey];if(signals==nil)return[RACSignal empty];return[signals.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];}]concat]publish]autoconnect];

通過rac_valuesAndChangesForKeyPath: options: observer: 方法監聽self.activeExecutionSignals數組裡面是否有增加新的訊號。rac_valuesAndChangesForKeyPath: options: observer: 方法的返回時是一個RACTuple,它的定義是這樣的:RACTuplePack(value, change)。

只要每次數組裡面加入了新的訊號,那麼rac_valuesAndChangesForKeyPath: options: observer: 方法就會把新加的值和change字典包裝成RACTuple返回。再對這個訊號進行一次reduceEach:操作。

舉個例子,change字典可能是如下的樣子:

1234567 {indexes="[number of indexes: 1 (in 1 ranges), indexes: (0)]";kind=2;new=(" name: ");}

取出change[NSKeyValueChangeNewKey]就能取出每次變化新增的訊號陣列,然後把這個陣列通過signalWithScheduler:轉換成訊號。

把原訊號中每個值是裡面裝滿RACTuple的訊號通過變換,變換成了裝滿RACSingnal的三階訊號,通過concat進行降階操作,降階成了二階訊號。最後通過publish和autoconnect操作,把冷訊號轉換成熱訊號。

newActiveExecutionSignals最終是一個二階熱訊號。

接下來再看看executionSignals是如何變換而來的。