iOS 开源库源码分析之ReactiveCocoa

此文基于ReactiveObjC-3.1.0

ReactiveCocoa是一个将函数响应式编程范式带入Objective-C的一个开源库,由Josh AbernathyJustin 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

results matching ""

    No results matching ""