ReactiveCocoa 中 RACCommand底層實現分析
前言
在ReactiveCocoa 過程中,除去RACSignal和RACSubject這些信號類以外,有些時候我們可能還需要封裝一些固定的操作集合。這些操作集合都是固定的,每次只要一觸發就會執行事先定義好的一個過程。在iOS開發過程中,按鈕的點擊事件就可能有這種需求。那么RACCommand就可以實現這種需求。
當然除了封裝一個操作集合以外,RACCommand還能集中處理錯誤等等功能。今天就來從底層來看看RACCommand是如何實現的。
目錄
- 1.RACCommand的定義
- 2.initWithEnabled: signalBlock: 底層實現分析
- 3.execute:底層實現分析
- 4.RACCommand的一些Category
一. 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
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的變化引起的一些變化畫了一張數據流圖:
除去沒有影響到enabled信號,activeExecutionSignals的變化會影響到其他三個信號。
7. RACSignal *immediateEnabled
這個信號也是一個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: 底層實現分析
首先先來看看比較短的那個初始化方法。
- (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:底層實現分析
- (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步來分析:
-
self.immediateEnabled為了保證第一個值能正常的發送給訂閱者,所以這里用了同步的first的方法,也是可以接受的。調用了first方法之后,根據這第一個值來判斷RACCommand是否可以開始執行。如果不能執行就返回一個錯誤信號。
-
這里就是RACCommand開始執行的地方。self.signalBlock是RACCommand在初始化的時候傳入的一個參數,RACSignal * (^signalBlock)(id input)這個閉包的入參是一個id input,返回值是一個信號。這里正好把execute的入參input傳進來。
-
把RACCommand執行之后的信號先調用subscribeOn:保證didSubscribe block( )閉包在主線程中執行,再轉換成RACMulticastConnection,準備轉換成熱信號。
-
在最終的信號被訂閱者訂閱之前,我們需要優先更新RACCommand里面的executing和enabled信號,所以這里要先把connection.signal加入到self.activeExecutionSignals數組里面。
-
訂閱最終結果信號,出現錯誤或者完成,都要更新self.activeExecutionSignals數組。
-
這里想說明的是,最終的execute:返回的信號,和executionSignals是一樣的。
四. RACCommand的一些Category
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底層實現分析都已經分析完成。最后請大家多多指教。
來自:https://halfrost.com/reactivecocoa_raccommand/