ReactiveCocoa笔记(二)—— 源码探究
  • 看完源码画的流程图:

RACSignal流程

理解RAC

初学者总是容易被一堆概念搞得晕头转向,我想其实无非是这几种:

1
2
3
4
5
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber){
[subscriber sendNext:@(1)];
[subscriber sendCompleted];
return nil;
}];

  1、createSignal好难啊;
  2、subscriber是什么?
  3、这个block什么时候调用?

1
2
3
4
5
6
7
8
9
10
[signal subscribeNext:^(id x) {
if ([x boolValue]) {
_navView.hidden = YES;
} else {
_navView.hidden = NO;
[UIView animateWithDuration:.5 animations:^{
_navView.alpha = 1;
}];
}
}];

  4、subscribeNext又是什么?
  5、这个block什么时候调用?
  6、看起来上面两段代码有关系,但是具体怎么作用的?

让我们先来解决上面的困惑吧!

第一部分 订阅者和信号

1、隐藏的订阅者

平时我们打交道的就是信号,但是总是说订阅,却不知道订阅到底是如何进行的,也无法解答上面的问题,让我们根据源码分析一下订阅过程。

首先来认识一个对象:订阅者(RACSubscriber)。
订阅者订阅信号,就是这么简单的一件事情。只不过框架隐藏了这个对象,我们也不必要和订阅者打交道,只需要告诉信号一件事情,那就是如果发送了数据(三种事件:next、complete、error),我们需要做什么事情(类似回调的概念)。

第一步创建信号

看一下上面的第一段代码,createSignal类方法:
这里要说一下,信号RACSignal有一些子类,我们常用的是RACDynamicSignal和RACSubject,先不理会RACSubject。createSignal类方法创建的就是RACDynamicSignal对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-----RACDynamicSignal.h-----
@property (nonatomic, copy, readonly) RACDisposable * (^didSubscribe)(id<RACSubscriber> subscriber);

-----RACSignal.m-----
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
return [RACDynamicSignal createSignal:didSubscribe];
}

-----RACDynamicSignal.m-----
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
RACDynamicSignal *signal = [[self alloc] init];
signal->_didSubscribe = [didSubscribe copy];
return [signal setNameWithFormat:@"+createSignal:"];
}
  • 可以发现,RACDynamicSignal有一个属性,名字叫didSubscribe的block对象。createSignal方法传递的block参数,就是赋值给didSubscribe属性。

对于问题1,我们可以暂时这么回答,createSignal的意义是,创建一个signal对象,并且把block参数赋值给signal的名为didSubscribe的属性,这个block的参数是subscriber,返回RACDisposable。

第二步订阅信号

  看一下第二段代码subscribeNext:

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
-----RACSubscriber.m-----
@property (nonatomic, copy) void (^next)(id value);

-----RACSignal.m-----
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
NSCParameterAssert(nextBlock != NULL);

RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
return [self subscribe:o];
}

-----RACDynamicSignal.m-----

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
NSCParameterAssert(subscriber != nil);

RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable];

if (self.didSubscribe != NULL) {
RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{
RACDisposable *innerDisposable = self.didSubscribe(subscriber);
[disposable addDisposable:innerDisposable];
}];

[disposable addDisposable:schedulingDisposable];
}

return disposable;
}

  我们可以看到,subscribeNext方法第一步是创建了一个RACSubscriber,也就是创建了一个订阅者,而且把subscribeNext的参数传递给RACSubscriber对象,RACSubscriber会把参数赋值给自己一个名为next的Block类型的属性,这里,我们可以回答上面第4个问题,subscribeNext方法创建一个订阅者,并且把block参数,传递给订阅者一个名字叫next的属性,block参数接收的是id类型,返回的是RACDisposable对象。接下来执行[self subscribe:o],也就是订阅操作。我们在看看订阅方法subscribe的实现:上面的代码很清晰,直接是self.didSubscribe(subscriber),我们可以知道,刚刚创建的subscriber对象,直接传递给上文中我们提到的signal的didSubscribe属性。这样,我们可以解释上面的第二个和第三个问题,subscriber就是didSubscribe的形参,block对象是在subscribeNext的时候执行的,刚刚的订阅者对象作为参数传入,就是subscriber对象。

  那么createSignal方法中,[subscriber sendNext:@(1)]是什么意思呢?
  看一下sendNext方法吧:

1
2
3
4
5
6
7
8
- (void)sendNext:(id)value {
@synchronized (self) {
void (^nextBlock)(id) = [self.next copy];
if (nextBlock == nil) return;

nextBlock(value);
}
}

  我们可以发现,sendNext的实现,也就是直接执行上文中的nextBlock。也就是回答了上面第五个问题。

总结

signal持有didSubscribe参数(createSignal传进来的那个block),subscriber持有nextBlock(就是subscribeNext传进来的那个block),当执行[signal subscribe:subscriber]的时候,signal的didSubscribe执行,内部有subscriber sendNext的调用,触发了subscriber的nextBlock的调用。到这里,我们基本把信号和订阅者,以及订阅过程分析完毕。

  • RACSignal流程图:

RACSignal流程

第二部分 信号和事件

  刚才我们说过,signal有几个子类,每一个类型的signal订阅过程其实大同小异,而且初期常见的也就是RACDynamicSignal,其实我们不需要太关心这个问题,因为无论是自定义信号,还是框架定义的一些category,例如,textFiled的rac_textSignal属性,大多数都是RACDynamicSignal。另一个常见的类型RACSubject可以以后理解。

  还有就是,我们刚刚谈到了三种事件,分别是next、error、complete,和分析next的订阅过程一样,举个例子,我们发送网络请求,希望在出错的时候,能给用户提示,那么首先,创建信号的时候,在网络请求失败的回调中,我们要[subscriber sendError:netError],也就是发送错误事件。然后在订阅错误事件,也就是subscriberError:…这样就完成了错误信号的订阅。

  complete事件比较特殊,它有终止订阅关系的意味,我们先大致了解一下RACDispoable对象吧,我们知道,订阅关系需要有终止的时候,比如,在tableViewCell的复用的时候,cell会订阅model类产生一个信号,但是当cell被复用的时候,如果不把之前的订阅关系取消掉,就会出现同时订阅了2个model的情况。我们可以发现,subscribeNext、subscribeError、subscribeComplete事件返回的都是RACDisopable对象,当我们希望终止订阅的时候,调用[RACDisposable dispose]就可以了。complete也是这个原理。

冷热信号

  • 冷信号: 冷信号是被动的,只有当你订阅的时候,它才会发布消息,只能一对一,当有不同的订阅者,消息是重新完整发送。
  • 热信号: 是主动的,尽管你并没有订阅事件,但是它会时刻推送; 可以有多个订阅者,是一对多,集合可以与订阅者共享信息;

为什么要区分冷热信号?

在我们日常开发中,大多数情况是要产生副作用的,objc 本身也并不是纯函数语言。移动开发有很多页面与数据的交互,丰富的变化,也可以说我们是期待副作用的。
但是单纯的冷信号有时候会产生严重的问题,比如:

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
//--------------------------------------------------------------------------------------------
// 下面代码存在一个非常严重的问题
// 每一次在 RACSignal 上执行 -subscribeNext: 以及类似方法时,都会发起一次新的网络请求,我们希望避免这种情况的发生。
// RACSignal 一对一的单向数据流
// RACMulticastConnection 一对多的单向数据流
// RACChannel 一对一的双向数据流
//--------------------------------------------------------------------------------------------
RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSURL *url = [NSURL URLWithString:@"http://localhost:3000"];

AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
NSString *URLString = [NSString stringWithFormat:@"/api/products/1"];
NSURLSessionDataTask *task = [manager GET:URLString
parameters:nil
progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];

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

[requestSignal subscribeNext:^(id _Nullable x) {
NSNumber *productId = [x objectForKey:@"id"];
NSLog(@"productId: %@", productId);
}];

//--------------------------------------------------------------------------------------------

每一次订阅都会触发一次网络请求,这显然不是我们期望的。
此时可以使用 RACCommand 它并不表示数据流,只是一个继承自 NSObject 的类,但是它却可以用来创建和订阅用于响应某些事件的信号,是一个用于管理 RACSignal 的创建与订阅的类。

第三部分 进一步的深入

RAC是一个非常庞大的框架,平时的一些教程会误导大家纠结flattenMap和map的区别,这些问题,让人找不到头绪,导致入门更加的困难。实际上,学习它需要一个循循渐进的过程,RAC有很多作用,解耦合,更高效的解决一类问题等等,总之,他是对常规的面向对象编程很好的补充。所以,在理解完订阅的过程之后,重要的是,投入实际的运用中,我观察了不少开源的项目,并结合自己的实践发现,其实flattenMap这样的操作,非常少,几乎没有,常用的无非就是以下几个:手动构建信号(createSignal)、订阅信号(subscribeNext)、使用框架的一些宏定义(RACObserve、RAC)、然后就是学习几个最简单的操作,例如map、merge等就可以开始了。如果希望深入研究,一定要把这些基础的东西吃透,然后在学习更多的操作,例如flattenMap,了解side effect和多播的概念,学会RACSubject的用法(这个也是非常重要的对象)等等。如果把这些操作比作武器的话,可能更重要的是内功,也就是理解他的思想,我们如何通过实战,知道恰当的利用他的强大,慢慢的熟悉和深入是水到渠成的事情。

作为iOS开发人员,您编写的几乎所有代码都是对某些事件的反应;按钮点击,收到的网络消息,属性更改(通过键值观察)或通过CoreLocation更改用户位置都是很好的例子。但是,这些事件都以不同的方式编码;作为行动,代表,KVO,回调等。 ReactiveCocoa定义了事件的标准接口,因此可以使用一组基本工具更轻松地链接,过滤和组合它们。

ReactiveCocoa结合了几种编程风格:

  • 函数式编程,即以其他函数作为参数的函数
  • 响应式编程,重点关注数据流和变化传播

第四部分 ReactiveCocoa的使用

常见类

  1. RACSignal信号类

RACSignal信号类表示当数据改变时,在信号内部会利用订阅者发送数据.RACSignal默认是一个冷信号,只有被订阅以后才会变成热信号.
RACSignal信号类的简单使用:

1
2
3
4
5
6
7
8
9
10
11
12
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3.利用订阅者发送数据
// 只有当有订阅者订阅时,才会调用这个block
[subscriber sendNext:@"这是发送的数据"];
return nil;
}];

// 2.订阅信号
[signal subscribeNext:^(id x) {
NSLog(@"接收到数据:%@",x);
}];
  1. RACSubscriber订阅者

RACSubscriber是一个协议,任何遵循RACSubscriber协议的对象并且实现协议方法都可以是一个订阅者,订阅者可以帮助信号发送数据.
RACSubscriber协议中有四个方法.

1
2
3
4
- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
  1. RACDisposable

RACDisposable用于取消订阅和清理资源,当信号发送完成或发送错误时会自动调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3.利用订阅者发送数据
[subscriber sendNext:@"这是发送的数据"];
// 如果为未调用,当信号发送完成或发送错误时会自动调用
return [RACDisposable disposableWithBlock:^{
NSLog(@"资源被清理了");
}];
}];

// 2.订阅信号
[signal subscribeNext:^(id x) {
NSLog(@"接收到数据:%@",x);
}];
  1. RACSubject信号提供者

RACSubject继承RACSignal,又遵循了RACSubscriber协议,所以既可以充当信号,又可以发送信号,通常用它代替代理.

1
2
3
4
5
6
7
8
9
10
// 1.创建信号
RACSubject *subject = [RACSubject subject];

// 2.订阅信号
[subject subscribeNext:^(id x) {
NSLog(@"接收到数据:%@",x);
}];

// 3.发送信号
[subject sendNext:@"发送数据"];

RACSubject的底层实现

  • 在执行[RACSubject subject]时,RACSubject会在初始化时创建disposable对象属性和subscribers订阅者数组.

  • 在执行subscribeNext订阅信号时,会创建一个订阅者RACSubscriber,并将订阅者RACSubscriber添加到subscribers订阅者数组.

  • 在执行sendNext发送信号时,会遍历subscribers订阅者数组,执行sendNext

  1. RACReplaySubject

RACReplaySubject重复提供信号类,RACSubject的子类.由于RACReplaySubject的底层实现和RACSubject不同,RACReplaySubject可以先发送数据,再订阅信号.

1
2
3
4
5
6
7
8
9
10
// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];

// 2.订阅信号
[replaySubject subscribeNext:^(id x) {
NSLog(@"订阅信号,%@",x);
}];

// 3.发送数据
[replaySubject sendNext:@"发送的数据"];

RACReplaySubject的底层实现

  • 在执行[RACReplaySubject subject]时,创建一个valuesReceived数组

  • 在执行subscribeNext时,创建订阅者,遍历valuesReceived数组,利用订阅者执行sendNext发送valuesReceived中的数据.

  • 在执行sendNext时,将要发送的数据保存到valuesReceived数组中,执行sendNext

  1. RACMulticastConnection

我们在使用RACsignal,RACReplaySubject或者RACReplaySubject时,当一个信号被多个订阅者订阅时,在信号内部的block或被调用多次,有时这样并不能满足我们的需求,我们想要信号被多个订阅者订阅时,信号内部的block只被执行一次,那么RACMulticastConnection就能帮助我们完成需求.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3.发送数据
NSLog(@"发送数据");
[subscriber sendNext:@"发送数据"];
return nil;
}];

RACMulticastConnection *connection = [signal publish];

// 2.订阅信号
[connection.signal subscribeNext:^(id x) {
NSLog(@"接收到数据1:%@",x);
}];
[connection.signal subscribeNext:^(id x) {
NSLog(@"接收到数据2:%@",x);
}];

// 4.激活信号
[connection connect];

log中的打印如下

1
2
3
2017-06-20 16:55:50.809 MVVMRACDemoOC[2848:856666] 发送数据
2017-06-20 16:55:50.810 MVVMRACDemoOC[2848:856666] 接收到数据1:发送数据
2017-06-20 16:55:50.810 MVVMRACDemoOC[2848:856666] 接收到数据2:发送数据
  1. RACCommand

RACCommand是处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中.
使用一个demo说明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
// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"接收到命令:%@", input);
// 返回一个信号,不能为空.(信号中的信号)
// 3.创建信号用来传递数据
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"信号中的信号发送的数据"];
// 注意:数据传递完成,要调用sendCompleted才能执行完毕
[subscriber sendCompleted];
return nil;
}];
}];
self.command = command;

// 2.订阅信号中的信号(必须要在执行命令前订阅)
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"接收到信号中的信号发送的数据:%@",x);
}];

// 4.执行命令
[command execute:@1];

[[command.executing skip:1] subscribeNext:^(id x) {
if ([x boolValue] == YES) {
NSLog(@"正在执行");
}else{
NSLog(@"未开始/执行完成");
}
}];

log中的打印

1
2
3
4
2017-06-20 17:26:28.013 MVVMRACDemoOC[3166:970238] 接收到命令:1
2017-06-20 17:26:28.016 MVVMRACDemoOC[3166:970238] 正在执行
2017-06-20 17:26:28.016 MVVMRACDemoOC[3166:970238] 接收到信号中的信号发送的数据:信号中的信号发送的数据
2017-06-20 17:26:28.017 MVVMRACDemoOC[3166:970238] 未开始/执行完成

常见宏

  1. RAC(TARGET, [KEYPATH, [NIL_VALUE]])给某个对象的某个属性做绑定.
1
2
// 只要passwordTextField内容变化,accountTextField的text就会跟着改变
RAC(self.accountTextField, text) = self.passwordTextField.rac_textSignal;
  1. RACObserve(self, name)监听某个对象的某个属性,返回信号
1
2
3
4
// 监听passwordTextField背景色的改变
[RACObserve(self.passwordTextField, backgroundColor) subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
  1. @weakify(Obj)@strongify(Obj)一般用来防止循环引用,组合使用.
  2. RACTuplePack把数据包装成RACTuple(元组类)
1
2
// 把参数中的数据包装成元组
RACTuple *tuple = RACTuplePack(@1,@2);
  1. RACTupleUnpack把RACTuple(元组类)解包成对应的数据
1
2
3
4
5
// 把参数中的数据包装成元组
RACTuple *tuple = RACTuplePack(@"OneAlon",@"HangZhou");

// 解包元组,会把元组的值,按顺序给参数里面的变量赋值
RACTupleUnpack(NSString *name,NSNumber *address) = tuple;

常见用法

  1. 代替代理
    代替代理有两种方法,一种是使用RACSubject代替代理,另一种是使用rac_signalForSelector方法代替代理.
    这里模拟一个需求:自定义一个红色的view,在view中有一个按钮,监听按钮的点击.
    如果不使用RAC,在红色的view中定义一个代理属性,点击按钮的时候通知代理做事情.
    如果使用RAC,直接让红色的view调用rac_signalForSelector方法即可.
1
2
3
[[self.redView rac_signalForSelector:@selector(buttonClick:)] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
  1. 代替KVO

监听红色view的背景色的改变

1
2
3
[[self.redView rac_valuesAndChangesForKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew observer:nil] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
  1. 监听事件
1
2
3
[[self.button rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
NSLog(@"按钮被点击了");
}];
  1. 代替通知
1
2
3
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] subscribeNext:^(id x) {
NSLog(@"键盘弹出");
}];
  1. 监听文本框文字改变
1
2
3
4
[_textField.rac_textSignal subscribeNext:^(id x) {

NSLog(@"文字改变了%@",x);
}];
  1. 处理当界面有多次请求时,需要都获取到数据时,才能展示界面,rac_liftSelector:withSignalsFromArray:Signals

参考

  1. iOS Reactivecocoa(RAC)知其所以然(源码分析,一篇足以) - 简书
  2. ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2 | raywenderlich.com
  3. ReactiveCocoa(OC版) - 简书

延伸阅读

文章作者: MichaelMao
文章链接: http://frizzlefur.com/2018/12/29/ReactiveCocoa笔记(二) —— 源码探究/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下