ReactiveCocoa 学习 - 3

RAC指南与最佳实践

2016-06-10 | 阅读

Design Guidelines

关于RACSequence的一些特性

延迟加载

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;

关于RACSignal一些需要注意的地方

Signal events are serialized

信号事件是连续的串行的。一个信号可以分发事件到任何一个线程,连续的事件可以被选择在不同的线程或者schedulers.但是有些情况会要求事件必须在特殊的scheduler上执行,如UI操作,就必须在主线程上进行。

RAC保证,不会有两个信号同时到达,所以不会有两个信号的事件同时在一个线程上被激活的可能。即在RAC中,当一个事件在处理过程中,不会有其他事件被分发。只有当事件被处理完成后才会有新的事件发送出去。

这就意味着 : 在-subscribeNext:error:completed:回调中,不需要对变量进行加锁操作,因为事件处理全部是串行的。

Subscription will always occur on a scheduler

订阅操作,始终执行在一个schedular上。为了确保createSignalsubscribe方法执行的一致性表现,RAC会保证 消息操作的执行和 消息订阅的操作会在同一个scheduler中。

如果在订阅时,执行代码+[RACScheduler currentScheduler]无法获得一个RACScheduler时,会将订阅和消息放在一个后台的RACScheduler中执行。如果能够得到一个RACScheduler则会在当前RACScheduler中执行。

再说明一下这个[RACScheduler currentScheduler],这个函数会再 RACScheduler中返回正确的Scheduler,以及在主线程中返回+[RACScheduler mainThreadScheduler]. 所以上面一段话的意思,就是如果在主线程或者在执行一个RACScheduler中,订阅会发生在这个Scheduler中,否则会发生在一个后台的Scheduler中。

Errors are propagated immediately

在RAC中,error在语义上表示异常。当信号中发生一个Error信号时,会立即发送给所有相关的信号,并使整个消息链终止。

但这并表示 ,对于Error的处理的操作符 ,如catch: catchTo:materialize 这几个错误处理也会终止。

Side effects occur for each subscription

每一次对信号的订阅,都会触发副作用。原因很简单,因为这些信号是冷信号,冷信号会在订阅时执行。而需要注意的是,所有对信号的操作,都是订阅信号,并发送新的信号。

想要取消这种效果,那就是用热信号吧。

再次说明一下,冷信号的副作用效果,会产生许多问题,且难以发现,一定要注意。要理解冷热信号的区别。

Subscriptions are automatically disposed upon completion or error

当一个消息发送了completederror事件时,这个subscription会自动被释放。节省手动释放的操作。

而释放信号时,要对那些 文件操作或者网络操作等,进行资源释放和过程中断。

Best practices

Use descriptive declarations for methods and properties that return a signal

当一个方法或者属性返回一个RACSignal类型的信号时,很难很快地理解一个信号的含义。

对于声明一个信号,有以下三个关键性的问题:

  1. 信号是热信号还是冷信号?
  2. 信号有一个值还是没有值还是多个值?
  3. 信号有副作用吗?

热信号且没有副作用 ,这种情况应该将信号作为一种属性。使用属性,表明对信号的订阅不需要进行初始化,而且添加新的订阅也不会改变这个用法。信号的属性一般被命名为 名字 + 事件 ,如 textChanged.

冷信号且无副作用 , 这种情况应该作为一个函数,且命名使用一个名词来表示,如currentText. 一个名词的函数声明,表示了这个信号不会被一直持有,同时声明操作是发生在订阅时.如果信号发送了复数个值,需要在命名时表明这一点,如currentModels

有副作用的信号, 信号应该是以方法形式返回,并表示动作,如logIn. 动词表明了这个函数不是静态的,调用者要小心调用时的副作用. 如果信号会发送一个或者多值,应该要再命名中表明值的含义,如loadConfigurationfetchLastestEvents.

Indent stream operations consistently

使用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);
    }];

Use the same type for all the values of a stream

在一个流中,使用一种类型来作为各个过程的信号值.虽然RAC中支持使用任何类型的值作为信号值来传递,但是在一个完整地流中,使用多种不同类型的值,会导致代码可读性降低,也会增加订阅者的负担,必须更加小心地去处理这个奇怪的信号.

Avoid retaining streams for too long

不要持有RACStream对象过长时间.持有一个RACStream对象的同时,也会导致以来这个RACStream对象的所有对象都被持有,无法正常释放,这将降低内存利用率.

例如 :一个RACSequence对象在需要使用其head属性时,可以持有这个对象,但当不再使用head时,就应该抛弃这个RACSequence了,如果需要之后的数据,可以持有其tail属性而不是持有这个RACSequence本身.

Process only as much of a stream as needed

保持一个Stream或者RACSignal的订阅,会浪费性能和内存,如果一个信号的结果不需要使用,就应该丢弃这些信号.

我们可以使用take:takeUntil:等方法,来做这种判断逻辑.这个方法会在逻辑判断不会在接收消息时,取消该信号订阅的堆栈,终止所有的依赖项的订阅.

Deliver signal events onto a known scheduler

可以将信号使用deliverOn:在一个指定的Scheduler上发送事件,如对于一些UI的操作,可以声明其在主线程中执行. 这个命令指的是subscribe的操作在指定的Scheduler中执行,但是副作用还是在原始的线程中执行.

但是,尽量少得去切换Scheduler,线程间的切换,会有不必要的延迟出现,而且会消耗CPU的性能. 所以deliverOn:的操作,一般放在信号链的最后一级执行.

Make the side effects of a signal explicit

明确地说明一个信号有副作用. 我们应该避免信号的副作用,因为我们很难控制副作用的发生.

但这种场景还是需要的,所以RAC中提供了doNext: doError:doCompleted三个方法来提供明确地副作用的处理.

Share the side effects of a signal by multicasting

在热信号中,分享副作用.使用publishmulticast两个命令来让一个信号发布成一个热信号,变成RACMulticastConnection对象.

Debug streams by giving them names

每个RACStream都有一个属性name,来用于调试.而一个Streamdescription的中会自动包含所有操作的列举出来.

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,logCompletedlogAll这些方法,可以自动的在事件发生时打日志.

Avoid explicit subscriptions and disposal

避免明确地 订阅和释放操作.而使用以下几个方法:

  • 使用RAC()RACChannelTo()宏,来绑定一个信号到对应的属性,而不是声明一个手动的改动来执行操作.
  • 使用rac_liftSelector:withSignals:方法,当信号发生时会调用Selector.
  • takeUntil:这样的方法,用来自动的释放一次订阅.

尽量使用RAC提供的方法来操作信号来导出一个正确的符合效果的信号流,供订阅.

Avoid using subjects when possible

避免使用Subjects. Subjects是一个强力的工具,桥接代码与信号.但是过度使用会导致代码变得更加复杂.尽量少的使用热信号,建议:

  • 对于要给信号中得值初始化的情况,使用createSignal:的block进行初始化操作.
  • 对于分发一个中间的结果给subject的情况,改用 combineLatest:zip:方法来合并多个信号来实现.
  • 对于想要做出一个热信号供多个对象订阅的情况,通过multicast一个基础的信号来解决.
  • instead of implementing an action method which simply controls a subject,use a RACCommand or rac_signalForSelector instead.

如果要使用subjects,应该将其作为一个信号链的基础,而不是中间的一个环节.