ReactiveCocoa 知识点整理

前言

本文仅作为个人对于 ReactiveCocoa 学习与使用的总结,文章绝大部分内容来自网络,详情请见参考链接。

Last updated on 2017.03.24


FRP 思想

1
2
3
4
5
6
a = 2
b = 2
c = a + b // c is 4

b = 3
// now what is the value of c?

简单来讲,FRP 是基于异步事件流进行编程的一种编程范式。针对离散事件序列进行有效的封装,利用函数式编程的思想,满足响应式编程的需要。


ReactiveCocoa 常见类

信号源相关

RACStream

An abstract class representing any stream of values.

RACStream 是一个抽象类,是以 Monad(函数式编程语言)的概念为依据进行设计的,它代表的就是一个 Monad。有了 Monad 作为基石后,许多基于流的操作就可以被建立起来了,比如 mapfilterzip 等。

RACSignal

可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。可以在水龙头上加一个过滤嘴(filter),不符合的不让通过,也可以加一个改动装置,把球改变成符合自己的需求(map)。也可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),这样只要其中的一个水龙头有玻璃球出来,这个新合并的水龙头就会得到这个球。

RACSignal 代表的是将来会被传递的值,它是一种 push-driven 的流。RACSignal 可以向订阅者发送三种不同类型的事件:

  • nextRACSignal 通过 next 事件向订阅者传送新的值,并且这个值可以为 nil
  • errorRACSignal 通过 error 事件向订阅者表明信号在正常结束前发生了错误;
  • completedRACSignal 通过 completed 事件向订阅者表明信号已经正常结束,不会再有后续的值传送给订阅者。

注意,ReactiveCocoa 中的值流只包含正常的值,即通过 next 事件传送的值,并不包括 errorcompleted 事件,它们需要被特殊处理。通常情况下,一个信号的生命周期是由任意个 next 事件和一个 error 事件或一个 completed 事件组成的。

RACSignalSubscription 过程概括起来可以分为三个步骤:

  1. [RACSignal createSignal] 来获得 signal
  2. [signal subscribeNext:] 来获得 subscriber,然后进行subscription
  3. 进入 didSubscribe ,通过 [subscriber sendNext:] 来执行 next block

RACSubject

RACSubject 代表的是可以手动控制的信号,我们可以把它看作是 RACSignal 的可变版本,就好比 NSMutableArrayNSArray 的可变版本一样。RACSubject 继承自 RACSignal ,所以它可以作为信号源被订阅者订阅,同时,它又实现了 RACSubscriber 协议,所以它也可以作为订阅者订阅其他信号源,这个就是 RACSubject 为什么可以手动控制的原因。

实际使用中,在 MVVM 中使用 RACSubject 可以非常方便地实现统一的错误处理逻辑。比如,我们可以在 viewModel 的基类中声明一个 RACSubject 类型的属性 errors ,然后在 viewController 的基类中编写统一的错误处理逻辑:

1
2
3
[self.viewModel.errors subscribeNext:^(NSError *error) {
// 错误处理逻辑
}

RACCommand

A command is a signal triggered in response to some action, typically UI-related.

RACCommand 通常用来表示某个 Action 的执行,比如点击Button。它有几个比较重要的属性:executionSignals / errors / executing。

  • executionSignals :是 signal of signals ,如果直接 subscribe 的话会得到一个 signal ,而不是我们想要的 value,所以一般会配合 switchToLatest
  • errors :跟正常的 signal 不一样,RACCommand 的错误不是通过 sendError 来实现的,而是通过 errors 属性传递出来的。
  • executing :表示该 command 当前是否正在执行。

RACSequence

Represents an immutable sequence of values. Unless otherwise specified, the sequences’ values are evaluated lazily on demand. Like Cocoa collections, sequences cannot contain nil.

RACSequence 代表的是一个不可变的值的序列,与 RACSignal 不同,它是 pull-driven 类型的流。从严格意义上讲,RACSequence 并不能算作是信号源,因为它并不能像 RACSignal 那样,可以被订阅者订阅,但是它与 RACSignal 之间可以非常方便地进行转换。

因此,我们可以非常方便地使用 RACSequence 来实现集合的链式操作,直到得到你想要的最终结果为止,常用的使用场景为「字典转模型」。

注意RACSequence 会涉及到性能与效率的问题。

订阅者相关

RACSubscriber

Represents any object which can directly receive values from a RACSignal.

订阅者对信号源的一次订阅过程可以抽象为:通过 RACSignal-subscribe: 方法传入一个订阅者,并最终返回一个 RACDisposable 对象的过程。

注意:在 ReactiveCocoa 中并没有专门的类 RACSubscription 来代表一次订阅,而间接地使用 RACDisposable 来充当这一角色。因此,一个 RACDisposable 对象就代表着一次订阅,并且我们可以用它来取消这次订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
trueNSCParameterAssert(nextBlock != NULL);
true
trueRACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
truereturn [self subscribe:o];
}

+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
trueRACSubscriber *subscriber = [[self alloc] init];

truesubscriber->_next = [next copy];
truesubscriber->_error = [error copy];
truesubscriber->_completed = [completed copy];

truereturn subscriber;
}

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
trueNSCAssert(NO, @"This method must be overridden by subclasses");
truereturn nil;
}

调度器相关

RACScheduler

Schedulers are used to control when and where work is performed.

RACSchedulerReactiveCocoa 中就是扮演着调度器的角色,本质上,它就是用 GCD 的串行队列来实现的,并且支持取消操作。是的,在 ReactiveCocoa 中,并没有使用到 NSOperationQueueNSRunloop 等技术,RACScheduler 也只是对 GCD 的简单封装而已。

清洁工相关

RACDisposable

A disposable encapsulates the work necessary to tear down and cleanup a subscription.

RACDisposableReactiveCocoa 中就充当着清洁工的角色,它封装了取消和清理一次订阅所必需的工作。它有一个核心的方法 -dispose ,调用这个方法就会执行相应的清理工作,这有点类似于 NSObject-dealloc 方法。


ReactiveCocoa 常见用法

代替代理

  • rac_signalForSelector 用户代替代理

代替 KVO

  • rac_valuesAndChangesForKeyPath 用于监听某个对象的某个属性的改变

代替事件监听

  • rac_signalForControlEvents 用于监听某个事件

代替通知

  • rac_addObserverForName 用于监听某个通知,且不需要在 - (void)dealloc 中移除监听

监听文本框文字改变

  • rac_textSignal 用于监听文本框文字变化

代替手势

  • rac_gestureSignal用于监听手势操作

多个请求完成时,再执行后继操作

  • rac_liftSelector:withSignalsFromArray:Signals 当传入的 Signals,每一个 Signal 都至少 sendNext 过一次,就会去触发第一个 selector 参数的方法。

信号的相关操作

  • bind :函数会返回一个新的信号 N。整体思路是对原信号 O 进行订阅,每当信号 O 产生一个值就将其转变成一个中间信号 M ,并马上订阅 M ,之后将信号M的输出作为新信号 N 的输出。
  • map \ flattenMap :用于把源信号内容映射成新的内容(信号)。
  • concat :组合,按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号。
  • then :用于连接两个信号,当第一个信号完成,才会连接 then 返回的信号。
  • merge :把多个信号合并为一个信号,任何一个信号有新值的时候就会调用。
  • zipWith :把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的 next 事件。
  • combineLatest :将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的 signal 至少都有过一次 sendNext ,才会触发合并的信号。
  • reduce :聚合,用于信号发出的内容是元组,把信号发出元组的值聚合成一个值。
  • filter :过滤信号,使用它可以获取满足条件的信号。
  • ignore :忽略某些值的信号,使用 RACObserve 时可配合使用,其实现由 filter 完成。
  • distinctUntilChanged :实现是用 bind 来完成的,每次变换中都记录一下原信号上一次发送过来的值,并与这一次进行比较,如果是相同的值,就「吞」掉,返回 empty 信号。只有和原信号上一次发送的值不同,变换后的新信号才把这个值发送出来。
  • take :从开始一共取 N 次的信号。
  • takeLast :取最后 N 次的信号,前提条件:订阅者必须调用完成,因为只有完成,才知道总共有多少信号。
  • takeUntil :获取信号直到某个信号执行完成。
  • skip :跳过几个信号,不接受。
  • switchToLatest :用于 signalOfSignals(信号的信号),有时候信号也会发出信号,会在 signalOfSignals 中,获取 signalOfSignals 发送的最新信号。
  • doNext :执行 next 之前,会先执行这个 Block 。
  • doCompleted :执行 sendCompleted 之前,会先执行这个Block 。
  • timeout :超时,可以让一个信号在一定的时间后,自动报错。
  • interval :定时:每隔一段时间发出信号。
  • delay :延迟发送 next
  • retry :重试,只要失败,就会重新执行创建信号中的 block ,直到成功。
  • replay :重放,当一个信号被多次订阅,反复播放内容。
  • throttle :节流,当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。

ReactiveCocoa 常见宏

  • RAC(TARGET, ...) 用于绑定某个对象的某个属性
  • RACObserve(TARGET, KEYPATH) 用于监听某个对象的某个属性,返回的是信号
  • @weakify(Obj) & @strongify(Obj) 配套使用

注意事项

Side Effect

Side effects occur for each subscription by default, but there are certain situations where side effects should only occur once – for example, a network request typically should not be repeated when a new subscriber is added.

如果某个信号被多个 subscriber 订阅,那么它的 didSubscribe 会被多次调用。

如果想要避免这种情况的发生,可以使用 reply / replayLast / replayLazily 方法,它们的作用是保证 signal 只被触发一次,然后把 sendNext:value 给缓存起来,下一次再有新的 subscriber 时,直接发送缓存的 value

其内部实现依赖:- (RACMulticastConnection *)multicast:(RACSubject *)subject; 这个方法。

Cell 重用

RAC 给 UITableViewCell 提供了一个方法: rac_prepareForReuseSignal ,它的作用是当 Cell 即将要被重用时,告诉 Cell 。想象 Cell 上有多个 Button ,Cell 在初始化时给每个 Button 都 addTarget:action:forControlEvents ,被重用时需要先移除这些 target ,下面这段代码就可以很方便地解决这个问题:

1
2
3
4
5
6
[[[self.cancelButton
truerac_signalForControlEvents:UIControlEventTouchUpInside]
truetakeUntil:self.rac_prepareForReuseSignal]
truesubscribeNext:^(UIButton *x) {
true// do other things
}];

Strong / Weak Dance

因为 RAC 很多操作都是在 Block 中进行的,所以最常见的问题便是「循环引用」,所以需要通过 @weakify@strongify 来消除循环引用。

注意:事实上 RACObserve(TARGET, KEYPATH) 总是会引用 self ,即使 target 不是 self ,所以只要有 RACObserve 的地方都要使用 @weakify / @strongify

flattenMap 与 map 的区别

  • flattenMap 中的 block 返回信号。
  • map 中的 block 返回对象。
  • map 的实现是用了 flattenMap 函数来实现的。把 map 的入参闭包,放到了 flattenMap 的返回值中。
  • 开发中,如果信号发出的值不是信号,映射一般使用 map
  • 开发中,如果信号发出的值是信号,映射一般使用 flattenMap
  • signalOfsignalsflattenMap

冷信号与热信号的区别

  • Hot Observable 是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而 Cold Observable 是被动的,只有当你订阅的时候,它才会发布消息。
  • Hot Observable 可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而 Cold Observable 只能一对一,当有不同的订阅者,消息是重新完整发送。
  • Subject 类似「直播」,错过了就不再处理。而 Signal 类似「点播」。
  • RACSubject 及其子类是热信号,RACSignal 排除 RACSubject 类以外的是冷信号。
  • RACSubject 会持有订阅者(因为 RACSubject 是热信号,为了保证未来有事件发送的时候,订阅者可以收到信息,所以需要对订阅者保持状态,做法就是持有订阅者),而 RACSignal 不会持有订阅者。

其他

  • 当一个 signal 被一个 subscriber subscribe 后,这个 subscriber 何时会被移除?答案是:当 subscribersendCompletesendError 时,或者手动调用 [disposable dispose]

  • replaymulticast 的一个特殊 case 而已。

  • subscriberdispose 后,所有该 subscriber 相关的工作都会被停止或取消,如 http 请求,资源也会被释放。

  • Errors 有优先权,如果有多个 signals 被同时监听,只要其中一个 signal sendError,那么 error 就会立刻被传送给 subscriber ,并导致 signals 终止执行。相当于Exception

  • 使用 RACSubject ,如果进行了 map 操作,那么一定要发送完成信号,不然会内存泄漏。

  • 任何的信号转换即是对原有的信号进行订阅从而产生新的信号。


系列文章

  1. 美团点评技术团队
  2. 一缕殇流化隐半边冰霜
  3. Draveness

参考链接

  1. ReactiveCocoa v2.5 源码解析之架构总览
  2. ReactiveCocoa2实战
  3. 说说ReactiveCocoa 2
  4. iOS函数响应式编程以及ReactiveCocoa的使用
  5. 最快让你上手ReactiveCocoa之基础篇
  6. 最快让你上手ReactiveCocoa之进阶篇
  7. ReactiveCocoa 讨论会
我知道是不会有人点的,但万一有人想不开呢?