iOS 开源库源码分析之ReactiveCocoa
ReactiveCocoa是一个将函数响应式编程范式带入Objective-C的一个开源库,由Josh Abernathy和Justin Spahr-Summers在对GitHub for Mac的开发过程中建立。
Objective-C能够得此发展源于大量其他语言的工程师加盟iOS社区,Objective-C第一次被苹果以外的人打磨。
本文主要介绍什么是函数响应式编程,RAC的概览,冷热信号转换的原理,基本的操作符等
函数式编程
什么是函数式编程?举例:
命令式编程
int a, b, r;
void add_abs() {
scanf("%d %d", &a, &b);
r = abs(a) + abs(b);
printf("%d", r);
}
函数式编程
int add_abs(int a, int b) {
return abs(a) + abs(b);
}
函数式编程的几个特点
- 函数是第一公民,可以作为参数和返回值
- 函数的调用不会修改状态,不会修改全局变量,任何一次调用相同的输入都会返回相同的输出
函数式编程的几个技术
递归
递归的好处就是减少代码
高阶函数
高阶函数是参数或者返回值为函数的函数
pipeline
把函数实例成一个一个的action,然后把一组action放到一个数组中,然后把数据传给这个action list,数据就像一个pipeline一样顺序地被各个函数操作
map & reduce & filter
map是一个高阶函数,一个集合的每一个元素通过给定一个函数进行处理,然后转化为另一个集合。
例如map (toLower) "abcDEFG12!@#" 的结果就是"abcdefg12!@#"
reduce也是一个高阶函数,通过一个combiner函数,让集成中两个元素进行处理,得到结果后再与其余元素进行处理,最后得到一个返回值
例如reduce (+) 0 [1..5]最后的结果是15
filter,高阶函数,集合中每一个元素通过一个predicate函数得到true/false,最后得到一个元素处理结果为true组成的集成
例如filter (isAlpha) "$#!+abcDEF657" 结果是 "abcDEF"
柯里化(curry)
curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
// JS
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
响应式
响应式编程最开始是来自微软的.Net框架Reactive Extensions.aspx)
什么是响应式编程?响应式编程就是用asynchronous data streams 进行编程。比较常听到的是asynchronous event streams,比如click event,event bus等。但这边注重的是data与stream,Reactive Extensions将event延伸为data,Stream就是一个 按时间排序的Events(Ongoing events ordered in time)序列。
流可以发送3种不同的事物:一个值(类型不限),一个错误或者一个已完成的信号。 我们只能异步捕获这些发送的事件,即:定义一个函数用于当一个值发送出来时再执行,定义一个函数用于当错误发送出来时执行,定义一个函数用于当完成发送出来时执行。
如图
--a---b-c---d---X---|-> a, b, c, d 都是发送出的值 X 是错误 | 是 'completed' 信号 ---> 是时间线
RP本身是建立于观察者模式之上的一种编程范式。对流的 “侦听” 又称为 订阅(subscribing),而定义的函数即为 观察者(observer),流就是 主题(subject, observable)。这是一个典型的观察者模式。
Streams
ReactiveCocoa由两大主要部分组成:signals (RACSignal) 和 sequences (RACSequence)。
signal 和 sequence 都是streams,他们共享很多相同的方法。ReactiveCocoa在功能上做了语义丰富、一致性强的一致性设计:signal是push-driver的stream,sequence是pull-driver的stream。pull-driver是任何时刻我有数据了你都可以获取到,因为数据先存储了,取数据的时间控制在调用者上。push-driver是任何时刻有数据了都会push给调用者,如果你没处理就丢失了。
RACStream是一个抽象类,是不能直接实例化的
Signals
根据上面响应式编程流的概念,信号给他们的订阅者发送三种不同的事件类型:
- next事件,next从流中提供一个新的值
- error事件,该事件表示早一个信号正常结束之前发生了一个错误。
- completed事件:表示信号正常结束,同时也没有其他更多的值添加到流中。
Subjects
一个Subject,在RAC中代表的是RACSubject类,是一种可以被手动控制的信号。Subject可以认为是可变的信号,就像NSMutableArray对于NSArray一样。Subject是很有用的连接非RAC代码到RAC的很有用的工具。
Sequences
sequence,在RAC中代表的是RACSequence类,是一种pull-driven的流。Sequence是一种集合类型,类似NSArray.
RACSubscriber
RACSubscriber是订阅者,所有实现了 RACSubscriber 协议的类都可以作为信号源的订阅者。一次订阅是通过调用-subscribeNext:error:completed产生的。订阅会持有它的signal对象,并且在信号completed或者error的时候释放。
RACScheduler
调度器,是一个串行的信号执行队列,用来执行任务或者传递结果。Schedulers类似于GCD中的队列,但是scheduler支持取消队列(通过disposables),并总是串行执行的。
RACDisposable
清洁工,Disposables常常用来取消对一个信号的订阅。
Commands
command,在RAC中表示的是RACCommand类,可以创建或者订阅一个信号用来响应某些action动作。这可以很方便的来处理App中的用户交互。
基本用法如下
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"执行命令");
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"请求数据"];
[subscriber sendCompleted];
return nil;
}];
}];
[command.executionSignals subscribeNext:^(id x) {
NSLog(@"signal %@",x);
[x subscribeNext:^(id x) {
NSLog(@"value %@",x);
}];
}];
[command execute:@1];
// output: 执行命令 - signal <RACDynamicSignal: 0x608000227280> - value 请求数据
首先创建一个RACCommand,signalBlock是需要返回一个信号的,可以input(execute传入的值)来返回不同signal。在执行execute的时候会调用RACCommand的_signalBlock,并且把block返回的冷信号通过connection转换为热信号,然后把热信号加入_activeExecutionSignals,这样订阅command.executionSignals(_activeExecutionSignals数组转换的高阶信号)里面收到的信号就可以获取_signalBlock冷信号的值了。
UIButton这里有一个常用的用法可以方便的接收touch事件,如下
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];
那是因为setRac_command:方法里面先addTarget:action:forControlEvents,然后action里面的实现是self.button.rac_command 调用execute:,这样touch事件发出时RACCommand的signal就能收到回调了。
Connections
在提到RACMulticastConnection的时候需要说一下冷信号和热信号的概念。
Hot Observable是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而Cold Observable是被动的,只有当你订阅的时候,它才会发布消息。
Hot Observable可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而Cold Observable只能一对一,当有不同的订阅者,消息是重新完整发送。
RACSignal家族中,RACSubject就是热信号,除此还有RACReplaySubject,RACBehaviorSubject,RACGroupedSignal;
RACSubject是继承自RACSignal,并且它还遵守RACSubscriber协议。这就意味着它既能订阅信号,也能发送信号。在RACSubject里面有一个NSMutableArray数组,里面装着该信号的所有订阅者。
RACSignal就是冷信号,除此还有RACEmptySignal,RACReturnSignal,RACDynamicSignal,RACErrorSignal,RACChannelTerminal。
根据RACSignal订阅和发送信号的流程,我们可以知道,每订阅一次冷信号RACSignal,就会执行一次didSubscribe的block。这个时候就是可能出现问题的地方。如果RACSignal是被用于网络请求,那么在didSubscribe block里面会被重复的请求。
如何做到信号只执行一次didSubscribe block,最重要的一点是RACSignal冷信号只能被订阅一次。由于冷信号只能一对一,那么想一对多就只能交给热信号去处理了。这时候就需要把冷信号转换成热信号。
冷信号转换成热信号需要用到RACMulticastConnection 这个类。
RACMulticastConnection最主要的是保存了两个信号,一个是暴露给外部的类型为RACSubject的signal属性,一个是内部的sourceSignal(RACSignal类型)。
用sourceSignal去发送信号,内部再用RACSubject去订阅sourceSignal,然后RACSubject会把sourceSignal的信号值依次发给它的订阅者们。
RACSignal的订阅过程
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}];
[signal subscribeNext:^(id x) {
NSLog(@"x%@",x);
}];
- 首先RACSignal调用createSignal方法创建实例,这里实际是通过RACDynamicSignal子类
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
RACDynamicSignal *signal = [[self alloc] init];
signal->_didSubscribe = [didSubscribe copy];
return [signal setNameWithFormat:@"+createSignal:"];
}
实际就是给_didSubscribe变量赋值执行订阅操作需要执行的 block
调用信号的subscribeNext操作,先传入nextBlock创建一个RACSubscriber实例,然后调用子类信号RACDynamicSignal的subscribe方法,通过调度者执行前面的block类型变量_didSubscribe
当执行_didSubscribe的时候,subscriber会调用sendNext方法,就是执行前面创建的RACSubscriber实例的next回调
这里再讲一下热信号的过程
RACSubject *subject = [RACSubject subject];
[subject subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
[subject sendNext:@"A"];
- 首先RACSubject alloc的时候是没有产生_didSubscribe block的,当subscribeNext的时候会创建一个订阅者,再调用信号的subscribe方法
- 接着上一步,RACSubject是重写了subscribe方法的,RACSubject会把订阅者保存在subscribers数组里面
- sendNext方法会遍历所有的subscriber然后sendNext的值
基本操作符
这里的操作符是应用于是sequences 和 signals 的流操作符。
Performing side effects with signals
冷信号是只有订阅了之后才会执行side effects,当然side effects也可以注入。我理解的side effects就是订阅信号时要执行的代码。
- Subscription,-subscribe… 系列方法能够让你执行signal的side effects,访问信号中当前或者之后的值,对于一个冷信号,side effects会在每一次订阅的时候执行,当然这种行为可以通过connection转化为热信号改变。
- Injecting effects,-do...系列方法可以将side effects注入到信号中,当订阅者订阅的时候就会执行这个side effects。
转换流
这些操作能够将一个流转换为一个新的流
- Mapping,-map:方法前面有讲过,就是通过一个函数操作每一个元素然后得到一个新的流
- Filtering,-filter:方法前面也讲过,就是通过一个函数操作每一个元素通过true/false返回为true的元素组成的流
Combining streams
将多个信号流聚合成一个信号流
- Concatenating,-concat: 方法把一个流添加在另一个流后面,成为一个新的流包含两个流的每一个元素
实现就是创建一个新的信号,在 side effects 里面实现源信号订阅,当订阅完成后,再执行添加的目标信号的订阅方法。如下
RACDisposable *sourceDisposable = [self subscribeNext:^(id x) {
[subscriber sendNext:x];
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
RACDisposable *concattedDisposable = [signal subscribe:subscriber];
serialDisposable.disposable = concattedDisposable;
}];
- Flattening,flatten操作必须是对高阶信号(即信号里面还是信号)进行操作,flatten是对高阶信号进行的降阶操作。
RACSignal *signal1 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}];
RACSignal *signal2 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
[subscriber sendCompleted];
return nil;
}];
RACSignal *highOrderSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:signal1];
[subscriber sendNext:signal2];
[subscriber sendCompleted];
return nil;
}];
RACSignal *flattenSignal = [highOrderSignal flatten];
[flattenSignal subscribeNext:^(id x) {
NSLog(@"recveive: %@", x);
}];
// print 1,2
- Mapping and flattening,flatten操作实际就是就是调用flattenMap:
- (instancetype)flatten {
__weak RACStream *stream __attribute__((unused)) = self;
return [[self flattenMap:^(id value) {
return value;
}] setNameWithFormat:@"[%@] -flatten", self.name];
}
flatten操作如果操作对象不是高阶信号就会crash,"Value returned from -flattenMap: is not a stream:",那是因为block(value)的值如果不是RACStream就会崩溃
- (instancetype)flattenMap:(RACStream * (^)(id value))block {
Class class = self.class;
return [[self bind:^{
return ^(id value, BOOL *stop) {
id stream = block(value) ?: [class empty];
NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream);
return stream;
};
}] setNameWithFormat:@"[%@] -flattenMap:", self.name];
}
flattenMap:的作用把流的每个值都转换为一个流,然后所有流都flatten化为一个新的流。先map再flatten
RACSignal *signal1 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendNext:@2];
[subscriber sendCompleted];
return nil;
}];
RACSignal *flattenMapSignal = [signal1 flattenMap:^RACStream *(id value) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@([value intValue] * 2)];
[subscriber sendCompleted];
return nil;
}];
}];
[flattenMapSignal subscribeNext:^(id x) {
NSLog(@"recveive: %@", x);
}];
// print 2 4
flattenMap是基于bind方法的,其实前面提到的很多操作符都是基于流的bind方法,这里看一下bind的简化后的实现。
- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block {
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACSignalBindBlock bindingBlock = block();
void (^addSignal)(RACSignal *) = ^(RACSignal *signal) {
RACDisposable *disposable = [signal subscribeNext:^(id x) {
[subscriber sendNext:x];
} error:^(NSError *error) {
} completed:^{
}];
};
RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
id signal = bindingBlock(x, &stop);
if (signal != nil) addSignal(signal);
} error:^(NSError *error) {
} completed:^{
}];
return compoundDisposable;
}];
}
归纳下来的步骤就是
- 创建一个新的信号,在side effects里面原信号进行subscribeNext操作
- 当原信号send一个值经过新信号的时候,使用bindingBlock进行操作
- bindingBlock的操作是需要返回一个信号的,接收到信号后会一开始创建的信号会对其subscribeNext,并且在其nextBlock里面,把第二步中经过处理的值send出去
Combining signals
将多个信号聚合成一个信号.
- Sequencing,使用-then:方法,一般形式为
signalC = [signalA then:^{
return signalB
}]
当订阅signalC的时候,会忽略signalA所有next事件(next会执行但订阅者接收不到,因为signalA全部被执行filter操作),当signalA的completed事件发送时,订阅B信号并返回B所有事件
- Merging,+merge:方法,merge操作其实就是调用flatten,对高阶信号的降阶操作
Combining latest values,+combineLatest: 和 +combineLatest:reduce:用来观察多个信号的改变,然后发送几个信号最新的值组成RACTuple(元组),reduce的话就是在combineLatest基础上会对RACTuple进行reduce操作。
Switching, -switchToLatest用于操作高阶信号(含有多个信号),然后总是输出高阶信号中最新信号的值。
NSObject的category
- NSObject+RACDeallocating
这个category 会有一个rac_willDeallocSignal信号(RACSubject),在dealloc调用前订阅这个信号就能够收到dealloc的消息,主要是hook了dealloc方法,保存了一个RACCompoundDisposable,在dealloc的时候调用dispose,回调_disposeBlock,然后由rac_willDeallocSignal sendCompleted。
- NSObject+RACLifting
提供rac_liftSelector:withSignalOfArguments:方法,其实就是提供runtime调用方法,就是map的时候把arguments的信号传入selector invoke。
- NSObject+RACSelectorSignal
rac_signalForSelector方法可以代替代理,处理回调事件。
这里是hook了消息转发的方法forwardInvocation,在调用了rac_signalForSelector之后会创建一个subject,订阅之后,当有人调用传入的selector后就会收到回调。
- NSObject+RACKVOWrapper
rac_observeKeyPath方法可以替代KVO,很方便的在block里面处理回调
在调用方法的时候会生成一个RACKVOTrampoline(RACDisposable),通过RACKVOProxy实现监听。
UIControl的RACSignalSupport
在RAC中UIControl都有一个RACSignalSupport的category,目的是更加方便的响应UI事件。
- (RACSignal *)rac_signalForControlEvents:(UIControlEvents)controlEvents {
@weakify(self);
return [RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
@strongify(self);
[self addTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents];
}];
}
UIControl的实例调用这个方法之后,就会创建一个信号,在接收controlEvents的时候就会sendNext了,这样订阅信号的人就能很方便的响应事件了。
基于此,很多子类也利用了这个方法,例如UITextField的category就是通过此来接收UIControlEventAllEditingEvents的消息并且map出textField.text给信号订阅者,使用起来非常方便,如下:
RAC(self.label, text) = _textField.rac_textSignal;
RAC的宏这里解释一下最终其实是通过赋值号右边的signal调用了RACSignal的setKeyPath:onObject方法,这里面订阅了这个信号并且在nextBlock改变了RAC传入对象的属性值。以此最终达到textField文案改变时候,label也会改变的效果。
最后,学习RAC能够让大家了解OOP之外的编程范式,让大家更加深入理解编程这件事。
本文作者coderyi