临别前给小伙伴的RAC培训

开篇扯谈——响应式编程

请看以下的式子:

a = 2
b = 2
c = a + b   // c is 4
d = c + 2   // d is 6
b = 3
// now what is the value of d?

在传统的编程的方式里,我就要重新计算c,然后再去求d出来。大概是这样:

c = a + b   // c is 5
d = c + 2   // d is 7 

在代码层面上,大概就是起码要再写两句代码,才能让d获得一个新的值。

幸好,我们现在可以利用响应式的方法,将d绑定到c上,c就绑定到a + b上。然后上面的操作通通不用做,d的值会时刻保持为当前值。

以上就是基本的原理,但实际使用肯定不是这么简单的绑定响应,而是对文字、控件大小、网络请求等,组成各种复杂的响应链。这个响应链就是每一个事件流的组合,像链条一样发生连锁响应。

iOS上的响应式编程

iOS的设计模式

MVC示意图

这就是目前iOS应用普遍使用的主体框架的设计模式

问题是:这就是MVC的全部吗?
MVC表情

显然不是,这实际上只是iOS上的MVC而不是MVC的完整概念。

其实MVC这一层还有一个概念,就是Model与View的绑定。但是显然,iOS原生根本不提供这种支持,所以绑定也就无从入手。
MVC完整图

ReactiveCocoa的出现

ReactiveCocoa是由Github团队开源的一个框架级的项目。这个玩意就是将大家带进了iOS的FRP(Functional Reactive Programming)时代,上面MVC中的M和V的绑定,就可以通过RAC去轻松实现了。当然,数据绑定只是其中一个基本的特性,其中事件流的响应还可以有各种的加工处理。RAC中的流通基本单位就是Signal,通过不断流通Signal来实现了整个响应式的链条。简单总结一下,就是RAC用它这种Signal的模式,把iOS开发常用的KVO、delegate、target等常用模式,都统一起来。

说到这里,是不是开始产生了一种可以拯救世界的错觉??

ReactiveCocoa的缺点

  1. 和传统的开发认知相差甚远,会让使用者怀疑人生
  2. 学习曲线陡峭得有点猥琐
  3. 方法命名魔性不友好
  4. 调用堆栈层次深不见底,debug难度加大
  5. 使用时必须时刻注意引用问题
  6. 据说过于复杂的事件流会影响性能(未遇到过)
  7. 框架级代码,不是每个人都能接受

ReactiveCocoa基本概念

信号(Signal):也就是流,RAC中所有的信号都是RACStream的子类。信号有冷热之分,信号也有中继、改变等操作。信号是让订阅者去订阅,然后触发对应事件。

  • 冷信号(Cold):就是什么事情也不干的,没有订阅者的时候则是这种状态。
  • 热信号(Hot):已经有了订阅者就会激活为热信号,同时持有对应的订阅block,在complete之前都不会释放。

订阅者(Subscriber):负责接收信号执行操作,都必须实现RACSubscriber协议。订阅者之间的关系原则上是平等的,但实际操作上也是有先后顺序,而且还有副作用的情况需要考虑。

副作用(Side Effect):在每一次订阅的是时候都会触发对应的操作,导致订阅者收到信号的时候,结果会和其他订阅者不同。

RAC基本入门

创建最基本的信号

先看看这个例子:

RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    NSLog(@"信号发出了");
    [subscriber sendNext:@"成功"];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
                NSLog(@"取消");
            }];
}];

看到这个玩意,或许根本没办法明白这玩意到底干了什么操作是吧?好的,下面一步一步来分解:

  • ^ RACDisposable * (id<RACSubscriber> subscriber)

    • id<RACSubscriber> subscriber,这就是所谓的订阅者,我们需要把结果传给它,由它来负责把结果分发到所有的订阅者去。如:[subscriber sendNext:@"成功"]
    • RACDisposable *,返回这类对象是为了在取消订阅的时候可以执行相关的操作。
  • [subscriber sendCompleted],就是表示信号已经完成了任务,可以安息了。实质就是对应订阅者的block也会释放掉了。

实际上,创建了信号以后,信号是不会干任何事情的。上面那个信号还是一个冷信号(Cold),需要激活才会执行操作。

[signal subscribeNext:^(id x):^{
    NSLog(@"%@", x);
}];

只有通过subscribe,或Signal Operation的时候,信号才会真正执行。好的,那现在x的结果是什么?

数据绑定

RAC最大的特性就是优雅的数据绑定方式。在传统的KVO中API过于猥琐,用的人基本都得写- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context这个奇丑无比的方法(特别是那个context)。当然KVO的功能还是强大的,引用一下文档的描述:

This message is sent to the receiver when the value at the specified key path relative to the given object has changed.

本质也就是通过截取相关setter来实现的,所以类似_name = newName这种直接针对成员变量赋值的操作就不要妄想可以使用KVO了。有点扯远了,拉回正题,RAC的数据绑定就是基于KVO的。因此,如果某个property不支持KVO的话,RAC也是无能为力的(目前无解决办法,至于如何得知是否支持KVO?你猜?)。

以下也是举个例子:

RAC(self, isEditing) = RACObserve(self.viewModel, isEditing);

好的,这就已经写完数据绑定了。上面是一个单向绑定的例子,目的就是selfisEditing的值直接绑定到self.viewModelisEditing。也就是是说self.viewModelisEditing改变时,会直接赋值到selfisEditing上去。首次绑定的时候,selfisEditing会被改为后者的值。
这里RAC和RACObserve干了些什么暂时不作解释了,接着说。

RACChannelTo(self, blockShowText) = RACChannelTo(self.viewModel, blockShowText);

这就是双向绑定,两边任何一边变化,都会对另一边产生影响。
看完了这种绑定方式,估计大家再也不想用原生的KVO了。

RACCommand

以下也是一个很简单的例子,

RACCommand

看到了按钮被disabled的样式了吧,如果正常情况下要实现,需要做些什么操作?

点击按钮–>禁用按钮–>网络请求–>启用按钮

现在我们只需要这样:

self.loginButton.rac_command = self.loginCommand;

loginCommand的操作很可能只需要这样:

self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            return [DASLoginServiceModel qryLoginUserName:self.userName
                                                 password:self.password];
        }];

DASLoginServiceModel里面的操作这里先不关心,我们优先理解RACCommand是怎么工作。
我们看看源码先:

static void *UIButtonRACCommandKey = &UIButtonRACCommandKey;
static void *UIButtonEnabledDisposableKey = &UIButtonEnabledDisposableKey;

@implementation UIButton (RACCommandSupport)

- (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];
}

@end

简要解释一下:

  1. disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]这句把button的状态和command的执行状态绑定在一起了。因此,在command执行的时候,button就会被禁用掉。只有command执行完成的时候,button才会恢复启用状态。
  2. [self addTarget:self action:hijackSelector forControlEvents:UIControlEventTouchUpInside]button响应UIControlEventTouchUpInside事件触发command。
  3. 至于command什么时候才算完成执行,实现上就跟^RACSignal *(id input)返回的Signal有关了,只要Signal还没complete或者error,则还算是执行状态。
  4. 点击的时候,本质就是会调用[self.rac_command execute:sender],实际上也可以自己手动调用,也可以有类似的效果。

除了结合Button,也有其他实际应用的例子:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ((indexPath.section == (_numberOfSections-1)) &&
        (indexPath.row == (_numberOfRowsInLastSection - 1)) &&
        !_completed) {
        [_loadMoreCommand execute:self];
    }

    return [super tableView:tableView cellForRowAtIndexPath:indexPath];
}

RACCommand还有一个特性就是不会在当前任务没完成执行的时候,就接受一个新的任务。因此这里可以理解为,如果当前加载更多的请求没完成,则不会再次请求。

常用的几种Operation

map

算是比较简单的operation之一,举例之:

RAC(self.textNumberLabel, text) = [self.contentTextView.rac_textSignal map:^id(NSString *value) {
        return [NSString stringWithFormat:@"%d", (int)value.length;
    }];

通过map就可以直接在operation里面进行数值映射,绑定的数据也就可以可以经过自己的一层处理。

takeUntil

指定一个信号在另一个信号发出sendNext之后,就被complete掉,如下:

@weakify(self);
[[[[NSNotificationCenter defaultCenter]
     rac_addObserverForName:UIKeyboardWillHideNotification
     object:nil]
     takeUntil:self.rac_willDeallocSignal]
     subscribeNext:^(NSNotification *notification) {
         @strongify(self);
         self.scrollView.contentOffset = CGPointMake(0.0, 0.0);
         self.bottomGapConstraint.constant = defaultBottomGap;
     }];

为什么这里要执行takeUntil??
因为,[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillHideNotification object:nil]本来是冷信号,订阅以后立即变成热信号。如果不做这一步操作,self即使释放了,订阅的block也是不会释放的。每次通知发出的时候,还是会再次来到这个block去处理。

flattenMap

之前的map其实也是通过flattenMap来实现的,map只是改变分发的值。而flattenMap不仅可以获得上一个信号的值,还能激活下一个信号,形成这种链式反应。

RACSignal __block *queueSignal = nil;
for (AskImageModel *model in self.imageModels) {
    if (model.uploadUrl.length)
        continue;

    NSString *imagePath = model.imagePath;
    @weakify(model);
    if (queueSignal) {
        queueSignal = [queueSignal flattenMap:^RACStream *(id value) {
            RACSignal *oneSignal = [PicUploadServiceModel qryUploadPic:imagePath];
            [oneSignal subscribeNext:^(PicUploadServiceModel *x) {
                @strongify(model);
                model.uploadUrl = x.imageUrl;
            }];
            return oneSignal;
        }];
    } else {
        queueSignal = [PicUploadServiceModel qryUploadPic:imagePath];
        [queueSignal subscribeNext:^(PicUploadServiceModel *x) {
            @strongify(model);
            model.uploadUrl = x.imageUrl;
        }];
    }
}

@weakify(self);
if (queueSignal) {
    [queueSignal
     subscribeNext:^(id x) {
         @strongify(self);
         NSArray *imageUrls = [self.imageModels linq_select:^id(AskImageModel *item) {
             return item.uploadUrl;
         }];
         [self submitQuestionWithImageUrls:imageUrls];
     }
     error:^(NSError *error) {
         [SVProgressHUD showErrorWithStatus:@"上传失败,请重新提交"];
     }];
} else {
    NSArray *imageUrls = [self.imageModels linq_select:^id(AskImageModel *item) {
        return item.uploadUrl;
    }];
    [self submitQuestionWithImageUrls:imageUrls];
}

上面的代码实际是把本地的图片一张张上传,如果其中一张错误则中断。下次上传的时候只上传还没成功上传的图片。通过flattenMap可以组成一个链条,对于错误处理可以直接在最终的合成好的信号上执行。

combineLatestWith和combineLatest

这个就是组合信号的operation,只有当这些组合的信号都起码发出了sendNext,才会分发到订阅者。下一次,只要其中一个信号再次sendNext的时候,也会再次分发。

@weakify(self);
[[RACObserve(self, isEditing)
  combineLatestWith:RACObserve(self, templateModel)]
  subscribeNext:^(id x) {
      @strongify(self);
      RACTupleUnpack(NSNumber *flag, DASTemplateModel *tModel) = x;
      if (!flag.boolValue) {
          self.title = @"模版详情";
      } else if (flag.boolValue && tModel) {
          self.title = @"编辑模版";
      } else {
          self.title = @"添加模版";
      }
  }];

和网络请求的信号组合使用可以达到并发的效果,成功和失败可以进行统一处理。
由于operation的种类很多,我也只能介绍一部分,详细的还是看文档吧。

事后PS

RAC(…)是什么鬼

循例先看看源码:

#define RAC(TARGET, ...) \
    metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \
        (RAC_(TARGET, __VA_ARGS__, nil)) \
        (RAC_(TARGET, __VA_ARGS__))

/// Do not use this directly. Use the RAC macro above.
#define RAC_(TARGET, KEYPATH, NILVALUE) \
    [[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(TARGET) nilValue:(NILVALUE)][@keypath(TARGET, KEYPATH)]

看到这句metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)),大家也是可以忽略了。因为里面是多个宏的嵌套,略复杂,我直接解释,metamacro_argcount(__VA_ARGS__)是用于计算可变参数的个数(编译期),metamacro_if_eq则是判断值是否相等(编译期)。从而做到可以选择编译成RAC_(TARGET, __VA_ARGS__, nil)还是RAC_(TARGET, __VA_ARGS__)

@weakify和@strongify

我这里不作源码解释了,当遇到会出现循环引用的情况,请主动加上。这类的循环引用编译器无法检测。嵌套多个作用域的时候,也要再次加上@strongify。

1 Comment

发表回复