0%

ReactiveCocoa-RACCommand原理分析

在项目中,经常会把用户操作动作和业务操作进行绑定,比如点击登录按钮会进行登录网络请求、点击下载图片等等。ReactiveCocoa 中提供捆绑副作用和信号的 RACCommand,开发者可以利用 RACCommand 来实现类似这种 动作-响应 绑定的功能。

RACCommand 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// RACCommand.h
@interface RACCommand<__contravariant InputType, __covariant ValueType> : NSObject

@property (nonatomic, strong, readonly) RACSignal<RACSignal<ValueType> *> *executionSignals;

@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *executing;

@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *enabled;

@property (nonatomic, strong, readonly) RACSignal<NSError *> *errors;

@property (atomic, assign) BOOL allowsConcurrentExecution;

- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

- (instancetype)initWithEnabled:(nullable RACSignal<NSNumber *> *)enabledSignal signalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

- (RACSignal<ValueType> *)execute:(nullable InputType)input;

@end

头文件中,RACCommand 对外暴露了5个属性,作用分别是:

  1. executionSignals:高阶信号,当执行 -execute: 方法的时候,会创建新的信号,这个信号通过 executionSignals 发送出去
  2. executing:发送的是布尔值,标志着 RACCommand 是否正在执行中,信号值是布尔值
  3. enabled:标志 RACCommand 是否可以执行 -execute:
  4. errors:执行 -execute: 方法,过程中出现的 error 都由此发送
  5. allowsConcurrentExecution:是否允许 RACCommand 并发执行,atomic 修饰,getter/setter方法是原子性的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// RACCommand.m
@interface RACCommand () {
// Atomic backing variable for `allowsConcurrentExecution`.
volatile uint32_t _allowsConcurrentExecution;
}

/// A subject that sends added execution signals.
@property (nonatomic, strong, readonly) RACSubject *addedExecutionSignalsSubject;

/// A subject that sends the new value of `allowsConcurrentExecution` whenever it changes.
@property (nonatomic, strong, readonly) RACSubject *allowsConcurrentExecutionSubject;

// `enabled`, but without a hop to the main thread.
//
// Values from this signal may arrive on any thread.
@property (nonatomic, strong, readonly) RACSignal *immediateEnabled;

// The signal block that the receiver was initialized with.
@property (nonatomic, copy, readonly) RACSignal * (^signalBlock)(id input);

@end

RACCommand 实现文件中定义了4个属性以及1个实例变量:

  1. _allowsConcurrentExecution:头文件属性 allowsConcurrentExecution 对应的实例变量,用 volatile 修饰,结合头文件属性的 atomic 关键子保证读写的原子性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    - (BOOL)allowsConcurrentExecution {
    return _allowsConcurrentExecution != 0;
    }

    - (void)setAllowsConcurrentExecution:(BOOL)allowed {
    if (allowed) {
    OSAtomicOr32Barrier(1, &_allowsConcurrentExecution);
    } else {
    OSAtomicAnd32Barrier(0, &_allowsConcurrentExecution);
    }

    [self.allowsConcurrentExecutionSubject sendNext:@(_allowsConcurrentExecution)];
    }

    在 setter 方法中,当 allowed == YES,采用 OSAtomicOr32Barrier 对 _allowsConcurrentExecution 和 1 做或运算;否则就执行 OSAtomicAnd32Barrier 和 0进行与运算;OSAtomicOr32Barrier/OSAtomicAnd32Barrier 都是原子运算

  2. allowsConcurrentExecutionSubject:当 _allowsConcurrentExecution 被修改的时候会通过该信号给订阅发送修改后的 _allowsConcurrentExecution 值

  3. addedExecutionSignalsSubject:执行 -execute: 方法的时候会创建一个新的 RACSignal 信号并将其通过 addedExecutionSignalsSubject 发送出去

  4. immediateEnabled:判断 -execute: 方法创建的 RACSignal 是否能被订阅,immediateEnabled 会任意的线程给订阅者发送信号值

  5. RACSignal * (^signalBlock)(id input):执行 -execute: 方法的时候,会根据 signalBlock 创建RACSignal

RACCommand 初始化

RACCommand 提供了2个初始化方法:

  1. - (instancetype)initWithSignalBlock:(RACSignal<id> * (^)(id input))signalBlock
  2. - (instancetype)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal<id> * (^)(id input))signalBlock

第一个初始化方法是基于第二个进行封装,所以这里直接分析第二个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
- (instancetype)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal<id> * (^)(id input))signalBlock {
NSCParameterAssert(signalBlock != nil);

self = [super init];

_addedExecutionSignalsSubject = [RACSubject new];
_allowsConcurrentExecutionSubject = [RACSubject new];
_signalBlock = [signalBlock copy];

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

// `errors` needs to be multicasted so that it picks up all
// `activeExecutionSignals` that are added.
//
// In other words, if someone subscribes to `errors` _after_ an execution
// has started, it should still receive any error from that execution.
RACMulticastConnection *errorsConnection = [[[self.addedExecutionSignalsSubject
flattenMap:^(RACSignal *signal) {
return [[signal
ignoreValues]
catch:^(NSError *error) {
return [RACSignal return:error];
}];
}]
deliverOn:RACScheduler.mainThreadScheduler]
publish];

_errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self];
[errorsConnection connect];

RACSignal *immediateExecuting = [[[[self.addedExecutionSignalsSubject
flattenMap:^(RACSignal *signal) {
return [[[signal
catchTo:[RACSignal empty]]
then:^{
return [RACSignal return:@-1];
}]
startWith:@1];
}]
scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) {
return @(running.integerValue + next.integerValue);
}]
map:^(NSNumber *count) {
return @(count.integerValue > 0);
}]
startWith:@NO];

_executing = [[[[[immediateExecuting
deliverOn:RACScheduler.mainThreadScheduler]
// This is useful before the first value arrives on the main thread.
startWith:@NO]
distinctUntilChanged]
replayLast]
setNameWithFormat:@"%@ -executing", self];

RACSignal *moreExecutionsAllowed = [RACSignal
if:[self.allowsConcurrentExecutionSubject startWith:@NO]
then:[RACSignal return:@YES]
else:[immediateExecuting not]];

if (enabledSignal == nil) {
enabledSignal = [RACSignal return:@YES];
} else {
enabledSignal = [enabledSignal startWith:@YES];
}

_immediateEnabled = [[[[RACSignal
combineLatest:@[ enabledSignal, moreExecutionsAllowed ]]
and]
takeUntil:self.rac_willDeallocSignal]
replayLast];

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

return self;
}

在初始化方法中对定义的属性进行初始化

executionSignals

executionSignals 是通过对 addedExecutionSignalsSubject 信号值进行 map 操作,这里如果捕捉到 addedExecutionSignalsSubject 发送了 error 就转换成 RACEmptySignal 返回。executionSignals 所有信号都在主线程发送。

executionSignals 是高阶信号,当 _allowsConcurrentExecution == NO,可以通过 -switchToLatest 来进行降阶操作;反之需要用 -flatten 方法降阶

errors

errors 信号也是对 addedExecutionSignalsSubject 进行转换后的新信号。

  1. 对 addedExecutionSignalsSubject 进行 flattenMap 将 addedExecutionSignalsSubject 发送的信号进行 ignoreValues 处理,也就是忽略常规的信号
  2. 后面通过 -catch: 捕获 signal 发送的 NSError,包装成RACSignal
  3. 将所有信号都放在主线程发送给订阅者

概括来说,errors 信号主要作用是把 RACCommand 执行过程中产出的 NSError 发送给订阅者

immediateExecuting

  1. 对 addedExecutionSignalsSubject 进行 -flatten: 操作,对其发送的信号进行 -catchTo: 操作,捕获到 error 会转化成 RACEmptySignal。
  2. 通过 -then: 方法将所有信号值再转化成 -1 发送出去。
  3. 步骤2发送序列之前在头部插入 1
  4. 对步骤3的序列进行迭代相加
  5. 判断步骤四的序列值是否大于0,返回 BOOL 值信号序列,并且头部插入 NO

大概过程如图所示(_allowsConcurrentExecution == NO):

image-20190323202025627

executing

该信号是通过封装 immediateExecuting 信号并将其在主线程发送,首先会在信号序列里插入 NO,然后当 immediateExecuting 信号值发生变化的时候会给订阅者发送信号值,并且每次被订阅都可以收到最新的一次信号值。

executing 表示 RACCommand 当前是否有正在执行的任务

moreExecutionsAllowed

如果收到 allowsConcurrentExecutionSubject 信号值且为 YES,则会执行 -then: 给订阅者发送 YES,反之给订阅发送 immediateExecuting 最新信号值的取反结果

moreExecutionsAllowed 表示当前 RACCommand 是否允许并发执行多个任务

immediateEnabled

immediateEnabled 发送的信号值是通过将参数 enabledSignal 和 moreExecutionsAllowed 这个2个信号的最新信号值做与运算后得到的结果

image-20190323202159063

enabled

enabled 是通过封装 immediateEnabled 获取,与后者的主要区别是,enabled 会将除了第一个信号外的信号放在主线程发送。

RACCommand 执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (RACSignal *)execute:(id)input {
// `immediateEnabled` is guaranteed to send a value upon subscription, so
// -first is acceptable here.
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];
}

RACSignal *signal = self.signalBlock(input);
NSCAssert(signal != nil, @"nil signal returned from signal block for value: %@", input);

// We subscribe to the signal on the main thread so that it occurs _after_
// -addActiveExecutionSignal: completes below.
//
// This means that `executing` and `enabled` will send updated values before
// the signal actually starts performing work.
RACMulticastConnection *connection = [[signal
subscribeOn:RACScheduler.mainThreadScheduler]
multicast:[RACReplaySubject subject]];

[self.addedExecutionSignalsSubject sendNext:connection.signal];

[connection connect];
return [connection.signal setNameWithFormat:@"%@ -execute: %@", self, RACDescription(input)];
}
  1. 同步获取 immediateEnabled 信号的第一个值,判断是否当前是否可以执行任务,不能则返回 RACErrorSignal

  2. 通过初始化保存的 signalBlock 闭包创建 RACSignal 类型对象 signal,断言判断 signal 是否为空

  3. signal 在主线程订阅并将其转化成热信号 (connection.signal)

  4. connection.signal 被订阅之前通过 addedExecutionSignalsSubject 发送,更新 executing 和 enabled 信号

  5. 执行 [connection connect],signal 被订阅,最后返回 connection.signal

备注: -execute: 方法和 executionSignals 信号都是返回最终信号 connection.signal;但是 executionSignals 是热信号,它最终信号被订阅之前发送,所以要通过 executionSignals 获取当前执行的最终信号,需要在 -execute: 执行之前进行订阅。

RACCommand 对于系统类的扩展

UIButton+RACCommandSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (RACCommand *)rac_command {
return objc_getAssociatedObject(self, UIButtonRACCommandKey);
}

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

// Check for stored signal in order to remove it and add a new one
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];
}

- (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];
}
  1. -setRac_command: 通过对象关联把 RACCommand 和 RACDisposable 绑定到 UIButton 中,绑定新 RACDisposable 前会将旧的先进行 dispose,如果 command 为空直接返回
  2. 对 command 的 enabled 信号调用 -setKeyPath:onObject: 方法,把 UIButton 的 enable 属性和 enabled 信号进行绑定
  3. 执行 rac_hijackActionAndTargetIfNeeded ,检查是否有对应的 action 为 rac_commandPerformAction,若没有则设置 target 和 action 为 @selector(rac_commandPerformAction) 的点击事件。
  4. 按钮点击会触发 rac_command
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
- (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object nilValue:(id)nilValue {
NSCParameterAssert(keyPath != nil);
NSCParameterAssert(object != nil);

keyPath = [keyPath copy];

RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];

// Purposely not retaining 'object', since we want to tear down the binding
// when it deallocates normally.
__block void * volatile objectPtr = (__bridge void *)object;

RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) {
// 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.
__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);

// Log the error if we're running with assertions disabled.
NSLog(@"Received error from %@ in binding for key path \"%@\" on %@: %@", self, keyPath, object, error);

[disposable dispose];
} completed:^{
[disposable dispose];
}];

[disposable addDisposable:subscriptionDisposable];

RACDisposable *clearPointerDisposable = [RACDisposable disposableWithBlock:^{
while (YES) {
void *ptr = objectPtr;
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];
}];
}

-setKeyPath:onObject:nilValue: 主要作用是将 object 对应的 keypath 值和信号绑定到一起,主要有以下几个流程

  1. 先订阅原信号,当收到原信号 sendNext 的时候,参数 object 根据 keypath 通过 KVO 赋值成对应的信号值。

  2. 当 object 被销毁的时候,会将 objectPtr 赋值为 NULL,取消步骤 1 的订阅

  3. 为了订阅的过程避免 object 被提前销毁,作者使用了 objc_precise_lifetime 修饰关键字来精确地控制 object 生命周期。注释中作者描述这可能是编译器的 bug:

    即使显示使用 __strong 修饰 object,在这里也不会对 object 进行一次 retain,相反实际效果和 unsafe_unretain 相似。所以这里使用 objc_precise_lifetime 明确告诉编译器 object 不会在 didSubcribe 闭包中销毁

UIRefreshControl+RACCommandSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (RACCommand *)rac_command {
return objc_getAssociatedObject(self, UIRefreshControlRACCommandKey);
}

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

// Dispose of any active command associations.
[objc_getAssociatedObject(self, UIRefreshControlDisposableKey) dispose];

if (command == nil) return;

// Like RAC(self, enabled) = command.enabled; but with access to disposable.
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];
}];
}] // mapSignal
concat]
deliverOnMainThread]
subscribeNext:^(UIRefreshControl *x) {
[x endRefreshing];
}];

RACDisposable *commandDisposable = [RACCompoundDisposable compoundDisposableWithDisposables:@[ enabledDisposable, executionDisposable ]];
objc_setAssociatedObject(self, UIRefreshControlDisposableKey, commandDisposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  1. 同样也是通过关联属性的方法把 command 和 RACDisposable 关联到 UIRefreshControl 中,UIRefreshControl 的 enable 和 command.enable 绑定到一起
  2. 订阅 UIRefreshControl 控件 UIControlEventValueChanged 事件信号,并对其事件信号进行 map 处理(返回的暂且称为mapSignal),在 block 中 将信号值 x 也就是 UIRefreshControl 对象自己,作为参数执行 command的 -execute: 方法,方法返回的 RACSignal 执行 catchTo: 忽略其中的 error,然后进行 then 来将 UIRefreshControl 包装成RACSignal 返回。
  3. mapSignal 利用 concat 操作进行降阶,并把最终信号值放在主线程中发送。订阅最终信号,收到 sendNext 事件 UIRefreshControl 结束刷新

参考文章:

ReactiveCocoa 中 RACCommand 底层实现分析

优雅的 RACCommand