[关闭]
@myron-lee 2017-03-22T12:34:39.000000Z 字数 10351 阅读 2103

改进RACCommand──一句话完成工作信号的创建、Next/Error事件的订阅,并保证互斥执行

iOS Blog


1.前言

本文将会介绍RACCommand进行异步操作(比如网络请求)的用法,分析其中存在的问题。然后介绍改进方案STButtonSignal的用法,给出STButtonSignal的原理以及具体实现。

同时推荐阅读我的另一篇文章——RAC扩展──异步filter、map

2.RACCommand的用法

假设我们有个下单按钮,点击之后提交新订单到服务器,一般我们会这么写。

  1. self.submitBtn.rac_command =
  2. [[RACCommand alloc] initWithEnabled:RACObserve(self.viewModel, agreeProtocol)
  3. signalBlock:^RACSignal *(id input) {
  4. return someWebRequestSignal;
  5. }];
  6. [[self.submitBtn.rac_command.executionSignals flatten] subscribeNext:^(id value) {
  7. //Handle data from server
  8. }];
  9. [self.submitBtn.rac_command.errors subscribeNext:^(NSError *error) {
  10. //Handle error
  11. }];

这样使用存在如下问题:
1. 网络请求信号的创建和订阅过程是分离的,代码逻辑不够集中。
2. 网络请求的Next事件、Error事件是分离的,为了处理它们,我需要订阅两个信号。
3. Next事件信号executionSignals是signal of signals,想要订阅网络请求返回数据要先进行flatten操作,这个操作是重复的,产生了门板代码。

3. RACCommand为什么需要这么用

1.为什么工作信号的创建和订阅要分离?
因为,在RACCommand实现中subscriber并不直接订阅工作信号。按钮是可以重复点击的,每次点击可以根据输入(input)创建不同的工作信号,RACCommand会在“创建”与“订阅”中间插入一些对工作信号的变换处理操作,实现控制并发、持久订阅等功能,详细可以参考源码。

2.为什么工作信号的Next事件和Error事件要分开从executionSignals、errors两个信号发出,我直接订阅一个Signal不是更简单吗?
因为订阅关系会在Erro发出的时候自动解除。关键源码如下(RACSubscriber.m):

  1. - (void)sendError:(NSError *)e {
  2. @synchronized (self) {
  3. void (^errorBlock)(NSError *) = [self.error copy];
  4. [self.disposable dispose];
  5. if (errorBlock == nil) return;
  6. errorBlock(e);
  7. }
  8. }

假如让Next事件和Error事件从同一个信号发出,如果我点击按钮,进行网络请求,出错了,订阅关系自动解除。那么我再次点击按钮,响应处理代码就不会被执行。另外我们注意到errors信号内发送的也是封装过的error事件,所以可以持续接收Error事件。

4.RACommand的性能问题

首先我们知道,我们拿到的executionSignals是Signal of signals,读源码我们可以发现这个executionSignals是一个signal通过concat得到的。关键源码(RACCommand.m):

  1. RACSignal *newActiveExecutionSignals = [[[[[self
  2. rac_valuesAndChangesForKeyPath:@keypath(self.activeExecutionSignals) options:NSKeyValueObservingOptionNew observer:nil]
  3. reduceEach:^(id _, NSDictionary *change) {
  4. NSArray *signals = change[NSKeyValueChangeNewKey];
  5. if (signals == nil) return [RACSignal empty];
  6. return [signals.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
  7. }]
  8. concat]
  9. publish]
  10. autoconnect];
  11. _executionSignals = [[[newActiveExecutionSignals
  12. map:^(RACSignal *signal) {
  13. return [signal catchTo:[RACSignal empty]];
  14. }]
  15. deliverOn:RACScheduler.mainThreadScheduler]
  16. setNameWithFormat:@"%@ -executionSignals", self];

那这个signal就是signal of (signal of signal)s,一个三维的signal,好复杂。分析一下,这个signal是由self.activeExecutionSignals产生的,而self.activeExecutionSignals是一个signal数组,保存的是正在执行的工作信号。self.activeExecutionSignals之所以是一个数组,而不是单个signal,是为了支持并发执行工作信号。
也就是说,RACCommand为了支持并发,内部采用了一种比较复杂的实现。而我们绝大多数的应用场景中,按钮的处理逻辑是互斥的,我们完全可以采用另外一种比较简单的实现,不去支持并发,获取更高的性能。
针对以上所述的RACCommand的用法复杂和性能的问题,我进行改进,下面介绍一下我改进的STButtonSignal的用法和实现原理。

5. STButtonSignal的用法

用法示例:

  1. [[STButtonSignal associateButton:self.submitButton
  2. withSignalBlock:^RACSignal *(id input) {
  3. return someWebRequestSignal;
  4. }]
  5. subscribeNext:^(id x) {
  6. //Handle data from server
  7. } error:^(NSError *error) {
  8. //Handle error
  9. }];

注意:someWebRequestSignal网络请求返回之后一定要发出Complete事件,如果你对someWebRequestSignal进行了flattenMap
,flattenMap出来的Signal也一定要发出Complete事件,我依赖complete事件将Button恢复为enable状态,如果不发送compete事件,Button将一直处于disable状态。

这段代码看着问题很多。比如:
1. 你只订阅了一个Signal,可是我的按钮是可以重复点击的,新创建workSignal没有被订阅,这怎么行?
2. 我一个workSignal sendComplete或者sendError了,你的订阅关系不就结束了,这个按钮不就没法点击了吗?
别着急,我内部做了处理,这些问题都没问题。

6. STButtonSignal主要解决的问题以及解决办法

1.并发问题(比如,快速点击按钮导致的重复提交订单问题)
按照如下流程控制按钮状态,按钮被点击->disable按钮->创建workSignal->workSignal complete->enable按钮。

2.我们知道Button可重复点击(在上一次响应处理结束后),每次点击都将创建一个Signal,我们要让一个subscriber订阅这些Signal。
借鉴Multicast的实现原理,借用一个RACSubject中中间做消息转发,调用方的subscriber实际将会订阅这个RACSubject,这个RACSubject将会订阅workSignal。如下图所示:
这里写图片描述
3.subscriber可以持续接收error、complete事件,并不会因为接收到error、complete事件而终止订阅。
自定义一个STManualDisposeSubscriber,它和RACSubscriber唯一的不同是不会在接收到error、complete事件时自动dispose。然后,STButtonSignal复写了RACSignal中订阅方法,获取调用方的subscriber,在内部转化成为一个我自定义的STManualDisposeSubscriber。自定义subscriber不会因为接收到error、complete事件而自动dispose掉自己,所以一次订阅每次按钮点击处理的结果(无论成功还是失败)都能接收到。

7. 完整源码

  1. #import "STButtonSignal.h"
  2. #import "STManualDisposeSubscriber.h"
  3. static void *UIButtonSignalKey = &UIButtonSignalKey;
  4. @interface STButtonSignal ()
  5. @property (nonatomic, strong, readonly) RACSignal * (^signalBlock)(id input);
  6. @property (nonatomic, strong, readonly) UIButton *button;
  7. @property (nonatomic, strong) RACDisposable *activeSignalDisposable;
  8. @property (nonatomic, strong) RACSubject *transitSubject;
  9. @end
  10. @implementation STButtonSignal
  11. + (STButtonSignal *)associateButton:(UIButton *)button withSignalBlock:(RACSignal * (^)(id input))signalBlock{
  12. STButtonSignal *signal = [[STButtonSignal alloc] initWithSignalBlock:(RACSignal * (^)(id input))signalBlock button:(UIButton *)button];
  13. return signal;
  14. }
  15. + (STButtonSignal *)createSignalWithSignalBlock:(RACSignal * (^)(id input))signalBlock button:(UIButton *)button {
  16. STButtonSignal *signal = [[STButtonSignal alloc] initWithSignalBlock:(RACSignal * (^)(id input))signalBlock button:(UIButton *)button];
  17. return signal;
  18. }
  19. - (instancetype)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock button:(UIButton *)button{
  20. self = [super init];
  21. if (self) {
  22. _signalBlock = signalBlock;
  23. _button = button;
  24. _transitSubject = [[RACSubject alloc] init];
  25. [self rac_hijackActionAndTargetIfNeeded];
  26. objc_setAssociatedObject(button, UIButtonSignalKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  27. }
  28. return self;
  29. }
  30. - (void)rac_hijackActionAndTargetIfNeeded {
  31. SEL hijackSelector = @selector(rac_commandPerformAction:);
  32. for (NSString *selector in [self.button actionsForTarget:self forControlEvent:UIControlEventTouchUpInside]) {
  33. if (hijackSelector == NSSelectorFromString(selector)) {
  34. return;
  35. }
  36. }
  37. [self.button addTarget:self action:hijackSelector forControlEvents:UIControlEventTouchUpInside];
  38. }
  39. - (void)rac_commandPerformAction:(id)sender {
  40. // [self.rac_command execute:sender];
  41. NSAssert(self.activeSignalDisposable == nil || [self.activeSignalDisposable isDisposed], @"Don't allow concurrent execution, activeSignal should complete and be set nil when you can click the button again");
  42. RACSignal *newWorkSignal = self.signalBlock(sender);
  43. self.activeSignalDisposable = [[[newWorkSignal initially:^{
  44. [self.button setEnabled:NO];
  45. }] finally:^{
  46. [self.button setEnabled:YES];
  47. }] subscribeNext:^(id x) {
  48. [self.transitSubject sendNext:x];
  49. } error:^(NSError *error) {
  50. [self.transitSubject sendError:error];
  51. } completed:^{
  52. [self.transitSubject sendCompleted];
  53. }];
  54. }
  55. #pragma mark RACSubscriber
  56. //Override subscribe method, use STManualDisposeSubscriber to replace RACSubscriber
  57. - (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
  58. NSCAssert(NO, @"This method is not implemented yet");
  59. return nil;
  60. }
  61. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
  62. NSCParameterAssert(nextBlock != NULL);
  63. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
  64. return [self.transitSubject subscribe:o];
  65. }
  66. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock {
  67. NSCParameterAssert(nextBlock != NULL);
  68. NSCParameterAssert(completedBlock != NULL);
  69. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:nextBlock error:NULL completed:completedBlock];
  70. return [self.transitSubject subscribe:o];
  71. }
  72. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock {
  73. NSCParameterAssert(nextBlock != NULL);
  74. NSCParameterAssert(errorBlock != NULL);
  75. NSCParameterAssert(completedBlock != NULL);
  76. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];
  77. return [self.transitSubject subscribe:o];
  78. }
  79. - (RACDisposable *)subscribeError:(void (^)(NSError *error))errorBlock {
  80. NSCParameterAssert(errorBlock != NULL);
  81. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:NULL error:errorBlock completed:NULL];
  82. return [self.transitSubject subscribe:o];
  83. }
  84. - (RACDisposable *)subscribeCompleted:(void (^)(void))completedBlock {
  85. NSCParameterAssert(completedBlock != NULL);
  86. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:NULL error:NULL completed:completedBlock];
  87. return [self.transitSubject subscribe:o];
  88. }
  89. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock {
  90. NSCParameterAssert(nextBlock != NULL);
  91. NSCParameterAssert(errorBlock != NULL);
  92. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:nextBlock error:errorBlock completed:NULL];
  93. return [self.transitSubject subscribe:o];
  94. }
  95. - (RACDisposable *)subscribeError:(void (^)(NSError *))errorBlock completed:(void (^)(void))completedBlock {
  96. NSCParameterAssert(completedBlock != NULL);
  97. NSCParameterAssert(errorBlock != NULL);
  98. STManualDisposeSubscriber *o = [STManualDisposeSubscriber subscriberWithNext:NULL error:errorBlock completed:completedBlock];
  99. return [self.transitSubject subscribe:o];
  100. }
  101. @end
  1. #import "STManualDisposeSubscriber.h"
  2. #import "RACSubscriber.h"
  3. #import "RACEXTScope.h"
  4. #import "RACCompoundDisposable.h"
  5. @interface STManualDisposeSubscriber ()
  6. // These callbacks should only be accessed while synchronized on self.
  7. @property (nonatomic, copy) void (^next)(id value);
  8. @property (nonatomic, copy) void (^error)(NSError *error);
  9. @property (nonatomic, copy) void (^completed)(void);
  10. @property (nonatomic, strong, readonly) RACCompoundDisposable *disposable;
  11. @end
  12. @implementation STManualDisposeSubscriber
  13. #pragma mark Lifecycle
  14. + (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
  15. STManualDisposeSubscriber *subscriber = [[STManualDisposeSubscriber alloc] init];
  16. subscriber->_next = [next copy];
  17. subscriber->_error = [error copy];
  18. subscriber->_completed = [completed copy];
  19. return subscriber;
  20. }
  21. - (id)init {
  22. self = [super init];
  23. if (self == nil) return nil;
  24. @unsafeify(self);
  25. RACDisposable *selfDisposable = [RACDisposable disposableWithBlock:^{
  26. @strongify(self);
  27. @synchronized (self) {
  28. self.next = nil;
  29. self.error = nil;
  30. self.completed = nil;
  31. }
  32. }];
  33. _disposable = [RACCompoundDisposable compoundDisposable];
  34. [_disposable addDisposable:selfDisposable];
  35. return self;
  36. }
  37. - (void)dealloc {
  38. [self.disposable dispose];
  39. }
  40. #pragma mark RACSubscriber
  41. - (void)sendNext:(id)value {
  42. @synchronized (self) {
  43. void (^nextBlock)(id) = [self.next copy];
  44. if (nextBlock == nil) return;
  45. nextBlock(value);
  46. }
  47. }
  48. - (void)sendError:(NSError *)e {
  49. @synchronized (self) {
  50. void (^errorBlock)(NSError *) = [self.error copy];
  51. // [self.disposable dispose];
  52. if (errorBlock == nil) return;
  53. errorBlock(e);
  54. }
  55. }
  56. - (void)sendCompleted {
  57. @synchronized (self) {
  58. void (^completedBlock)(void) = [self.completed copy];
  59. // [self.disposable dispose];
  60. if (completedBlock == nil) return;
  61. completedBlock();
  62. }
  63. }
  64. - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)otherDisposable {
  65. if (otherDisposable.disposed) return;
  66. RACCompoundDisposable *selfDisposable = self.disposable;
  67. [selfDisposable addDisposable:otherDisposable];
  68. @unsafeify(otherDisposable);
  69. // If this subscription terminates, purge its disposable to avoid unbounded
  70. // memory growth.
  71. [otherDisposable addDisposable:[RACDisposable disposableWithBlock:^{
  72. @strongify(otherDisposable);
  73. [selfDisposable removeDisposable:otherDisposable];
  74. }]];
  75. }
  76. @end
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注