ReactiveCocoa 中 RACCommand 底層實現分析

一縷殤流化隱半邊冰霜發表於2017-01-08

前言

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

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

目錄

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

一. RACCommand的定義

ReactiveCocoa 中 RACCommand 底層實現分析

首先說說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的定義如下:


@interface RACCommand : 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) BOOL allowsConcurrentExecution;
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:去訂閱錯誤訊號


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

5. BOOL allowsConcurrentExecution

ReactiveCocoa 中 RACCommand 底層實現分析

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

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

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

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


// 重寫 get方法
- (BOOL)allowsConcurrentExecution {
    return _allowsConcurrentExecution != 0;
}

// 重寫 set方法
- (void)setAllowsConcurrentExecution:(BOOL)allowed {
    [self willChangeValueForKey:@keypath(self.allowsConcurrentExecution)];

    if (allowed) {
        OSAtomicOr32Barrier(1, &_allowsConcurrentExecution);
    } else {
        OSAtomicAnd32Barrier(0, &_allowsConcurrentExecution);
    }

    [self didChangeValueForKey:@keypath(self.allowsConcurrentExecution)];
}複製程式碼

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

6. NSArray *activeExecutionSignals

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


- (NSArray *)activeExecutionSignals {
    @synchronized (self) {
        return [_activeExecutionSignals copy];
    }
}複製程式碼

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

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


- (void)addActiveExecutionSignal:(RACSignal *)signal {
    NSCParameterAssert([signal isKindOfClass:RACSignal.class]);

    @synchronized (self) {
        NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:_activeExecutionSignals.count];
        [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];
        [_activeExecutionSignals addObject:signal];
        [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];
    }
}複製程式碼

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


- (void)removeActiveExecutionSignal:(RACSignal *)signal {
    NSCParameterAssert([signal isKindOfClass:RACSignal.class]);

    @synchronized (self) {
        NSIndexSet *indexes = [_activeExecutionSignals indexesOfObjectsPassingTest:^ BOOL (RACSignal *obj, NSUInteger index, BOOL *stop) {
            return obj == signal;
        }];

        if (indexes.count == 0) return;

        [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];
        [_activeExecutionSignals removeObjectsAtIndexes:indexes];
        [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@keypath(self.activeExecutionSignals)];
    }
}複製程式碼

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


+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}複製程式碼

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

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

ReactiveCocoa 中 RACCommand 底層實現分析

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

7. RACSignal *immediateEnabled

ReactiveCocoa 中 RACCommand 底層實現分析

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

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

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


- (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: 底層實現分析

ReactiveCocoa 中 RACCommand 底層實現分析

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


- (id)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock {
    return [self initWithEnabled:nil signalBlock:signalBlock];
}複製程式碼

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


- (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訊號的初始化


RACSignal *newActiveExecutionSignals = [[[[[self rac_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字典可能是如下的樣子:


{
    indexes = "<_NSCachedIndexSet: 0x60000023b8a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        "<RACReplaySubject: 0x6000006613c0> name: "
    );
}複製程式碼

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

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

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

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


_executionSignals = [[[newActiveExecutionSignals
                       map:^(RACSignal *signal) {
                           return [signal catchTo:[RACSignal empty]];
                       }]
                      deliverOn:RACScheduler.mainThreadScheduler]
                     setNameWithFormat:@"%@ -executionSignals", self];複製程式碼

executionSignals把newActiveExecutionSignals中錯誤訊號都換成空訊號。經過map變換之後,executionSignals是newActiveExecutionSignals的無錯誤訊號的版本。由於map只是變換並沒有降階,所以executionSignals還是一個二階的高階冷訊號。

注意最後加上了deliverOn,executionSignals訊號每個值都是在主執行緒中傳送的。

2. errors訊號的初始化

在RACCommand中會蒐集其所有的error訊號,都裝進自己的errors的訊號中。這也是RACCommand的特點之一,能把錯誤統一處理。


RACMulticastConnection *errorsConnection = [[[newActiveExecutionSignals
                                              flattenMap:^(RACSignal *signal) {
                                                  return [[signal ignoreValues]
                                                          catch:^(NSError *error) {
                                                              return [RACSignal return:error];
                                                          }];
                                              }]
                                             deliverOn:RACScheduler.mainThreadScheduler]
                                             publish];複製程式碼

從上面分析中,我們知道,newActiveExecutionSignals最終是一個二階熱訊號。這裡在errorsConnection的變換中,我們對這個二階的熱訊號進行flattenMap:降階操作,只留下所有的錯誤訊號,最後把所有的錯誤訊號都裝在一個低階的訊號中,這個訊號中每個值都是一個error。同樣,變換中也追加了deliverOn:操作,回到主執行緒中去操作。最後把這個冷訊號轉換成熱訊號,但是注意,還沒有connect。


_errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self];
[errorsConnection connect];複製程式碼

假設某個訂閱者在RACCommand中的訊號已經開始執行之後才訂閱的,如果錯誤訊號是一個冷訊號,那麼訂閱之前的錯誤就接收不到了。所以錯誤應該是一個熱訊號,不管什麼時候訂閱都可以接收到所有的錯誤。

error訊號就是熱訊號errorsConnection傳出來的一個熱訊號。error訊號每個值都是在主執行緒上傳送的。

3. executing訊號的初始化

executing這個訊號表示了當前RACCommand是否在執行,訊號裡面的值都是BOOL型別的。那麼如何拿到這樣一個BOOL訊號呢?


RACSignal *immediateExecuting = [RACObserve(self, activeExecutionSignals) map:^(NSArray *activeSignals) {
    return @(activeSignals.count > 0);
}];複製程式碼

由於self.activeExecutionSignals是可以被KVO的,所以每當activeExecutionSignals變化的時候,判斷當前陣列裡面是否還有訊號,如果陣列裡面有值,就代表了當前有在執行中的訊號。



_executing = [[[[[immediateExecuting
                  deliverOn:RACScheduler.mainThreadScheduler]
                  startWith:@NO]
                  distinctUntilChanged]
                  replayLast]
                  setNameWithFormat:@"%@ -executing", self];複製程式碼

immediateExecuting訊號表示當前是否有訊號在執行。初始值為NO,一旦immediateExecuting不為NO的時候就會發出訊號。最後通過replayLast轉換成永遠只儲存最新的一個值的熱訊號。

executing訊號除去第一個預設值NO,其他的每個值也是在主執行緒中傳送的。

4. enabled訊號的初始化


RACSignal *moreExecutionsAllowed = [RACSignal
                                    if:RACObserve(self, allowsConcurrentExecution)
                                    then:[RACSignal return:@YES]
                                    else:[immediateExecuting not]];複製程式碼

先監聽self.allowsConcurrentExecution變數是否有變化,allowsConcurrentExecution預設值為NO。如果有變化,allowsConcurrentExecution為YES,就說明允許併發執行,那麼就返回YES的RACSignal,allowsConcurrentExecution為NO,就說明不允許併發執行,那麼就要看當前是否有正在執行的訊號。immediateExecuting就是代表當前是否有在執行的訊號,對這個訊號取非,就是是否允許執行下一個訊號的BOOL值。這就是moreExecutionsAllowed的訊號。


if (enabledSignal == nil) {
    enabledSignal = [RACSignal return:@YES];
} else {
    enabledSignal = [[[enabledSignal
                       startWith:@YES]
                       takeUntil:self.rac_willDeallocSignal]
                       replayLast];
}複製程式碼

這裡的程式碼就說明了,如果第一個引數傳的是nil,那麼就相當於傳進來了一個[RACSignal return:@YES]訊號。

如果enabledSignal不為nil,就在enabledSignal訊號前面插入一個YES的訊號,目的是為了防止傳入的enabledSignal雖然不為nil,但是裡面是沒有訊號的,比如[RACSignal never],[RACSignal empty],這些訊號傳進來也相當於是沒用的,所以在開頭加一個YES的初始值訊號。

最後同樣通過replayLast操作轉換成只儲存最新的一個值的熱訊號。


_immediateEnabled = [[RACSignal
                      combineLatest:@[ enabledSignal, moreExecutionsAllowed ]]
                      and];複製程式碼

這裡涉及到了combineLatest:的變換操作,這個操作在之前的文章裡面分析過了,這裡不再詳細分析原始碼實現。combineLatest:的作用就是把後面陣列裡面傳入的每個訊號,不管是誰傳送出來一個訊號,都會把陣列裡面所有訊號的最新的值組合到一個RACTuple裡面。immediateEnabled會把每個RACTuple裡面的元素都進行邏輯and運算,這樣immediateEnabled訊號裡面裝的也都是BOOL值了。

immediateEnabled訊號的意義就是每時每刻監聽RACCommand是否可以enabled。它是由2個訊號進行and操作得來的。每當allowsConcurrentExecution變化的時候就會產生一個訊號,此時再加上enabledSignal訊號,就能判斷這一刻RACCommand是否能夠enabled。每當enabledSignal變化的時候也會產生一個訊號,再加上allowsConcurrentExecution是否允許併發,也能判斷這一刻RACCommand是否能夠enabled。所以immediateEnabled是由這兩個訊號combineLatest:之後再進行and操作得來的。



_enabled = [[[[[self.immediateEnabled
                take:1]
                concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]]
                distinctUntilChanged]
                replayLast]
                setNameWithFormat:@"%@ -enabled", self];複製程式碼

由上面原始碼可以知道,self.immediateEnabled是由enabledSignal, moreExecutionsAllowed組合而成的。根據原始碼,enabledSignal的第一個訊號值一定是[RACSignal return:@YES],moreExecutionsAllowed是RACObserve(self, allowsConcurrentExecution)產生的,由於allowsConcurrentExecution預設值是NO,所以moreExecutionsAllowed的第一個值是[immediateExecuting not]。

這裡比較奇怪的地方是為何要用一次concat操作,把第一個訊號值和後面的連線起來。如果直接寫[self.immediateEnabled deliverOn:RACScheduler.mainThreadScheduler],那麼整個self.immediateEnabled就都在主執行緒上了。作者既然沒有這麼寫,肯定是有原因的。

This signal will send its current value upon subscription, and then all future values on the main thread.

通過檢視文件,明白了作者的意圖,作者的目的是為了讓第一個值以後的每個值都傳送在主執行緒上,所以這裡skip:1之後接著deliverOn:RACScheduler.mainThreadScheduler。那第一個值呢?第一個值在一訂閱的時候就傳送出去了,同訂閱者所線上程一致。

distinctUntilChanged保證enabled訊號每次狀態變化的時候只取到一個狀態值。最後呼叫replayLast轉換成只儲存最新值的熱訊號。

從原始碼上看,enabled訊號除去第一個值以外的每個值也都是在主執行緒上傳送的。

三. execute:底層實現分析

ReactiveCocoa 中 RACCommand 底層實現分析


- (RACSignal *)execute:(id)input {
    // 1
    BOOL enabled = [[self.immediateEnabled first] boolValue];
    if (!enabled) {
        NSError *error = [NSError errorWithDomain:RACCommandErrorDomain code:RACCommandErrorNotEnabled userInfo:@{
                          NSLocalizedDescriptionKey: NSLocalizedString(@"The command is disabled and cannot be executed", nil),RACUnderlyingCommandErrorKey: self }];

        return [RACSignal error:error];
    }
    // 2
    RACSignal *signal = self.signalBlock(input);
    NSCAssert(signal != nil, @"nil signal returned from signal block for value: %@", input);
    // 3
    RACMulticastConnection *connection = [[signal subscribeOn:RACScheduler.mainThreadScheduler] multicast:[RACReplaySubject subject]];

    @weakify(self);
    // 4
    [self addActiveExecutionSignal:connection.signal];
    [connection.signal subscribeError:^(NSError *error) {
        @strongify(self);
        // 5
        [self removeActiveExecutionSignal:connection.signal];
    } completed:^{
        @strongify(self);
        // 5
        [self removeActiveExecutionSignal:connection.signal];
    }];

    [connection connect];
     // 6
    return [connection.signal setNameWithFormat:@"%@ -execute: %@", self, [input rac_description]];
}複製程式碼

把上述程式碼分成6步來分析:

  1. self.immediateEnabled為了保證第一個值能正常的傳送給訂閱者,所以這裡用了同步的first的方法,也是可以接受的。呼叫了first方法之後,根據這第一個值來判斷RACCommand是否可以開始執行。如果不能執行就返回一個錯誤訊號。

  2. 這裡就是RACCommand開始執行的地方。self.signalBlock是RACCommand在初始化的時候傳入的一個引數,RACSignal * (^signalBlock)(id input)這個閉包的入參是一個id input,返回值是一個訊號。這裡正好把execute的入參input傳進來。

  3. 把RACCommand執行之後的訊號先呼叫subscribeOn:保證didSubscribe block( )閉包在主執行緒中執行,再轉換成RACMulticastConnection,準備轉換成熱訊號。

  4. 在最終的訊號被訂閱者訂閱之前,我們需要優先更新RACCommand裡面的executing和enabled訊號,所以這裡要先把connection.signal加入到self.activeExecutionSignals陣列裡面。

  5. 訂閱最終結果訊號,出現錯誤或者完成,都要更新self.activeExecutionSignals陣列。

  6. 這裡想說明的是,最終的execute:返回的訊號,和executionSignals是一樣的。

四. RACCommand的一些Category

ReactiveCocoa 中 RACCommand 底層實現分析

RACCommand在日常iOS開發過程中,很適合上下拉重新整理,按鈕點選等操作,所以ReactiveCocoa就幫我們在這些UI控制元件上封裝了一個RACCommand屬性——rac_command。

1. UIBarButtonItem+RACCommandSupport

一旦UIBarButtonItem被點選,RACCommand就會執行。


- (RACCommand *)rac_command {
    return objc_getAssociatedObject(self, UIControlRACCommandKey);
}

- (void)setRac_command:(RACCommand *)command {
    objc_setAssociatedObject(self, UIControlRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 檢查已經儲存過的訊號,移除老的,新增一個新的
    RACDisposable *disposable = objc_getAssociatedObject(self, UIControlEnabledDisposableKey);
    [disposable dispose];

    if (command == nil) return;

    disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self];
    objc_setAssociatedObject(self, UIControlEnabledDisposableKey, disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    [self rac_hijackActionAndTargetIfNeeded];
}複製程式碼

給UIBarButtonItem新增rac_command屬性用到了runtime裡面的AssociatedObject關聯物件。這裡給UIBarButtonItem類新增了2個關聯物件,key分別是UIControlRACCommandKey,UIControlEnabledDisposableKey。UIControlRACCommandKey對應的是繫結的command,UIControlEnabledDisposableKey對應的是command.enabled的disposable訊號。

set方法裡面最後會呼叫rac_hijackActionAndTargetIfNeeded,這個方法需要特別注意:


- (void)rac_hijackActionAndTargetIfNeeded {
    SEL hijackSelector = @selector(rac_commandPerformAction:);
    if (self.target == self && self.action == hijackSelector) return;

    if (self.target != nil) NSLog(@"WARNING: UIBarButtonItem.rac_command hijacks the control's existing target and action.");

        self.target = self;
        self.action = hijackSelector;
}

- (void)rac_commandPerformAction:(id)sender {
    [self.rac_command execute:sender];
}複製程式碼

rac_hijackActionAndTargetIfNeeded方法是對當前UIBarButtonItem的target和action進行檢查。

如果當前UIBarButtonItem的target = self,並且action = @selector(rac_commandPerformAction:),那麼就算檢查通過符合執行RACCommand的前提條件了,直接return。

如果上述條件不符合,就強制改變UIBarButtonItem的target = self,並且action = @selector(rac_commandPerformAction:),所以這裡需要注意的就是,UIBarButtonItem呼叫rac_command,會被強制改變它的target和action。

2. UIButton+RACCommandSupport

一旦UIButton被點選,RACCommand就會執行。


- (RACCommand *)rac_command {
    return objc_getAssociatedObject(self, UIButtonRACCommandKey);
}

- (void)setRac_command:(RACCommand *)command {
    objc_setAssociatedObject(self, UIButtonRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    RACDisposable *disposable = objc_getAssociatedObject(self, UIButtonEnabledDisposableKey);
    [disposable dispose];

    if (command == nil) return;

    disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self];
    objc_setAssociatedObject(self, UIButtonEnabledDisposableKey, disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    [self rac_hijackActionAndTargetIfNeeded];
}複製程式碼

這裡給UIButton新增繫結2個屬性同樣也用到了runtime裡面的AssociatedObject關聯物件。程式碼和UIBarButtonItem的實現基本一樣。同樣是給UIButton類新增了2個關聯物件,key分別是UIButtonRACCommandKey,UIButtonEnabledDisposableKey。UIButtonRACCommandKey對應的是繫結的command,UIButtonEnabledDisposableKey對應的是command.enabled的disposable訊號。



- (void)rac_hijackActionAndTargetIfNeeded {
    SEL hijackSelector = @selector(rac_commandPerformAction:);

    for (NSString *selector in [self actionsForTarget:self forControlEvent:UIControlEventTouchUpInside]) {
        if (hijackSelector == NSSelectorFromString(selector)) {
            return;
        }
    }

    [self addTarget:self action:hijackSelector forControlEvents:UIControlEventTouchUpInside];
}

- (void)rac_commandPerformAction:(id)sender {
    [self.rac_command execute:sender];
}複製程式碼

rac_hijackActionAndTargetIfNeeded函式的意思和之前的一樣,也是檢查UIButton的target和action。最終結果的UIButton的target = self,action = @selector(rac_commandPerformAction:)

3. UIRefreshControl+RACCommandSupport


- (RACCommand *)rac_command {
    return objc_getAssociatedObject(self, UIRefreshControlRACCommandKey);
}

- (void)setRac_command:(RACCommand *)command {
    objc_setAssociatedObject(self, UIRefreshControlRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    [objc_getAssociatedObject(self, UIRefreshControlDisposableKey) dispose];

    if (command == nil) return;

    RACDisposable *enabledDisposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self];

    RACDisposable *executionDisposable = [[[[self
                                             rac_signalForControlEvents:UIControlEventValueChanged]
                                             map:^(UIRefreshControl *x) {
                                                return [[[command
                                                          execute:x]
                                                          catchTo:[RACSignal empty]]
                                                          then:^{
                                                            return [RACSignal return:x];
                                                        }];
                                            }]
                                            concat]
                                            subscribeNext:^(UIRefreshControl *x) {
                                              [x endRefreshing];
                                            }];

    RACDisposable *commandDisposable = [RACCompoundDisposable compoundDisposableWithDisposables:@[ enabledDisposable, executionDisposable ]];
    objc_setAssociatedObject(self, UIRefreshControlDisposableKey, commandDisposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}複製程式碼

這裡給UIRefreshControl新增繫結2個屬性同樣也用到了runtime裡面的AssociatedObject關聯物件。程式碼和UIBarButtonItem的實現基本一樣。同樣是給UIButton類新增了2個關聯物件,key分別是UIRefreshControlRACCommandKey,UIRefreshControlDisposableKey。UIRefreshControlRACCommandKey對應的是繫結的command,UIRefreshControlDisposableKey對應的是command.enabled的disposable訊號。

這裡多了一個executionDisposable訊號,這個訊號是用來結束重新整理操作的。


[[[command execute:x] catchTo:[RACSignal empty]] then:^{ return [RACSignal return:x]; }];複製程式碼

這個訊號變換先把RACCommand執行,執行之後得到的結果訊號剔除掉所有的錯誤。then操作就是忽略掉所有值,在最後新增一個返回UIRefreshControl物件的訊號。

[self rac_signalForControlEvents:UIControlEventValueChanged]之後再map升階為高階訊號,所以最後用concat降階。最後訂閱這個訊號,訂閱只會收到一個值,command執行完畢之後的訊號傳送完所有的值的時候,即收到這個值的時刻就是最終重新整理結束的時刻。

所以最終的disposable訊號還要加上executionDisposable。

最後

關於RACCommand底層實現分析都已經分析完成。最後請大家多多指教。

相關文章