原文:http://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=402206631&idx=5&sn=4de00f0db60efa7c75c211b329717f14&scene=0#wechat_redirect
ReactiveCocoa 是一个 iOS 中的函数式响应式编程框架,它受 Functional Reactive Programming 的启发,是 Justin Spahr-Summers 和 Josh Abernathy 在开发 GitHub for Mac 过程中的一个副产品,它提供了一系列用来组合和转换值流的 API 。
Mattt Thompson 大神是这样评价 ReactiveCocoa 的:
Breaking from a tradition of covering Apple APIs exclusively,this edition of NSHipster will look at an open source project that exemplifies this brave new era for Objective-C.
他认为 ReactiveCocoa 打破了苹果 API 排他性的束缚,勇敢地开创了 Objective-C 的新纪元,具有划时代的意义。不得不说,这对于一个第三方框架来说,已经是非常高的评价了。
关于 ReactiveCocoa 的版本演进历程,简单介绍如下:
-
<= v2.5 :Objective-C ;
-
v3.x :Swift 1.2 ;
-
v4.x :Swift 2.x 。
注:本文所介绍的均为 ReactiveCocoa v2.5 版本中的内容,这是 Objective-C 最新的稳定版本。另外,本文的目录结构如下:
简介
信号源
-
RACStream
-
RACSignal
-
RACSubject
-
RACSequence
-
订阅者
-
RACSubscriber
-
RACMulticastConnection
-
调度器
-
RACScheduler
-
清洁工
-
RACDisposable
-
总结
-
参考链接
简介
ReactiveCocoa 是一个非常复杂的框架,在正式开始介绍它的核心组件前,我们先来看看它的类图,以便从宏观上了解它的层次结构:
从上面的类图中,我们可以看出,ReactiveCocoa 主要由以下四大核心组件构成:
信号源:RACStream 及其子类;
订阅者:RACSubscriber 的实现类及其子类;
调度器:RACScheduler 及其子类;
清洁工:RACDisposable 及其子类。
其中,信号源又是最核心的部分,其他组件都是围绕它运作的。
对于一个应用来说,绝大部分的时间都是在等待某些事件的发生或响应某些状态的变化,比如用户的触摸事件、应用进入后台、网络请求成功刷新界面等等,而维护这些状态的变化,常常会使代码变得非常复杂,难以扩展。而ReactiveCocoa 给出了一种非常好的解决方案,它使用信号来代表这些异步事件,提供了一种统一的方式来处理所有异步的行为,包括代理方法、block 回调、target-action 机制、通知、KVO 等:
// 代理方法
[[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) {
// 用户名发生了变化
}];
然而,这些还只是 ReactiveCocoa 的冰山一角,它真正强大的地方在于我们可以对这些不同的信号进行任意地组合和链式操作,从最原始的输入 input 开始直至得到最终的输出 output 为止:
[[[RACSignal
combineLatest:@[ RACObserve(self,username),RACObserve(self,password) ]
reduce:^(NSString *username,NSString *password) {
return
@(username.length > 0 && password.length > 0);
}]
distinctUntilChanged]
subscribeNext:^(NSNumber *valid) {
if
(valid.boolValue) {
}
else
{
}
}];
因此,对于 ReactiveCocoa 来说,我们可以毫不夸张地说,阻碍它发挥的瓶颈就只剩下你的想象力了。
信号源
在 ReactiveCocoa 中,信号源代表的是随着时间而改变的值流,这是对 ReactiveCocoa 最精准的概括,订阅者可以通过订阅信号源来获取这些值:
Streams of values over time.
你可以把它想象成水龙头中的水,当你打开水龙头时,水源源不断地流出来;你也可以把它想象成电,当你插上插头时,电静静地充到你的手机上;你还可以把它想象成运送玻璃珠的管道,当你打开阀门时,珠子一个接一个地到达。这里的水、电、玻璃珠就是我们所需要的值,而打开水龙头、插上插头、打开阀门就是订阅它们的过程。
RACStream 是 ReactiveCocoa 中最核心的类,代表的是任意的值流,它是整个 ReactiveCocoa 得以建立的基石,下面是它的继承结构图:
事实上,RACStream 是一个抽象类,通常情况下,我们并不会去实例化它,而是直接使用它的两个子类 RACSignal 和RACSequence 。那么,问题来了,为什么 RACStream 会被设计成一个抽象类?或者说它的抽象过程是以什么作为依据的呢?
是的,没错,看过我上一篇文章 《Functor、Applicative 和 Monad》 的同学,应该已经知道了,RACStream 就是以Monad 的概念为依据进行设计的,它代表的就是一个 Monad :
/// An abstract class representing any stream of values.
///
/// This class represents a monad,upon which many stream-based operations can
/// be built.
///
/// When subclassing RACStream,only the methods in the main @interface body need
/// to be overridden.
@interface RACStream : NSObject
/// Lifts `value` into the stream monad.
///
/// Returns a stream containing only the given value.
+ (instancetype)
return
:(id)value;
/// Lazily binds a block to the values in the receiver.
///
/// This should only be used if you need to terminate the bind early,or close
/// over some state. -flattenMap: is more appropriate for all other cases.
///
/// block - A block returning a RACStreamBindBlock. This block will be invoked
/// each time the bound stream is re-evaluated. This block must not be
/// nil or return nil.
///
/// Returns a new stream which represents the combined result of all lazy
/// applications of `block`.
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
@end
有了 Monad 作为基石后,许多基于流的操作就可以被建立起来了,比如 map 、filter 、zip 等。
RACSignal@H_251_404@
RACSignal 代表的是未来将会被传送的值,它是一种 push-driven 的流。RACSignal 可以向订阅者发送三种不同类型的事件:
next :RACSignal 通过 next 事件向订阅者传送新的值,并且这个值可以为 nil ;
completed :RACSignal 通过 completed 事件向订阅者表明信号已经正常结束,不会再有后续的值传送给订阅者。
注意,ReactiveCocoa 中的值流只包含正常的值,即通过 next 事件传送的值,并不包括 error 和 completed 事件,它们需要被特殊处理。通常情况下,一个信号的生命周期是由任意个 next 事件和一个 error 事件或一个 completed事件组成的。
从前面的类图中,我们可以看出,RACSignal 并非只有一个类,事实上,它的一系列功能是通过类簇来实现的。除去我们将在下节介绍的 RACSubject 及其子类外,RACSignal 还有五个用来实现不同功能的私有子类:
RACEmptySignal :空信号,用来实现 RACSignal 的 +empty 方法;
RACReturnSignal :一元信号,用来实现 RACSignal 的 +return: 方法;
RACDynamicSignal :动态信号,使用一个 block 来实现订阅行为,我们在使用 RACSignal 的 +createSignal: 方法时创建的就是该类的实例;
RACChannelTerminal :通道终端,代表 RACChannel 的一个终端,用来实现双向绑定。
对于 RACSignal 类簇来说,最核心的方法莫过于 -subscribe: 了,这个方法封装了订阅者对信号源的一次订阅过程,它是订阅者与信号源产生联系的唯一入口。因此,对于 RACSignal 的所有子类来说,这个方法的实现逻辑就代表了该子类的具体订阅行为,是区分不同子类的关键所在。同时,这也是为什么 RACSignal 中的 -subscribe: 方法是一个抽象方法,并且必须要让子类实现的原因:
- (RACDisposable *)subscribe:(id)subscriber {
NSCAssert(NO,@
"This method must be overridden by subclasses"
);
return
nil;
}
RACSubject
RACSubject 代表的是可以手动控制的信号,我们可以把它看作是 RACSignal 的可变版本,就好比 NSMutableArray 是NSArray 的可变版本一样。RACSubject 继承自 RACSignal ,所以它可以作为信号源被订阅者订阅,同时,它又实现了RACSubscriber 协议,所以它也可以作为订阅者订阅其他信号源,这个就是 RACSubject 为什么可以手动控制的原因:
根据官方的 Design Guidelines 中的说法,我们应该尽可能少地使用它。因为它太过灵活,我们可以在任何时候任何地方操作它,所以一旦过度使用,就会使代码变得非常复杂,难以理解。
根据我的实际使用经验,在 MVVM 中使用 RACSubject 可以非常方便地实现统一的错误处理逻辑。比如,我们可以在viewmodel 的基类中声明一个 RACSubject 类型的属性 errors ,然后在 viewController 的基类中编写统一的错误处理逻辑:
[self.viewmodel.errors subscribeNext:^(NSError *error) {
// 错误处理逻辑
}
此时,假设在某个界面的 viewmodel 中有三个用来请求远程数据的命令,分别是 requestReadmeMarkdownCommand、requestBlobCommand 和 requestReadmeHTMLCommand ,那么这个界面的错误处理逻辑就可以这么写:
[[RACSignal
merge:@[
self.requestReadmeMarkdownCommand.errors,
self.requestBlobCommand.errors,
self.requestReadmeHTMLCommand.errors
]]
subscribe:self.errors];
另外,RACSubject 也有三个用来实现不同功能的子类:
RACGroupedSignal :分组信号,用来实现 RACSignal 的分组功能;
RACSubject 的功能非常强大,但是太过灵活,也正是因为如此,我们只有在迫不得已的情况下才会使用它。
RACSequence@H_251_404@
RACSequence 代表的是一个不可变的值的序列,与 RACSignal 不同,它是 pull-driven 类型的流。从严格意义上讲,RACSequence 并不能算作是信号源,因为它并不能像 RACSignal 那样,可以被订阅者订阅,但是它与 RACSignal之间可以非常方便地进行转换。
从理论上说,一个 RACSequence 由两部分组成:
head :指的是序列中的第一个对象,如果序列为空,则为 nil ;
tail :指的是序列中除第一个对象外的其它所有对象,同样的,如果序列为空,则为 nil 。
事实上,一个序列的 tail 仍然是一个序列,如果我们将序列看作是一条毛毛虫,那么 head 和 tail 可表示如下:
同样的,一个序列的 tail 也可以看作是由 head 和 tail 组成,而这个新的 tail 又可以继续看作是由 head 和 tail 组成,这个过程可以一直进行下去。而这个就是 RACSequence 得以建立的理论基础,所以一个 RACSequence 子类的最小实现就是 head 和 tail :
/// Represents an immutable sequence of values. Unless otherwise specified,the
/// sequences' values are evaluated lazily on demand. Like Cocoa collections,
/// sequences cannot contain nil.
///
/// Most inherited RACStream methods that accept a block will execute the block
/// _at most_ once for each value that is evaluated in the returned sequence.
/// Side effects are subject to the behavior described in
/// +sequenceWithHeadBlock:tailBlock:.
///
/// Implemented as a class cluster. A minimal implementation for a subclass
/// consists simply of -head and -tail.
@interface RACSequence : RACStream
/// The first object in the sequence,or nil if the sequence is empty.
///
/// Subclasses must provide an implementation of this method.
@property (nonatomic,strong,readonly) id head;
/// All but the first object in the sequence,or nil if the sequence is empty.
///
/// Subclasses must provide an implementation of this method.
@property (nonatomic,readonly) RACSequence *tail;
@end
总的来说,RACSequence 存在的最大意义就是为了简化 Objective-C 中的集合操作:
Simplifying Collection Transformations: Higher-order functions like map,filter,fold/reduce are sorely missing from Foundation.
比如下面的代码:
NSMutableArray *results = [NSMutableArray array];
for
(NSString *str
in
strings) {
if
(str.length < 2) {
continue
;
}
NSString *newString = [str stringByAppendingString:@
"foobar"
];
[results addObject:newString];
}
可以用 RACSequence 来优雅地实现:
RACSequence *results = [[strings.rac_sequence
filter:^ BOOL (NSString *str) {
return
str.length >= 2;
}]
map:^(NSString *str) {
return
[str stringByAppendingString:@
"foobar"
];
}];
因此,我们可以非常方便地使用 RACSequence 来实现集合的链式操作,直到得到你想要的最终结果为止。除此之外,使用 RACSequence 的另外一个主要好处是,RACSequence 中包含的值在默认情况下是懒计算的,即只有在真正用到的时候才会被计算,并且只会计算一次。也就是说,如果我们只用到了一个 RACSequence 中的部分值的时候,它就在不知不觉中提高了我们应用的性能。
同样的,RACSequence 的一系列功能也是通过类簇来实现的,它共有九个用来实现不同功能的私有子类:
RACUnarySequence :一元序列,用来实现 RACSequence 的 +return: 方法;
RACIndexSetSequence :用来遍历索引集;
RACEmptySequence :空序列,用来实现 RACSequence 的 +empty 方法;
RACDynamicSequence :动态序列,使用 blocks 来动态地实现一个序列;
RACSignalSequence :用来遍历信号中的值;
RACArraySequence :用来遍历数组中的元素;
RACEagerSequence :非懒计算的序列,在初始化时立即计算所有的值;
RACStringSequence :用来遍历字符串中的字符;
RACTupleSequence :用来遍历元组中的元素。
RACSequence 为类簇提供了统一的对外接口,对于使用它的客户端代码来说,完全不需要知道私有子类的存在,很好地隐藏了实现细节。另外,值得一提的是,RACSequence 实现了快速枚举的协议 NSFastEnumeration ,在这个协议中只声明了一个看上去非常抽筋的方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;
有兴趣的同学,可以看看 RACSequence 中的相关实现,我们将会在后续的文章中进行介绍。因此,我们也可以直接使用 for in 来遍历一个 RACSequence 。
订阅者
现在,我们已经知道信号源是什么了,为了获取信号源中的值,我们需要对信号源进行订阅。在 ReactiveCocoa 中,订阅者是一个抽象的概念,所有实现了 RACSubscriber 协议的类都可以作为信号源的订阅者。
RACSubscriber@H_251_404@
在 RACSubscriber 协议中,声明了四个必须实现的方法:
/// Represents any object which can directly receive values from a RACSignal.
///
/// You generally shouldn't need to implement this protocol. +[RACSignal
/// createSignal:],RACSignal's subscription methods,or RACSubject should work
/// for most uses.
///
/// Implementors of this protocol may receive messages and values from multiple
/// threads simultaneously,and so should be thread-safe. Subscribers will also
/// be weakly referenced so implementations must allow that.
@protocol RACSubscriber @required
/// Sends the next value to subscribers.
///
/// value - The value to send. This can be `nil`.
- (void)sendNext:(id)value;
/// Sends the error to subscribers.
///
/// error - The error to send. This can be `nil`.
///
/// This terminates the subscription,and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendError:(NSError *)error;
/// Sends completed to subscribers.
///
/// This terminates the subscription,and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendCompleted;