ReactiveCocoa笔记(一)—— 框架概览

对于一个应用来说,绝大部分的时间都是在等待某些事件的发生或响应某些状态的变化,比如用户的触摸事件、应用进入后台、网络请求成功刷新界面等等,而维护这些状态的变化,常常会使代码变得非常复杂,难以扩展。而 ReactiveCocoa 给出了一种非常好的解决方案,它使用信号来代表这些异步事件,提供了一种统一的方式来处理所有异步的行为,包括代理方法、block 回调、target-action 机制、通知、KVO 等:
然而,这些还只是 ReactiveCocoa 的冰山一角,它真正强大的地方在于我们可以对这些不同的信号进行任意地组合和链式操作,从最原始的输入 input 开始直至得到最终的输出 output为止

ReactiveCocoa解决的问题:

  1. 传统iOS开发过程中,状态以及状态之间依赖过多的问题
  2. 传统MVC架构的问题:Controller比较复杂,可测试性差
  3. 提供统一的消息传递机制
  4. RAC使用信号监听,类似OC中的KVO机制,使用Block聚合代码逻辑,所以被称为:函数响应式编程。

ReactiveCocoa 的问题

RAC 在应用中大量使用了 block,由于 Objective-C 语言的内存管理是基于引用计数 的,为了避免循环引用问题,在 block 中如果要引用 self,需要使用@weakify(self)和@strongify(self)来避免强引用。另外,在使用时应该注意 block 的嵌套层数,不恰当的滥用多层嵌套 block 可能给程序的可维护性带来灾难。

有些地方很容易被忽略,比如RACObserve(thing, keypath),看上去并没有引用self,所以在subscribeNext时就忘记了weakify/strongify。但事实上RACObserve总是会引用self,即使target不是self,所以只要有RACObserve的地方都要使用weakify/strongify。

ReactiveCocoa 的理解

leezhong博客

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

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
// 代理方法
[[self
rac_signalForSelector:@selector(webViewDidStartLoad:)
fromProtocol:@protocol(UIWebViewDelegate)]
subscribeNext:^(id x) {
// 实现 webViewDidStartLoad: 代理方法
}];

// target-action
[[self.avatarButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(UIButton *avatarButton) {
// avatarButton 被点击了
}];

// 通知
[[[NSNotificationCenter defaultCenter]
rac_addObserverForName:kReachabilityChangedNotification object:nil]
subscribeNext:^(NSNotification *notification) {
// 收到 kReachabilityChangedNotification 通知
}];

// KVO
[RACObserve(self, username) subscribeNext:^(NSString *username) {
// 用户名发生了变化
}];

总结图示

  • 信号源:RACStream 及其子类;
  • 订阅者:RACSubscriber 的实现类及其子类;
  • 调度器:RACScheduler 及其子类;
  • 清洁工:RACDisposable 及其子类。

ReactiveCocoa v2.5 源码解析之架构总览 - 雷纯锋的技术博客

什么是ReactiveCocoa?

ReactiveCocoa(其简称为RAC)是由Github 开源的一个应用于iOS和OS X开发的新框架。RAC具有函数式编程(FP)和响应式编程(RP)的特性。它主要吸取了.Net的 Reactive Extensions的设计和实现。

ReactiveCocoa 的宗旨是Streams of values over time ,随着时间变化而不断流动的数据流。

ReactiveCocoa 主要解决了以下这些问题:

  • UI数据绑定

UI控件通常需要绑定一个事件,RAC可以很方便的绑定任何数据流到控件上。

  • 用户交互事件绑定

RAC为可交互的UI控件提供了一系列能发送Signal信号的方法。这些数据流会在用户交互中相互传递。

  • 解决状态以及状态之间依赖过多的问题

有了RAC的绑定之后,可以不用在关心各种复杂的状态,isSelect,isFinish……也解决了这些状态在后期很难维护的问题。

  • 消息传递机制的大统一

ReactiveCocoa 源码解析

对于一个应用来说,绝大部分的时间都是在等待某些事件的发生或响应某些状态的变化,比如用户的触摸事件、应用进入后台、网络请求成功刷新界面等等,而维护这些状态的变化,常常会使代码变得非常复杂,难以扩展。而 ReactiveCocoa 给出了一种非常好的解决方案,它使用信号来代表这些异步事件,提供了一种统一的方式来处理所有异步的行为,包括代理方法、block 回调、target-action 机制、通知、KVO 等:

WechatIMG414

例子代码

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
// 代理方法
[[self
rac_signalForSelector:@selector(webViewDidStartLoad:)
fromProtocol:@protocol(UIWebViewDelegate)]
subscribeNext:^(id x) {
// 实现 webViewDidStartLoad: 代理方法
}];

// target-action
[[self.avatarButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(UIButton *avatarButton) {
// avatarButton 被点击了
}];

// 通知
[[[NSNotificationCenter defaultCenter]
rac_addObserverForName:kReachabilityChangedNotification object:nil]
subscribeNext:^(NSNotification *notification) {
// 收到 kReachabilityChangedNotification 通知
}];

// KVO
[RACObserve(self, username) subscribeNext:^(NSString *username) {
// 用户名发生了变化
}];

RACSignal

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

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

  • ReactiveCocoa allows you to perform operations on

  • signals

  • map, filter, skip, take, throttle…

  • Operations return RACSignal, allowing method chaining

RACSubject

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

  • RACSubject:信号提供者,自己可以充当信号,又能发送信号

创建方法:

  1. 创建RACSubject
  2. 订阅信号
  3. 发送信号

工作流程:

  1. 订阅信号,内部保存了订阅者,和订阅者相应block
  2. 当发送信号的,遍历订阅者,调用订阅者的nextBolck

    注:如果订阅信号,必须在发送信号之前订阅信号,不然收不到信号,有利用区别RACReplaySubject

根据官方的 Design Guidelines 中的说法,我们应该尽可能少地使用它。因为它太过灵活,我们可以在任何时候任何地方操作它,所以一旦过度使用,就会使代码变得非常复杂,难以理解。

RACSequence

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

从理论上说,一个 RACSequence 由两部分组成:

head :指的是序列中的第一个对象,如果序列为空,则为 nil ;
tail :指的是序列中除第一个对象外的其它所有对象,同样的,如果序列为空,则为 nil 。

RACStream

RACStream 信号源,是 ReactiveCocoa 中最核心的类,代表的是任意的值流,它是整个 ReactiveCocoa 得以建立的基石,下面是它的继承结构图:

你可以把它想象成水龙头中的水,当你打开水龙头时,水源源不断地流出来;你也可以把它想象成电,当你插上插头时,电静静地充到你的手机上;你还可以把它想象成运送玻璃珠的管道,当你打开阀门时,珠子一个接一个地到达。这里的水、电、玻璃珠就是我们所需要的值,而打开水龙头、插上插头、打开阀门就是订阅它们的过程。

RACSubject的使用

一般不推荐使用RACSubject,因为它过于灵活,滥用的话容易导致复杂度的增加。但有一些场景用一下还是比较方便的,比如ViewModel的errors。

  • 一、UI 操作,连续的动作与动画部分,例如某些控件跟随滚动。

  • 二、网络库,因为数据是在一定时间后才返回回来,不是立刻就返回的。

  • 三、刷新的业务逻辑,当触发点是多种的时候,业务往往会变得很复杂,用 delegate、notification、observe 混用,难以统一。这时用 RAC 可以保证上层的高度一致性,从而简化逻辑上分层。

  • 基本上异步的事件能用 RAC 的都用的 RAC。

  • 不过代理方法用 RAC 的比较少,比如 UITableView 的代理方法一般都是直接写了。

  • 用 RACSubject + RACComand 来简化和统一应用的错误处理逻辑,这个算比较经典的吧。

ReactiveCocoa 讨论会 | 唐巧的博客

RAC调试

  • 的确很痛苦,跟断点有的时候计算堆栈都要等几分钟。
  • 关于调试,RAC 源码下有 instruments 的两个插件,方便大家使用。

RACCommand

  • RACCommand: 处理事件
  • 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// RACCommand: 处理事件
// RACCommand: 不能返回一个空的信号
// input: RACCommand执行传入的参数
// initWithSignalBlock: RACCommand执行时候调用保存的Block

RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {

NSLog(@"命令的参数input = %@", input);

return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {

// 通过订阅者发送数据
[subscriber sendNext:@"订阅者发送的数据"];
// 命令执行完毕
[subscriber sendCompleted];

return [RACDisposable disposableWithBlock:^{
NSLog(@"disposableWithBlock");
}];
}];
}];

// 订阅命令内部的信号

// 方式一:
RACSignal *signal = [command execute:@"命令----数据"];
[signal subscribeNext:^(id _Nullable x) {
NSLog(@"x = %@", x);
}];

// 方式二:
[command.executionSignals.switchToLatest subscribeNext:^(RACSignal *signal) {
NSLog(@"signal = %@", signal);
}];

// 执行命令
[command execute:@"命令----数据"];

// 监听事件有没有完成
[command.executing subscribeNext:^(NSNumber * _Nullable x) {
if(x.boolValue){
// 当前正在执行
NSLog(@"当前正在执行");
}else{
// 执行完成 / 没有执行
NSLog(@"执行完成 / 没有执行");
}
}];

总结

至此,我们介绍完了 ReactiveCocoa 的四大核心组件,对它的架构有了宏观上的认识。它建立于 Monad 的概念之上,然后围绕其搭建了一系列完整的配套组件,它们共同支撑了 ReactiveCocoa 的强大功能。尽管,ReactiveCocoa 是一个重型的函数式响应式框架,但是它并不会对我们现有的代码构成侵略性,我们完全可以在一个单独的类中使用它,哪怕只是简单的一行代码,也是没有问题的。所以,如果你对 ReactiveCocoa 感兴趣的话,不妨就从现在开始尝试吧,Let’s go !

信号总结

  • 首先在创建信号的时候,带进去一个已经订阅的Block(didSubscribe),把它保存在信号中,并没有执行;
  • 在订阅信号的时候,带进去一个发送信号的Block(nextBlock),然后创建一个订阅者(RACSubscriber对象),订阅者会把nextBlock进行保存,
  • 接着执行之前信号保存的didSubscribe这个Block,并将订阅者传递过去;
  • 然后在didSubscribe回调里面,由订阅者发送信号,也就是执行之前保存的nextBlock;
    最后在nextBlock(订阅回调)里面监听到发送的内容。

iOS RAC学习之路(一) - 简书

RACSignal的2个方法

    1. 创建信号: createSignal, 创建一个signal对象
      • 并且把createSignal方法的didSubscribeBlock赋值给所创建的signal的didSubscribe属性,这个didSubscribe的入参是subscriber,返回RACDisposable。
      • RACSignal使用didSubscribe属性,封装了订阅者对这个信号的处理
      • RACSignal的类方法
    1. 订阅信号: subscribeNext, 创建了一个RACSubscriber订阅者
      • 订阅者会把nextBlock赋值给自己的next的属性,
      • 然后,执行订阅操作[self subscribe:o]
      • didSubscribe是信号的invoke每个订阅者的Block属性, 订阅方法subscribe的实现:上面的代码很清晰,直接是self.didSubscribe(subscriber),
      • 我们可以知道,刚刚创建的subscriber对象,直接传递给上文中我们提到的signal的didSubscribe属性。这样,我们可以解释上面的第二个和第三个问题,subscriber就是didSubscribe的形参,block对象是在subscribeNext的时候执行的,刚刚的订阅者对象作为参数传入,就是subscriber对象。
      • RACSignal的实例方法

RAC的作用

而RAC为MVVM带来很大的便利,比如RACCommand, UIKit的RAC Extension等等。使用MVVM不一定能减少代码量,但能降低代码的复杂度。

参考

  1. 标签归档 | ReactiveCocoa - 美团点评技术团队
  2. 『状态』驱动的世界:ReactiveCocoa
  3. RAC基础学习一:信号和订阅者模式 - 简书
文章作者: MichaelMao
文章链接: http://frizzlefur.com/2018/12/29/ReactiveCocoa笔记(一) —— 框架概览/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下