在绘制UI时,我们常希望能够直接获取所需数据,但大多数情况下,数据需要经过多个步骤处理后才可使用,好比UI使用到的数据是经过流水线加工后最后一端产出的成品。众所周知,流水线是由多个片段管线组成,上端管线处理后的已加工品成为下端管线的待加工品,每段管线都有对应的管线工人来完成加工工作,直至成品完成。
RAC则为我们提供了构建数据流水线的能力,通过组合不同的加工管线来导出我们想要的数据。想要构建好RAC的数据流水线,我们需要先了解流水线中的组成元素——RAC管线。RAC管线的运作实质上就是RAC中一个信号被订阅的完整过程。下面我们来分析下RAC中一个完整的订阅过程,并由此来了解RAC中的核心元素。
RAC核心是Signal,对应的类为RACSignal。它其实是一个信号源,Signal会给它的订阅者(Subscriber)发送一连串的事件,一个Signal可比作流水线中的一段管线,负责决定管线传输什么样的数据。Subscriber是Signal的订阅者,我们将Subscriber比作管线上的工人,它在拿到数据后对其进行加工处理。数据经过加工后要么进入下一条管线继续处理,要么直接被当做成品使用。
关于ReactiveCocoa信号订阅的基本过程的具体信息参见美团技术博客文章
###使用pod管理 OC版本和Swift版本
- 纯OC使用 2.1.8
- 纯Swift 使用最新版本,注意与Swift版本兼容问题
- OC和Swift混编, 分别pod OC和Swift版本到项目中
下面先从几个UI控件使用RAC入手, 简单介绍一下RAC, 后面会有RAC高级用法
###UIButton使用RAC UIButton有两种使用RAC的方法:
####方式一:此种方式能控制按钮的状态, 而且状态也是一个信号
1 |
|
####方式二:
-
使用RAC UIControl的分类
1
2
3[[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *x) { NSLog(@"clicked===%@",x); }];
###UITextField
-
方式一:监听RAC给UITextField添加的分类属性, 来监听文字的变化
1
2
3
4//检测文字的变化1 [field.rac_textSignal subscribeNext:^(id x) { NSLog(@"%@",x); }];
-
方式二:使用RAC UIControl的分类,来监听
1 |
|
-
方式三:rac实现协议的方法
1
2
3
4
5
6
7
8//rac实现协议的方法 //设置代理, 必须设置 field.delegate = self; //rac信号绑定 [[self rac_signalForSelector:@selector(textFieldShouldReturn:) fromProtocol:@protocol(UITextFieldDelegate)] subscribeNext:^(id x) { UITextField *t = x[0]; NSLog(@"%@",t.text); }] ;
###UIAlertView
1 |
|
###KVO
-
直接实现
1
2
3
4
5//KVO监听 [[self rac_valuesForKeyPath:@"text" observer:nil] subscribeNext:^(id x) { NSLog(@"%@",x); }];
-
使用宏RACObserve实现KVO
1
2
3[RACObserve(self, text) subscribeNext:^(id x) { NSLog(@"%@",x); }];
###多信号合并combine 和 merge的区别
-
combine:所有信号都完成(至少发送过一次sendNext:)之后将结果合并成一个tuple返回, 使用方法如下:
1
2
3[[RACSignal combineLatest:@[signal1, signal2]] subscribeNext:^(id x) { NSLog(@"%@",x); }];
-
merge:只要有一个信号完成, 就将结果返回
1
2
31
2
3[[RACSignal merge:@[signal1, signal2]] subscribeNext:^(id x) { NSLog(@"%@",x); }];
###RAC同步和异步执行
-
同步执行
1
2
3[[signal1 concat:signal2] subscribeNext:^(id x) { NSLog(@"%@",x); }];
-
异步执行
1
2
31
2
3[[RACSignal merge:@[signal1, signal2]] subscribeNext:^(id x) { NSLog(@"%@",x); }];
###RAC中的常用宏
-
RACObserve
1
2
3
4//观察self中text属性的变化, [RACObserve(self, text) subscribeNext:^(id x) { NSLog(@"%@",x); }];
-
RAC
1
2//将self的text属性和文本框的信号绑定, 只要文本框的文本发生变化, 那么就会将文本框的值赋值给self的text属性 RAC(self, text) = field.rac_textSignal;
-
RACChannelTo: 实现绑定的一种方式
1
2//将A的text属性的值与B的text绑定 RACChannelTo(_textA, text) = [_textB rac_newTextChannel]; ###@weakify() 和 @strongify() RAC的所有block中使用self都会出现循环引用, 所有要使用@weakify和@strongify来防止循环引用, 使用方法如下:
weakify(self); self.foo = ^{ strongify(self) self.text = @””; };
###RAC的副作用以及冷热信号 副作用的产生和冷信号有关, 下面先介绍一下冷信号, 和热信号.详细介绍参见美团技术团队的技术博客:
- 细说ReactiveCocoa的冷信号与热信号(一)
- 细说ReactiveCocoa的冷信号与热信号(二):儿什么要区分冷信号和热信号
- 细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号和热信号
这里我简单的总结一下:
冷信号
- 在被订阅后才会发送信号, 没有订阅者不会发送信号.(被动)
- 冷信号有多个订阅者时, 消息是重新, 完整的发生一遍.
- 订阅者都能收到完整的信息,也就是说订阅者能收到其订阅此信号前和订阅此信号后的全部消息.
- 除了RACSubject及其子类的信号都是冷信号.
热信号
- 不被订阅也会主动发送信号.(主动)
- 有多个订阅者时, 是多个订阅者共享消息
- 订阅者只能收到订阅后的信息
- RACSubject及其子类的信号都是热信号.
所以subject类似“直播”,错过了就不再处理。而signal类似“点播”,每次订阅都会从头开始。所以我们有理由认定subject天然就是热信号。
冷信号转化成热信号: publish, multicast, replay, replayLast, replayLazily
冷信号转成热信号的原理是使用hotSignal(RACSubject)订阅coldSignal(冷信号), 然后在订阅此hotsignal信号, 就能将coldSignal转化成hotSignal.有如下一系列的API能完成此功能:
- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
` - (RACSignal *)replayLast;`
` - (RACSignal *)replayLazily;`
每次订阅一个信号, 信号里面的代码就会重复执行一次,可能得到意想不到的错误信息, 当然副作用页可以加以利用
-
解决副作用的方法很简单, 只要在信号后加上 replyLast, 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//重复订阅一个信号会重复执行信号里的内容 - (void)sideEffect { __block NSInteger a = 1; RACSignal *signal1 = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ a++; [subscriber sendNext:@(a)]; [subscriber sendCompleted]; }); return [RACDisposable disposableWithBlock:^{ NSLog(@"release"); }]; }] replayLast];//解决副作用的方法replayLast [signal1 subscribeNext:^(id x) { NSLog(@"%@",x); }]; [signal1 subscribeNext:^(id x) { NSLog(@"%@",x); }]; [signal1 subscribeNext:^(id x) { NSLog(@"%@",x); }]; }
-
解决方法二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18- (void)RACMulticastConnectionAction{ __block int a = 0; RACSignal *s1 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { a++; [subscriber sendNext:@(a)]; //完成后才会执行concat, 否则不执行 [subscriber sendCompleted]; return nil; }]; RACMulticastConnection *connect = [s1 multicast:[RACReplaySubject subject]]; [connect connect]; [connect.signal subscribeNext:^(id x) { NSLog(@"%@",x); }]; [connect.signal subscribeNext:^(id x) { NSLog(@"%@",x); }]; } ###定时器事件
-
延时执行
1
2
3
4//延时执行 [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{ NSLog(@"3 delays"); }];
-
定时执行
-
1
2
3
4
5//定时执行 //takeUntil方法, 防止控制器pop之后定时器仍然执行 [[[RACSignal interval:2 onScheduler:[RACScheduler mainThreadScheduler]] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) { NSLog(@"每两秒一次"); }];
-
双向绑定
-
方式一:最笨, 也是最安全的方式.
1
2
3
4RACChannelTerminal *Asignal = [_textA rac_newTextChannel]; RACChannelTerminal *Bsignal = [_textB rac_newTextChannel]; [Asignal subscribe:Bsignal]; [Bsignal subscribe:Asignal];
-
方式二:推荐的方式:
1
2RACChannelTo(_textA, text) = [_textB rac_newTextChannel]; RACChannelTo(_textB, text) = [_textA rac_newTextChannel];
-
方式三:最优雅但是会有漏洞的方法.因为RAC是完全基于KVO的, 而有UI控件不支持KVO, 所以会有漏洞. 如果双向绑定与UI没有关系,那么推荐使用此方法.
RACChannelTo(_textA, text) = RACChannelTo(_textB, text)
如果使用此方法将两个UI控件绑定, 那么在UI界面中, 改变其中一个文本框的值另一个文本框是不会跟着改变的, 这也就是我上面提到的bug, 但是如果使用代码修改文本框的值, 那么另一个文本款的值会跟随前面的改变
1 |
|
RACChannelTerminal
replay, replayLast, replayLazily
ReactiveCocoa提供了这三个简便的方法允许多个订阅者订阅一个信号,却不会重复执行订阅代码,并且能给新加的订阅者提供订阅前的值。replay和replayLast使信号变成热信号,且会提供所有值(-replay) 或者最新的值(-replayLast) 给订阅者。 replayLazily返回一个冷的信号,会提供所有的值给订阅者。
replay
- 当源信号被订阅时,会立即发送给订阅者全部历史的值,不会重复执行源信号中的订阅代码,不仅如此,订阅者还将收到所有未来发送过去的值。那么能接受到的值就是历史信息的所有值和所有未来值
replayLast
当源信号被订阅时,会立即发送给订阅者最新的值,不会重复执行源信号中的订阅代码。订阅者还会收到信号未来所有的值。那么能接受到的值就是历史信息的最新值和所有未来值
replayLazily
这replayLazily方法返回一个新的信号,当源信号被订阅时,会立即发送给订阅者全部历史的值,不会重复执行源信号中的订阅代码。跟replay不同的是,replayLazily被订阅生成新的信号之前是不会对源信号进行订阅的(原文写的有点绕,简单来讲 直到订阅时候才真正创建一个信号,源信号的订阅代码才开始执行)。那么能接受到的值就是没搞明白, 慎用
语法如下, 以replay为例, 其他一样
1 |
|
retry
只要失败, 或者在调用[subscriber sendCompleted]之前,就会重新执行创建信号中的block,直到成功或者完成.
1 |
|
throttle节流
throttle节流: 当某个信号发送比较频繁时,可以使用节流.如果发送信号的时间间隔小于throttle的时间间隔, 那么信号不会被发送, 如果信号信号发送之间的时间间隔大于throttle的时间, 信号就会被立即发送, 并且发送的是最新的那一条信号的信息。
1 |
|
throttle 带条件的方法:- (RACSignal *)throttle:(NSTimeInterval)interval valuesPassingTest:(BOOL (^)(id next))predicate;
1 |
|
timeout
可以让一个信号在一定的时间后,自动报错
1 |
|
interval
每隔一段时间发出信号, 定时器
1 |
|
delay
延时发送next
1 |
|
filter
filter:过滤信号,使用它可以获取满足条件的信号.
1 |
|
ignore
ignore:忽略完某些值的信号.
1 |
|
distinctUntilChanged
当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。
1 |
|
take
从开始起一共取n次的信号
1 |
|
takeLast
取最后N次的信号,前提条件,订阅者必须调用完成,因为只有完成,就知道总共有多少信号.
1 |
|
takeUntil
一直获取信号直到某个信号执行完成
1 |
|
skip
从开始跳过几个信号, 然后才开始监听
1 |
|
flatten
只能用于信号中的信号, 订阅者能获取信号中的信号的所有信息.
1 |
|
switchToLatest
switchToLatest只能用于signalOfSignals(信号的信号),switchToLatest的作用就是获得信号中的信号的最新信号所发的信息(一个信号订阅了多个信号之后, 将多个信号中的最后一个信号的信息发送给下一个订阅者). switchToLatest必须用于信号中的信号, 否者会崩溃. switchToLatestatten和flatten的作用都是获取信号中的信号, 区别就是前者是获取信号中的信号的最新信号的信息, 后者是获取信号中的信号的所有信息
1 |
|
combineLatest
combineLatest:将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。
zipWith
把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件
1 |
|
merge
把多个信号合并为一个信号,任何一个信号有新值的时候就会调用
1 |
|
then
用于连接两个信号,当第一个信号完成,才会连接then返回的信号 then:用于连接两个信号,当第一个信号完成,才会连接then返回的信号. 使用then连接信号,之前信号的值会被忽略掉, 只会返回then的信号值.
1 |
|
concat
concat:按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号 注意: 使用concat连接的信号和使用then连接的信号都是要在前一个信号发送完毕之后(sendCompleted),才会执行concat或者then后的信号
1 |
|
map, flattenMap
flattenMap,Map用于把源信号内容映射成新的内容
FlatternMap和Map的区别
1.FlatternMap中的Block返回信号。
2.Map中的Block返回对象。
3.开发中,如果信号发出的值不是信号,映射一般使用Map
4.开发中,如果信号发出的值是信号,映射一般使用FlatternMap。
scanWithStart
常用于信号输出值的聚合处理,也就是将上一次的输出结果, 当做参数传入下一次处理中.应用场景举例:没有做本地保存的下拉刷新和上拉加载更多可以使用scanWithStart将数据进行连接
1 |
|
deliverOnMainThread, deliverOn 和 subscribeOn
都是将信号发送到所需的线程.前两者就是讲信号的结果传递到所需的线程中, 但是副作用还是在原来的线程中, 而subscribeOn是将信号的订阅过程都放在所需的线程中, 自然副作用也是在所需的线程中.(所需的线程是开发者根据需求指定的线程)
###综合案例(来自美团技术博客):
需求: 用户在searchBar中输入文本,当停止输入超过0.3秒,认为seachBar中的内容为用户的意向搜索关键字searchKey,将searchKey作为参数执行搜索操作。搜索内容可能是多样的,也许包括搜单聊消息、群聊消息、公众号消息、联系人等,而这些信息搜索的方式也有不同,有些从本地获取,有些是去服务器查询,因此返回的速度快慢不一。我们不能等到数据全部获取成功时才显示搜索结果页面,而应该只要有部分数据返回时就将其抛到主线程渲染显示。在这个需求中,从数据输入到最后搜索数据的显示可以具象成一条数据流,数据流中各处对于数据的操作都可以使用上面提到的RAC Operation来完成,通过组合Operation完成以下RAC数据流图
####实现方法:
1 |
|
RACCommond
RACDisposable
RACScheduler
RACMulticastConnection
RACReplaySubject
RACSubject的使用方法
- 创建一个RACSubject的实例;
- 订阅subject的dealloc信号,在subject被释放的时候会发送完成信号;
- 订阅subject;
- 使用subject发送一个值。
1 |
|
RACSignal和RACSubject虽然都是信号,但是它们有一个本质的区别:
RACSubject会持有订阅者(因为RACSubject是热信号,为了保证未来有事件发送的时候,订阅者可以收到信息,所以需要对订阅者保持状态,做法就是持有订阅者),而RACSignal不会持有订阅者。
RACSignal的引用图如下
RACSubject的引用图如下
RAC内存泄露和解决方案
- flatmap, map,RACObserver都会隐式的持有self, 所以在block中使用上面这些放方法的时候一定要使用weakSelf解决循环引用的问题.使用方法如下:
1 |
|
-RACSubject造成block不被释放的解决办法
其实在ReactiveCocoa的实现中,几乎所有的操作底层都会调用到bind这样一个方法,包括但不限于:
map、filter、merge、combineLatest、flattenMap ……
所以在使用ReactiveCocoa的时候也一定要仔细,对信号操作完成之后,记得发送完成信号,不然可能在不经意间就导致了内存泄漏。
RACSubject就是一个比较典型直接的例子。除此之外,如果在对一个信号进行类似replay这样的操作之后,也一定要保证源信号发送完成;不然,也是会有内存泄漏的。
RAC使用注意事项
rac_textSignal监听文本框的text, 通过代码改动text的值不会调用block的值
1 |
|
原因: 看看源码实现可知此方法主要是实现textfield的代理方法, 所有用代码修改text的值是不会触发代理方法的
然而, 使用RACObserve只能监听通过代码改动的变化, 所以最佳的解决方案是将两者合并:
1 |
|
RAC语录
- 任何信号的转换都是对原有信号进行订阅,从而产生新的信号.