标签:
ReactiveCocoa
是一套开源的基于Cocoa的FRP框架 .FRP的全称是Functional Reactive Programming
,中文译作函数式响应式编程,是RP(Reactive Programm,响应式编程)的FP(Functional Programming,函数式编程)实现。说起来很拗口。太多的细节不多讨论,我们先关注下FRP的FP特征。
函数式编程,简单来说,就是多使用匿名函数,将逻辑处理过程,以一系列嵌套的函数调用来实现,以减少中间状态的存在。
简单举例,一个简单的表达式,(将表达式理解成一系列的逻辑处理流程):
(1 + 2) * 3 - 4
如果是传统的过程式编程,则会这样写:
a = 1 + 2;
b = a * 3;
c = b - 4;
而在函数式编程中,我们将运算过程(逻辑处理流程),定义为不同的函数,然后会写成:
result = subtract(multiply(add(1,2), 3), 4)
从这里,我们就可以看出一个特点,过程式编程会在运行中,将一步步操作的结果以状态的形式纪录下来,下一步操作是修改上一步操作的结果。 而在函数式编程中,上一步操作的结果会直接以参数的形式或者其它形式传递给下一步操作,不再本地保存一堆无用的中间状态,而是输入一个初始值,就返回一个相应的结果。中间状态会互相影响,过多的中间状态会降低代码可读性以及提高维护的难度。通过函数式编程,减少状态的存在,一个操作,一个流程,只由输入值来决定输出结果,不在运行过程中以来全局状态或者保存中间状态。 所以函数式编程的主要优点就在于 不保存中间状态,缺点的话,为了不保存这个中间状态,而在函数间传递,会增加函数的调用次数,而这样会在一定程度上降低效率。
函数式编程的其它优点:
函数式编程,指尽量减少状态的保存,直接由输入得到结果,而不是在一些地方放置一堆的状态.即Model更新时,是直接作用于View,让View做出相应的显示,而不是保存一个状态,然后再通知View来获取这个状态.对于少量的状态,这样处理起来可能没问题,但是一旦状态多起来,管理就变得十分麻烦,难以调试.所以函数式编程,目的是 让相同的输入导出相同的输出,减少由于保存状态带来的影响.
响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
例如,在命令式编程环境中,a:=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但在响应式编程中,a的值会随着b或c的更新而更新。
响应式编程对应的是命令式编程,命令式编程中,数据以状态的形式保存,通过命令来通知需要状态的对象来更新状态。iOS和UIKit在设计上是命令式的,如TableView的DataSource ,委托模式强制将状态保存在委托中,以在请求发生时,为TableView提供数据.
数据的改变导致View的改变,现在我们常用的一般是命令式的编程,控制器去发动数据的改变,然后保存数据的状态,然后再去驱动View来加载最新的数据状态.
随着业务变得更加复杂,这种做法导致Controller上有一堆的状态数据,而这些状态数据错综复杂,互相影响,导致代码可读性较差,所以我们倾向于使用一种函数式编程的方式.数据修改后,直接设置View,而不是保存数据的状态,或者说将这步数据状态保存的操作抽离出来,不放在业务代码中,以提升代码的可读性,这就是使用ReactiveCocoa
的原因.
ReactiveCocoa
是一种Funcational Reactive Programming
。 不是用一堆表示状态的变量,这些变量会在许多情况下被修改,而是使用一种事件流的方式,用Signal
信号来表示状态,实时传递状态。
事件流已经统一了Cocoa中的常用的异步和实现处理,如:
将这些事件处理统一成一种信号的形式,所以就可以声明链式处理和聚合信号。
总结,很多iOS程序,对于事件的处理和响应是基于应用状态的,随着回调的增加和状态变量的增加,处理这些事件的代码就回变得异常复杂,这就是我们使用ReactiveCocoa的原因,通过响应式的处理以消除中间状态,增加代码可读性和扩展性,降低复杂度。
RAC通过一堆Block来实现事件流的传递,所以调试是意见很可怕的事情,在信号处理中的断点,会发现整个堆栈上全是RAC自己处理信号发送信号的操作,很难找到有用的信息,找到当前断点的上一级触发者。
RAC中推荐通过在事件流中加入副作用来进行调试。.on(event: { print ($0) })
,也可以通过logEvents
来输出事件流,但是这是2.5以后添加的新接口,而我们当前使用的依旧是2.5版本,因为2.5版本是纯Obejctive-C的。
ReactiveCocoa
是由Reactive Extensions
(微软的Rx库)启发,并在之后也深深地受其影响的。而RxSwift
是RX的正式成员,
ReactiveCocoa
是受FRP启发,但开发环境是在Cocoa上,所以API更接近于Cocoa。
主要区别在于RAC中有冷热信号的概念,这是RAC中的一个核心特点,后面会介绍到。RX则把冷热信号统一。
目前我们主要是在OC上开发,以后OC和Swift混编,这些情况下使用RAC都会更加方便一点。
在RAC的世界中,使用信号流处理事件. 信号的发送者,称为 receiver
,信号的接收者,称为subscriber
.
Stream,由RACStream
抽象类表示,表示一个对象的一系列的值。
值可以立刻被拿到,也可以在未来被拿到,但收到值是顺序的。不可能在流上不接受到第一个值而直接获取第二个值。
Stream是monads
的(在FP中表示用步骤流程表示的运算操作的结构),基于简单的基础信号进行复杂的操作。
RACStream
是一个抽象基类,通过signal
和sequences
来实现。
信号由RACSignal
类表示。信号表示那些将要在未来被传递的数据。程序运行时,数据的值在信号中被传递,推送给Subscribers
。用户需要去订阅这些信号获取数据。
信号发送三种不同的事件:
RACStream
的操作方法只能处理next类型的事件。传递的数据可以为nil。NSError
的对象来表示具体错误。必须特别处理,因为错误并不包含在RACStream
中。Stream
中。也需要特别处理,事件不会被包含在Stream
中。next的事件可以有任意数量个,但是error
和completed
事件最多只会发生一个。
subscriber
:订阅者,订阅信号,等待处理信号所发送的事件。在RAC中,一个订阅者者指继承RACSubscriber
协议。
一个订阅的创建可以通过调用-subscribeNext:error:completed:
等方法。订阅者持有所订阅的信号,会在信号完成或出错的时候自动释放,当然也可以手动释放Subscription
。
Subject
用RACSubject
类表示,是一个可以手动管理的信号,也就是我们要着重讨论的热信号。
Subject
可以理解为一个信号的mutable
版本。
不在block中处理应用逻辑,而是将这些block发送给一个共享的Subject来处理。RACSubject
是RACSignal
的子类。
RACReplaySubject
可以缓存Event,供以后的订阅者进行监听。
RACCommands
类,创建和订阅一个信号,并监控其状态。是对信号的封装,将一个信号的状态,以executionSignals
表示封装的信号,用executing
表示信号执行的状态,用enabled
表示信号是否可用,用errors
表示信号执行中的异常。
这个属性通常与UI控件结合在一起,用enabled
信号来控制控件是否可用,用executionSignals
来表示控件可用时要执行的操作。
RACMulticastConnect
类,表示可以在任意数量的订阅者中共享的一个信号。即是我们重点要讨论的第二个问题,冷热信号。
信号默认是冷的,即每当一个新的订阅者添加的时候,他们才开始处理事件,发送信号。这是一件可取的做法,数据会在每次订阅的时候刷新。但是对于有副作用的信号,或者操作消耗太多资源的信号(如网络请求),显然是有问题的。尤其是在RAC中,每次都信号进行的逻辑处理操作都是在订阅前一个信号。
通过RACSignal上的pulish
或者multicast:
方法创建一个这样的热信号。
RACSequence
表示表示一组信号,类似于NSArray
。RACSequence
表示一些列的信号,其有两个主要属性,id类型的head
,和RACSequence
类型的tail
,则遍历这个列表时类似于一种递归的方式。则就体现了RACSequence
的懒加载特性,如果这组Sequences
中的值没有被使用,那就不会去获取这个值。
一般用于遍历数组 :
NSArray *numbers = @[@(1),@(2),@(3),@(4),@(5)];
NSArray *result = [[[[numbers rac_sequence]
filter:^BOOL(NSNumber *value) {
return [value intValue] %2 ==0;
}] map:^id(NSNumber *value) {
long square = [value intValue] * [value intValue];
return @(square);
}] array];
NSLog(@"results = %@",result);
RACDisposables
表示对信号的取消操作和资源的释放操作。用于取消信号的订阅,释放信号。一般网络操作和后台处理的一些耗时操作,都应该提供RACDisposables
。调用RACDisposables
的dispose
方法,以取消正在订阅中的信号. 而当信号执行完成时,也会调用RACDisposables
.
RACScheduler
,提供一系列的执行队列,供信号按需执行操作或者发送结果。
RACScheduler
类似于GCD
,但是提供了取消功能,通过disposables
,而且只执行串行任务。对于使用immediateScheduler
创建的scheduler,不支持使用同步方法。可以看到设计是在通过一些限制来避免死锁的发生。
RAC提供了一些类在Stream中传递值。
RACTupleNil
对象表示)。一般用于表示多个信号聚合时,聚合信号传递的数据的值。materialize
方法,将三种信号合成一种发送给订阅者来统一处理。介绍几个简单的基础的操作符.
使用 subscribe
命令来根据信号的当前或未来的值设定响应操作:
ACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// Outputs: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
在冷信号中,每次订阅信号都会执行副作用.
使用 do...
命令,在不订阅信号的情况下,添加副作用.
__block unsigned subscriptions = 0;
RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];
// Does not output anything yet
loggingSignal = [loggingSignal doCompleted:^{
NSLog(@"about to complete subscription %u", subscriptions);
}];
// Outputs:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
NSLog(@"subscription %u", subscriptions);
}];
do
的操作会比 subscribe
要先执行.
这些操作,将一个信号流转变为一个新的信号流.
使用map:
命令,将信号的值进行替换.
// 以" "为间隔符创建sequence
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
// Contains: AA BB CC DD EE FF GG HH II
RACSequence *mapped = [letters map:^(NSString *value) {
return [value stringByAppendingString:value];
}];
使用filter:
命令,过滤信号值,过滤NO的信号值:
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// Contains: 2 4 6 8
RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {
return (value.intValue % 2) == 0;
}];
将多个信号流聚合成一个信号流
使用 concat:
命令,将一个信号的值接在另一个信号后面:
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];
使用flatten:
命令,对于信号中的信号,将 信号的值整合进一个新的信号流. 如下,连接Sequence
时:
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[ letters, numbers ].rac_sequence;
// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattened = [sequenceOfSequences flatten];
这里 sequenceOfSequences
,这个信号的值 是一个信号,使用flatten
,将信号中的信号的值 给取出来,作为信号的值,创建一个新的信号.
再聚一个聚合信号的例子:
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:letters];
[subscriber sendNext:numbers];
[subscriber sendCompleted];
return nil;
}];
RACSignal *flattened = [signalOfSignals flatten];
// Outputs: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
使用flatten
的目的,一般并不是为了这个效果,而是为了操作flattenMap:
.
flattenMap:
命令,用于转换多个信号流中的信号值输出一个新的信号流.从字面意思上来解释,这个操作就是, 先map
操作,处理信号,然后再flatten
操作,将信号中的信号提取出来作为一个完整的信号. 也就是说,block中return的是一个信号,而flattenMap
返回值是一个信号,将return中的信号flatten
后的一个完整的信号.还是用sequence
来举例:
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
RACSequence *extended = [numbers flattenMap:^(NSString *num) {
return @[ num, num ].rac_sequence;
}];
// Contains: 1_ 3_ 5_ 7_ 9_
RACSequence *edited = [numbers flattenMap:^(NSString *num) {
if (num.intValue % 2 == 0) {
return [RACSequence empty];
} else {
NSString *newNum = [num stringByAppendingString:@"_"];
return [RACSequence return:newNum];
}
}];
flattenMap
也用于将复数个信号的工作自动的结合在一起:
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
[[letters
flattenMap:^(NSString *letter) {
return [database saveEntriesForLetter:letter];
}]
subscribeCompleted:^{
NSLog(@"All database entries saved successfully.");
}];
将多个信号聚合成一个信号.
使用then:
命令, 一般为 signalA then:^{return signalB}
,表示 订阅signalA
的信号,但是忽略所有的next
事件,当completed
事件发送时,订阅B信号并返回B信号的事件.
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// The new signal only contains: 1 2 3 4 5 6 7 8 9
//
// But when subscribed to, it also outputs: A B C D E F G H I
RACSignal *sequenced = [[letters
doNext:^(NSString *letter) {
NSLog(@"%@", letter);
}]
then:^{
return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
}];
使用场景, 执行完前一个信号的所有副作用,然后开始另一个新信号,将其返回值作为真正的信号值 传递.
merge:
命令,如名字一样,合并信号,但是,根据的是信号值到来的顺序:
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *merged = [RACSignal merge:@[ letters, numbers ]];
// Outputs: A 1 B C 2
[merged subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
combineLastest:
和combineLastest:reduce:
两个方法,用来聚合信号,当多个信号中的任何一个信号发生变化时,都会取每个信号的最新值,组合成一个新的值发送.而两个方法的区别在于,前者传递的信号是一个RACTuple
对象,将多个信号的值封装在一个对象中,而后者在reduce
的block中来处理多个信号的值,将其整合成一个值返回.
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
combineLatest:@[ letters, numbers ]
// reduce的block的参数可以自行添加,但顺序要与combineLatest中信号的顺序相同.
reduce:^(NSString *letter, NSString *number) {
return [letter stringByAppendingString:number];
}];
// Outputs: B1 B2 C2 C3
[combined subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
一定要注意这里,聚合的所有信号都有第一个值后,这个聚合信号才会发送第一个值,所以这里letters
发送的第一个信号A
,不会在聚合信号combined
中出现.
然后就要与另外一个方法zip
进行比较.
压缩信号.与上面的combineLastest
有些相似,但是区别在于, zip
需要等待多个信号都有一个最新的值后,才会发送一个信号,而combine
中,任何一个信号有新的值后,都会发送信号.
举例,还是类似combineLatest
中的例子,只是将combineLatest
改为zip
,但是结果就变了.
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
zip:@[ letters, numbers ]
reduce:^(NSString *letter, NSString *number) {
return [letter stringByAppendingString:number];
}];
// Outputs: A1 B2 C3
[combined subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
在这里,最终输出地结果是A1
,B2
,C3
.
使用switchToLastest
来获取信号中的信号的值,传递next
和error
,但是不传递complete
:
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSubject *signalOfSignals = [RACSubject subject];
RACSignal *switched = [signalOfSignals switchToLatest];
// Outputs: A B 1 D
[switched subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
[signalOfSignals sendNext:letters];
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[signalOfSignals sendNext:numbers];
[letters sendNext:@"C"];
[numbers sendNext:@"1"];
[signalOfSignals sendNext:letters];
[numbers sendNext:@"2"];
[letters sendNext:@"D"];
当信号中的信号发送complete
时,由于switchToLastest
不接收这个completed
事件,所以 switchToLastest
的信号依旧继续处理.但是当信号中的信号发送error
时,会中断switchToLastest
的信号处理:
RACSubject *signalOfSignals = [RACSubject subject];
RACSignal *switched = [signalOfSignals switchToLatest];
[switched subscribeNext:^(id x) {
NSLog(@"Next : %@",x);
} error:^(NSError *error) {
NSLog(@"Error : %@",error);
} completed:^{
NSLog(@"Completed!");
}];
RACSubject *signalA = [RACSubject subject];
[signalOfSignals sendNext:signalA];
[signalA sendNext:@"A"];
[signalA sendCompleted];
[signalA sendNext:@"AA"];
RACSubject *signalB = [RACSubject subject];
[signalOfSignals sendNext:signalB];
[signalB sendNext:@"B"];
[signalB sendError:[NSError errorWithDomain:@"error" code:1 userInfo:nil]];
[signalB sendNext:@"BB"];
RACSubject *signalC = [RACSubject subject];
[signalOfSignals sendNext:signalC];
[signalC sendNext:@"C"];
上面这段代码示例中,输出结果为 :
Next : A
Next : B
Error : Error Domain=error Code=1 "(null)"
信号A发送完成后,AA的信号就无法发送给signalOfSignals
了,但是一个新的信号B还是能够继续发信号给signalOfSignals
的.但当信号B发送error后,这个signalOfSignals
会接受到这个error,从而结束订阅.
在RACSequence
中获取值,默认是延迟计算的:
NSArray *strings = @[ @"A", @"B", @"C" ];
RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
return [str stringByAppendingString:@"_"];
}];
只有真正使用时,才会进行计算,获取到真正的值;如通过sequence.head
才会去计算出A_
的值。
而且,只会计算一次,即多次访问sequence.head
,但[str stringByAppendingString:@"_"]
操作只会执行一次。
如果不需要这种延迟加载,而需要在以来开始的时候初始化整个数组,那就使用eagerSequence
这个属性。
计算操作是同步执行的,这需要注意一下。如果数组的计算操作是比较耗费时间的,可以通过接口signalWithScheduler:
来在一个队列中执行数组计算操作,并获取完成信号。
对于RACSequence
进行运算时,因为所做的运算一般是求得一个新的RACSequence
,而RACSequence
只会在第一次使用到值时才会进行计算:
NSArray *strings = @[ @"A", @"B", @"C" ];
RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
NSLog(@"%@", str);
return [str stringByAppendingString:@"_"];
}];
// Logs "A" during this call. 进行计算,执行block中方法,所以有log
NSString *concatA = sequence.head;
// Logs "B" during this call.
NSString *concatB = sequence.tail.head;
// Does not log anything. 已经完成计算,不会再输出log。
NSString *concatB2 = sequence.tail.head;
信号事件是连续的串行的。一个信号可以分发事件到任何一个线程,连续的事件可以被选择在不同的线程或者schedulers
.但是有些情况会要求事件必须在特殊的scheduler
上执行,如UI操作,就必须在主线程上进行。
RAC保证,不会有两个信号同时到达,所以不会有两个信号的事件同时在一个线程上被激活的可能。即在RAC中,当一个事件在处理过程中,不会有其他事件被分发。只有当事件被处理完成后才会有新的事件发送出去。
这就意味着 : 在-subscribeNext:error:completed:
回调中,不需要对变量进行加锁操作,因为事件处理全部是串行的。
订阅操作,始终执行在一个schedular
上。为了确保createSignal
和subscribe
方法执行的一致性表现,RAC会保证 消息操作的执行和 消息订阅的操作会在同一个scheduler
中。
如果在订阅时,执行代码+[RACScheduler currentScheduler]
无法获得一个RACScheduler
时,会将订阅和消息放在一个后台的RACScheduler
中执行。如果能够得到一个RACScheduler
则会在当前RACScheduler
中执行。
再说明一下这个[RACScheduler currentScheduler]
,这个函数会再 RACScheduler
中返回正确的Scheduler
,以及在主线程中返回+[RACScheduler mainThreadScheduler]
. 所以上面一段话的意思,就是如果在主线程或者在执行一个RACScheduler
中,订阅会发生在这个Scheduler
中,否则会发生在一个后台的Scheduler
中。
在RAC中,error
在语义上表示异常。当信号中发生一个Error信号时,会立即发送给所有相关的信号,并使整个消息链终止。
但这并表示 ,对于Error
的处理的操作符 ,如catch:
catchTo:
和materialize
这几个错误处理也会终止。
每一次对信号的订阅,都会触发副作用。原因很简单,因为这些信号是冷信号,冷信号会在订阅时执行。而需要注意的是,所有对信号的操作,都是订阅信号,并发送新的信号。
想要取消这种效果,那就是用热信号吧。
再次说明一下,冷信号的副作用效果,会产生许多问题,且难以发现,一定要注意。要理解冷热信号的区别。
当一个消息发送了completed
或error
事件时,这个subscription
会自动被释放。节省手动释放的操作。
而释放信号时,要对那些 文件操作或者网络操作等,进行资源释放和过程中断。
当一个方法或者属性返回一个RACSignal
类型的信号时,很难很快地理解一个信号的含义。
对于声明一个信号,有以下三个关键性的问题:
热信号且没有副作用 ,这种情况应该将信号作为一种属性。使用属性,表明对信号的订阅不需要进行初始化,而且添加新的订阅也不会改变这个用法。信号的属性一般被命名为 名字 + 事件
,如 textChanged
.
冷信号且无副作用 , 这种情况应该作为一个函数,且命名使用一个名词来表示,如currentText
. 一个名词的函数声明,表示了这个信号不会被一直持有,同时声明操作是发生在订阅时.如果信号发送了复数个值,需要在命名时表明这一点,如currentModels
有副作用的信号, 信号应该是以方法形式返回,并表示动作,如logIn
. 动词表明了这个函数不是静态的,调用者要小心调用时的副作用. 如果信号会发送一个或者多值,应该要再命名中表明值的含义,如loadConfiguration
和fetchLastestEvents
.
使用RAC书写代码时,在处理信号中得操作流很容易变得很重很多,大量的操作符与block聚集在一起,如果没有进行很好地格式化,那这段代码就将变得乱七八糟.所以,建议,在流的处理过程中,对操作符进行缩进 :
RACStream *result = [[[RACStream
zip:@[ firstStream, secondStream ]
reduce:^(NSNumber *first, NSNumber *second) {
return @(first.integerValue + second.integerValue);
}]
filter:^ BOOL (NSNumber *value) {
return value.integerValue >= 0;
}]
map:^(NSNumber *value) {
return @(value.integerValue + 1);
}];
在一个流中,使用一种类型来作为各个过程的信号值.虽然RAC中支持使用任何类型的值作为信号值来传递,但是在一个完整地流中,使用多种不同类型的值,会导致代码可读性降低,也会增加订阅者的负担,必须更加小心地去处理这个奇怪的信号.
不要持有RACStream
对象过长时间.持有一个RACStream
对象的同时,也会导致以来这个RACStream
对象的所有对象都被持有,无法正常释放,这将降低内存利用率.
例如 :一个RACSequence
对象在需要使用其head
属性时,可以持有这个对象,但当不再使用head
时,就应该抛弃这个RACSequence
了,如果需要之后的数据,可以持有其tail
属性而不是持有这个RACSequence
本身.
保持一个Stream
或者RACSignal
的订阅,会浪费性能和内存,如果一个信号的结果不需要使用,就应该丢弃这些信号.
我们可以使用take:
和takeUntil:
等方法,来做这种判断逻辑.这个方法会在逻辑判断不会在接收消息时,取消该信号订阅的堆栈,终止所有的依赖项的订阅.
可以将信号使用deliverOn:
在一个指定的Scheduler
上发送事件,如对于一些UI的操作,可以声明其在主线程中执行. 这个命令指的是subscribe的操作在指定的Scheduler
中执行,但是副作用还是在原始的线程中执行.
但是,尽量少得去切换Scheduler
,线程间的切换,会有不必要的延迟出现,而且会消耗CPU的性能. 所以deliverOn:
的操作,一般放在信号链的最后一级执行.
明确地说明一个信号有副作用. 我们应该避免信号的副作用,因为我们很难控制副作用的发生.
但这种场景还是需要的,所以RAC中提供了doNext:
doError:
和doCompleted
三个方法来提供明确地副作用的处理.
在热信号中,分享副作用.使用publish
和multicast
两个命令来让一个信号发布成一个热信号,变成RACMulticastConnection
对象.
每个RACStream
都有一个属性name
,来用于调试.而一个Stream
的description
的中会自动包含所有操作的列举出来.
RACSignal *signal = [[[RACObserve(self, username)
distinctUntilChanged]
take:3]
filter:^(NSString *newUsername) {
return [newUsername isEqualToString:@"joshaber"];
}];
NSLog(@"%@", signal);
如上面打印出来的结果是 :[[[RACObserve(self, username)] -distinctUntilChanged] -take: 3] -filter:
可以通过setNameWithFormat
来设置一个signal最开始的名称.RACSignal
也提供了logNext
,logError
,logCompleted
和logAll
这些方法,可以自动的在事件发生时打日志.
避免明确地 订阅和释放操作.而使用以下几个方法:
RAC()
和RACChannelTo()
宏,来绑定一个信号到对应的属性,而不是声明一个手动的改动来执行操作.rac_liftSelector:withSignals:
方法,当信号发生时会调用Selector
.takeUntil:
这样的方法,用来自动的释放一次订阅.尽量使用RAC提供的方法来操作信号来导出一个正确的符合效果的信号流,供订阅.
避免使用Subjects
. Subjects
是一个强力的工具,桥接代码与信号.但是过度使用会导致代码变得更加复杂.尽量少的使用热信号,建议:
createSignal:
的block进行初始化操作.subject
的情况,改用 combineLatest:
或zip:
方法来合并多个信号来实现.multicast
一个基础的信号来解决.RACCommand
or rac_signalForSelector
instead.如果要使用subjects
,应该将其作为一个信号链的基础,而不是中间的一个环节.
RAC中得内存管理是很复杂的,但是这样做的目的是,使用者不用持有信号来驱动信号发送的过程.
除了那些会长期存在的信号,会被以属性的形式持有,一般不要去持有信号.
当使用subscribeNext:error:completed:
订阅信号时,隐式地创建了一个RACSubscriber
对象.所以创建信号时使用的block所关联的对象会被订阅所持有.
在RAC的内存关联中,一个重要的注意事项就是, 订阅会在 completion 或是error时终止,订阅者也会被移除.
这样,信号的生命周期也就会跟随事件流的逻辑生命周期.
会有一些不会自行结束的信号存在,所以需要disposable
存在.
信号订阅的dispose
操作,会移除所有关联的订阅者,而且也会释放该信号所占有的资源.
有些信号是有self衍生出来的.如 RACObserve()
监听self
的一个属性时,在subscribeNext
使用self指针,就会形成一个引用环.
建议使用@weakify
和@strongify
这两个宏来处理指针. 当对象不能使用weak时,使用 __unsafe_unretained
或 @unsafeify
.
但很多时候,有一种更好地写法来解决循环指针的问题,如对于一般写法:
@weakify(self);
[RACObserve(self, username) subscribeNext:^(NSString *username) {
@strongify(self);
[self validateUsername];
}];
实际上我们可以这样写:
[self rac_liftSelector:@selector(validateUsername:) withSignals:RACObserve(self, username), nil];
或者这样写 :
RACSignal *validated = [RACObserve(self, username) map:^(NSString *username) {
// Put validation logic here.
return @YES;
}];
这里的内容主要是在学习 美团的技术分享文章,里面几张图也是直接从这里拿过来的.
然后我们再来讨论这个RAC中一个重点问题.也就是冷热信号.
冷热信号的起源来自于RX的Hot Observable
和Cold Observable
,两者的区别是:
产生热信号的原因,有些信号我们不想再订阅时就执行一次,而是全局共享一个信号.如网络请求的信号,我们并不希望在每次对网络请求结果信号进行订阅时,就执行一次新的网络请求.所以我们要将信号转换为热信号,以只执行一次网络访问,而结果可以供多次订阅共享.
热信号都属于一个类RACSubject
,这个类在RAC中表示一个可变的信号.我们写一段代码来演示一下其效果:
RACSubject *subject = [RACSubject subject];
RACSubject *replaySubject = [RACReplaySubject subject];
[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
// Subscriber 1
[subject subscribeNext:^(id x) {
NSLog(@"Subscriber 1 get a next value: %@ from subject", x);
}];
[replaySubject subscribeNext:^(id x) {
NSLog(@"Subscriber 1 get a next value: %@ from replay subject", x);
}];
// Subscriber 2
[subject subscribeNext:^(id x) {
NSLog(@"Subscriber 2 get a next value: %@ from subject", x);
}];
[replaySubject subscribeNext:^(id x) {
NSLog(@"Subscriber 2 get a next value: %@ from replay subject", x);
}];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
[subject sendNext:@"send package 1"];
[replaySubject sendNext:@"send package 1"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1.1 schedule:^{
// Subscriber 3
[subject subscribeNext:^(id x) {
NSLog(@"Subscriber 3 get a next value: %@ from subject", x);
}];
[replaySubject subscribeNext:^(id x) {
NSLog(@"Subscriber 3 get a next value: %@ from replay subject", x);
}];
// Subscriber 4
[subject subscribeNext:^(id x) {
NSLog(@"Subscriber 4 get a next value: %@ from subject", x);
}];
[replaySubject subscribeNext:^(id x) {
NSLog(@"Subscriber 4 get a next value: %@ from replay subject", x);
}];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
[subject sendNext:@"send package 2"];
[replaySubject sendNext:@"send package 2"];
}];
输出的结果是 :
2016-06-30 23:34:25.722 TestPods[17259:2788197] Subscriber 1 get a next value: send package 1 from subject
2016-06-30 23:34:25.723 TestPods[17259:2788197] Subscriber 2 get a next value: send package 1 from subject
2016-06-30 23:34:25.723 TestPods[17259:2788197] Subscriber 1 get a next value: send package 1 from replay subject
2016-06-30 23:34:25.724 TestPods[17259:2788197] Subscriber 2 get a next value: send package 1 from replay subject
2016-06-30 23:34:25.834 TestPods[17259:2788197] Subscriber 3 get a next value: send package 1 from replay subject
2016-06-30 23:34:25.834 TestPods[17259:2788197] Subscriber 4 get a next value: send package 1 from replay subject
2016-06-30 23:34:26.818 TestPods[17259:2788197] Subscriber 1 get a next value: send package 2 from subject
2016-06-30 23:34:26.818 TestPods[17259:2788197] Subscriber 2 get a next value: send package 2 from subject
2016-06-30 23:34:26.818 TestPods[17259:2788197] Subscriber 3 get a next value: send package 2 from subject
2016-06-30 23:34:26.819 TestPods[17259:2788197] Subscriber 4 get a next value: send package 2 from subject
2016-06-30 23:34:26.819 TestPods[17259:2788197] Subscriber 1 get a next value: send package 2 from replay subject
2016-06-30 23:34:26.819 TestPods[17259:2788197] Subscriber 2 get a next value: send package 2 from replay subject
2016-06-30 23:34:26.820 TestPods[17259:2788197] Subscriber 3 get a next value: send package 2 from replay subject
2016-06-30 23:34:26.820 TestPods[17259:2788197] Subscriber 4 get a next value: send package 2 from replay subject
根据时间线画图如下:
而如果是冷信号的情况的话,就有如下时间线:
可以发现,对于冷信号,类似于重播,每个订阅者都会观察到整个消息的处理过程.而对于subject
,类似于直播,多个订阅者接收到一个信号的事件,并且如果信号已经发送过的消息,错过就无法再次接收到.
而还有一个replaySubject
对象,将上面代码改写为该对象后,得到的效果图如下:
发现这个信号会保存之前发送过的信号,在新的对象订阅时,将之前的信号发送.
RACSubject
是支持RACSubscriber
协议的,热信号的实现就是通过这个RACSubject
来订阅一个冷信号,然后其他人在再来订阅这个RACSubject
.
观察以下代码:
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"Cold signal be subscribed.");
[[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{
[subscriber sendNext:@"A"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
[subscriber sendNext:@"B"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{
[subscriber sendCompleted];
}];
return nil;
}];
RACSubject *subject = [RACSubject subject];
NSLog(@"Subject created.");
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
[coldSignal subscribe:subject];
}];
[subject subscribeNext:^(id x) {
NSLog(@"Subscriber 1 recieve value:%@.", x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
[subject subscribeNext:^(id x) {
NSLog(@"Subscriber 2 recieve value:%@.", x);
}];
}];
输出结果为 :
2016-07-01 16:32:04.829 TestPods[17618:2842318] Subject created.
2016-07-01 16:32:07.029 TestPods[17618:2842318] Cold signal be subscribed.
2016-07-01 16:32:08.669 TestPods[17618:2842318] Subscriber 1 recieve value:A.
2016-07-01 16:32:10.319 TestPods[17618:2842318] Subscriber 1 recieve value:B.
2016-07-01 16:32:10.319 TestPods[17618:2842318] Subscriber 2 recieve value:B.
得到下图:
这样自行处理热信号,过于简单,有一些问题,如当Subject
取消订阅时,不能取消对应的冷信号的订阅.而RAC中有对冷信号转换为热信号的标准接口 :
//创建一个普通的热信号
-(RACMulticastConnection *)publish;
// 创建一个热信号,并将值发送给一个RACSubject对象
-(RACMulticastConnection *)multicast:(RACSubject *)subject;
// 创建重播热信号,并立即订阅,信号使用RACReplaySubject,即会重播已经发送的所有信号
-(RACSignal *)replay;
// 创建一个热信号,并立即订阅,使用RACReplaySubject,但设置capacity为1,即只会重播一次信号
-(RACSignal *)replayLast;
// 创建一个热信号.但不立即订阅,等待其他人订阅这个热信号.
-(RACSignal *)replayLazily;
RAC提供以上五个方法中,最重要的就是 - (RACMulticastConnection *)multicast:(RACSubject *)subject
,其他都是基于这个实现的.看一下这个函数的实现:
/// implementation RACSignal (Operations)
-(RACMulticastConnection *)multicast:(RACSubject *)subject {
[subject setNameWithFormat:@"[%@] -multicast: %@", self.name, subject.name];
RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
return connection;
}
/// implementation RACMulticastConnection
-(id)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
NSCParameterAssert(source != nil);
NSCParameterAssert(subject != nil);
self = [super init];
if (self == nil) return nil;
_sourceSignal = source;
_serialDisposable = [[RACSerialDisposable alloc] init];
_signal = subject;
return self;
}
#pragma mark Connecting
-(RACDisposable *)connect {
BOOL shouldConnect = OSAtomicCompareAndSwap32Barrier(0, 1, &_hasConnected);
if (shouldConnect) {
self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
}
return self.serialDisposable;
}
-(RACSignal *)autoconnect {
__block volatile int32_t subscriberCount = 0;
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
OSAtomicIncrement32Barrier(&subscriberCount);
RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber];
RACDisposable *connectionDisposable = [self connect];
return [RACDisposable disposableWithBlock:^{
[subscriptionDisposable dispose];
if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) {
[connectionDisposable dispose];
}
}];
}]
setNameWithFormat:@"[%@] -autoconnect", self.signal.name];
}
简单说明一下流程:
multicast:
方法将 signal
和subject
作为参数创建一个RACMulticastConnect
的热信号.RACMulticastConnection
的 initWithSourceSignal: subject :
初始化时,创建一个RACSerialDisposable
对象用于取消订阅.RACMulticastConnection
对象调用connect
时,会判断热信号是否已经与原始信号连接在一起了,如果没有的话,则用_signal
这个对象订阅sourceSignal
._signal
是一个RACSubject
的对象,所以是一个热信号,会在connect
时订阅sourceSignal
,然后传递事件.然后再来看一下 另外4个方法的实现:
/// implementation RACSignal (Operations)
-(RACMulticastConnection *)publish {
RACSubject *subject = [[RACSubject subject] setNameWithFormat:@"[%@] -publish", self.name];
RACMulticastConnection *connection = [self multicast:subject];
return connection;
}
-(RACSignal *)replay {
RACReplaySubject *subject = [[RACReplaySubject subject] setNameWithFormat:@"[%@] -replay", self.name];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
-(RACSignal *)replayLast {
RACReplaySubject *subject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"[%@] -replayLast", self.name];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
-(RACSignal *)replayLazily {
RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];
return [[RACSignal
defer:^{
[connection connect];
return connection.signal;
}]
setNameWithFormat:@"[%@] -replayLazily", self.name];
}
publish
,创建一个普通的RACSubject
对象,一个普通的热信号.replay
,创建一个RACReplySubject
对象的热信号,这个热信号会重播之前的历史信号值.replayLast
,以RACReplySubject
对象创建一个热信号,但是设置Capacity
为1,也就是只重发最后一次的历史值.replayLazily
,使用defer
命令,只在信号真正被订阅时,才去连接热信号.RACCommand
对信号进行封装,是信号在某些事件发生时触发,一般与UI操作结合.RACCommand
是对信号的一层非常漂亮的封装,用事件来触发信号执行的设计,使RACCommand
适用于许多情况,不仅仅是UI操作,其内部对信号热化,以及监控信号状态,可以用于一些耗时操作的信号化,如网络请求.
我们首先来看一下,RACCommand
提供的接口:
在调用execute:
后,一个返回信号的信号. 这个信号是一个信号的信号,封装了workSignal
(本文之后用workSignal
这个词来形容用户于RACCommand
中要执行的信号).当receiver
是enable
的时候,发送信号.RACCommand
将正在执行的信号封装在这里,作为executionSignals
的返回值,而信号中的error
被发往RACCommand
的errors
的信号中了,而遇到error信号时,executionSignals
的信号会返回一个completed
信号以标记事件完成:
RACCommand *comd = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:[NSString stringWithFormat:@"INPUT :%@",input]];
[subscriber sendError:[NSError errorWithDomain:@"ddd" code:1 userInfo:nil]];
return nil;
}];
}];
[[comd.executionSignals switchToLatest]
subscribeNext:^(id x) {
NSLog(@"Next : %@",x);
} error:^(NSError *error) {
NSLog(@"Next : %@",error);
} completed:^{
NSLog(@"Completed");
}];
[comd.executionSignals subscribeNext:^(id x) {
NSLog(@"Signal %@ ",x);
}];
[comd.errors subscribeNext:^(id x) {
NSLog(@"error 里才有? %@",x);
}];
[comd execute:@"hello world"];
如上代码,最终输出为:
2016-07-02 20:39:13.068 TestPods[23173:3420416] Signal <RACDynamicSignal: 0x7ff2d9406d30> name:
2016-07-02 20:39:13.068 TestPods[23173:3420416] Next : INPUT :hello world
2016-07-02 20:39:13.069 TestPods[23173:3420416] Completed
2016-07-02 20:39:13.069 TestPods[23173:3420416] error 里才有? Error Domain=ddd Code=1 "(null)"
但是,可以通过materialize
方法来获取这个inner errors
.
RACCommand
用executionSignals
封装workSignal
,workSignal
的执行必须调用excute
来驱动.内部信号的订阅操作会在主线程中执行.
表示当前命令是否正在执行的信号.RACCommand
被excute
调用后,且在信号终止前,这个信号会发送YES
.当信号结束了,会发送NO
.
RACCommand
中的信号全是热信号,可以随便订阅,且所有订阅结果都在主线程中执行. 这个executing
使用的是replayLast
,所以订阅时就会获得当前执行的状态.
上面说到,当workSignal
失败时,也会发送一个Completed
的事件,而这个事件也是executing
正确处理信号状态的前提.所以在workSignal
一定要正确处理信号的状态,在信号处理完成或者失败的时候,要正确地发送Completed
或者error
事件.
决定 workSignal
是否可以执行的 信号.
只在两种情况下返回 NO :
RACCommand
是使用initWithEnabled:signalBlock:
初始化,即设置了一个enabledSignal
,而且这个信号当前返回NO
.allowsConcurrentExecution
属性设置为 NO
,且这个信号正在执行中.除了这两种情况,一般都返回YES.
这个信号 一般用于操作UI控件的状态,如UIButton
的状态,当未满足某些判断逻辑时,enabled
为NO,同时设置UIButton
不可点击.
workSignal
的错误事件被转发到这里.
注意,这里错误订阅需要订阅Next
事件,而不是Error
事件,因为在RACSteam
中,错误事件的发生会关闭信号流.
workSignal
信号是否支持并发.
默认是NO
,即RACCommand
封装的workSignal
同时只能有一个信号正在执行.
以一个返回workSignal
的signalBlock
初始化,没有enableBlock
.
初始化时,设置 signalBlock
,和enabledSignal
.
对于enabledSignal
,初始状态是YES
, 即可用.
对于signalBlock
,其中返回一个workSignal
,这个信号可以带一个输入值,返回的workSignal
会以 executionSignals
的值的形式被订阅者获取,workSignal
会被加热,而executionSignals
本身也是一个热信号.
当RACCommand
是enable的时候,调用execute
,
signalBlock
中的初始化操作,以输入值input
来初始化一个新的workSignal
workSignal
,使用RACReplaySubject
.workSignal
在executionSignals
上发送.switchToLatest
来获取这个workSignal
,并在主线程中订阅事件.这个函数是有返回值,返回值也是一个RACSignal
,返回值信号为 加热后的workSignal
.而如果command的enbaled信号为No时,会返回一个 发送一个RACCommandErrorNotEnabled
错误的信号.
在RACCommand
中,封装的所有信号都是 热信号,订阅事件会发生在主线程上.
我们将workSignal
考虑为一个任务,而RACCommand
为这个任务提供了非常方便的 状态监控,并发控制,参数传递等功能. 我们可以将这样的一个任务以信号的形式融入RAC
的世界中,而不用自己去考虑冷热信号,不用去考虑状态控制.RACCommand
是我们使用RAC
的一个强力而方便的工具.
我们可以将很多任务通过RACCommand
进行封装,如网络请求,类似下面这种封装 :
RACCommand *logginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id params) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASEURL]
sessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[manager POST:LOGINURL parameters:params 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 nil;
}];
}];
像这样通过RACCommand
封装网络请求,通过excute:
传入一次网络请求的参数,伴随着RACCommand
完整的热信号保证,和状态控制,使用ReactiveCocoa
会变得更加方便高效.
标签:
原文地址:http://blog.csdn.net/luo_xianming/article/details/51821163