【iOS架构】iOS ReactiveCocoa函数响应式编程

声明式编程

声明式编程(declarative programming)是一种编程范型,与命令式编程相对立。它描述目标的性质,让电脑明白目标,而非流程。声明式编程不用告诉电脑问题领域,从而避免随之而来的副作用,大幅简化了并行计算的编写难度。而指令式编程则需要用算法来明确的指出每一步该怎么做。

声明式语言包括数据库查询语言(SQL,XQuery),正则表达式,逻辑编程,函数式编程和组态管理系统。

Objective-C和C是命令式编程语言(imperative programming language),程序员得按计算机执行顺序写好一行行语句,产生的行为就是执行这些指令。如果开发者写的语句和顺序都没有错,那么产生的行为就应该能满足项目的需要。

然而,这种命令式的编程通常会有缺陷,一般我们会用手动或自动化测试来发现并减少这些问题。但有另外一种更好的方法,把这些指令都抽象出来,并将重心放在所需行为上,这就是声明式编程(declarative programming)。

命令式编程让开发者将重心放在如何(how)写程序来实现需求。而声明式编程让开发者将重心放在描述需求是什么(what)。

响应式编程

响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

举个例子

命令式编程:

以C语言代码举个例子

int a = 2;
int b = 2;
int c = a + b;
printf("c = %d",c);

显然输出结果是“c = 3”。
如果改变一下代码,增加一行。

int a = 2;
int b = 2;
int c = a + b;
a++;
printf("c = %d",c);

显然输出结果依然是“c = 4”。

如果我们希望c永远等于a和b的和,那么目前看来唯一的方法是每次a和b发生变化的时候,重新执行c = a + b

响应式编程:

在响应式编程中,a的值会随着b或c的更新而更新。

Excel就是响应式编程的一个例子。单元格可以包含字面值或类似”=B1+C1″的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化 。

iOS开发中,Objective-C提供了KVO机制,而ReactiveCocoa框架利用了这个机制,并且进行了各种各样的拓展。

什么是 ReactiveCocoa

ReactiveCocoa(其简称为 RAC)是由 Github 在2012年开源的一个应用于 iOS 和 OS X 开发的框架,目前最新版本为v4.0.0-alpha.4,支持OS X 10.9+ and iOS 8.0+。RAC 具有函数式编程和响应式编程的特性。它主要吸取了 .Net 的 Reactive Extensions 的设计和实现。

Wikipedia:

Functional reactive programming (FRP) is a programming paradigm for reactive programming (asynchronous dataflow programming) using the building blocks of functional programming (e.g. map, reduce, filter).

FRP has been used for programming graphical user interfaces (GUIs), robotics, and music, aiming to simplify these problems by explicitly modeling time.

ReactiveCocoa for a better world

Josh Abernathy, GitHub staff, May 5, 2012

RAC is a framework for composing and transforming sequences of values.

ReactiveCocoa gives us a lot of cool stuff:

  1. The ability to compose operations on future data.

  2. An approach to minimize state and mutability.

  3. A declarative way to define behaviors and the relationships between properties.

  4. A unified, high-level interface for asynchronous operations.

  5. A lovely API on top of KVO.

FRP的核心是信号,信号在ReactiveCocoa中是通过RACSignal来表示的,信号是数据流,可以被绑定和传递。

可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。

  • 可以在水龙头上加一个过滤嘴(filter),不符合的不让通过。
  • 也可以加一个改动装置,把球改变成符合自己的需求(map)。
  • 也可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),这样只要其中的一个水龙头有玻璃球出来,这个新合并的水龙头就会得到这个球。

ReactiveCocoa原理

Signal & Subscriber

Signal(信号)是ReactiveCocoa中的核心。一个signal代表着一系列事件(事件流stream)的一个事件(event)。Subscribing(订阅)是访问signal的接口。

对于一个signal来说,刚刚创建的时候,它还是一个冷信号(Cold signal),只有在有了订阅者(Subscriber)之后,才会变为热信号(Hot signal)。订阅者就好比水龙头最下方的水盆,只有放好了水盆,水龙头才能打开。不然,水(value)不都浪费了么?

Subscribers subscribe to signals. Signals send their subscribers ‘next’, ‘error’, and ‘completed’ events.

在一个信号(Signal)的生命周期中,可以发送无数次next事件,和唯一一次complete或者error事件。

Signal:


// RACSignal.m
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock {
    NSCParameterAssert(nextBlock != NULL);
    NSCParameterAssert(errorBlock != NULL);
    NSCParameterAssert(completedBlock != NULL);

    RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];
    return [self subscribe:o];
}

Subscriber:

@protocol RACSubscriber <NSObject>
@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;

/// Sends the subscriber a disposable that represents one of its subscriptions.
///
/// A subscriber may receive multiple disposables if it gets subscribed to
/// multiple signals; however, any error or completed events must terminate _all_
/// subscriptions.
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end


// A simple block-based subscriber.
@interface RACSubscriber : NSObject <RACSubscriber>

// Creates a new subscriber with the given blocks.
+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed;

@end

- (void)sendNext:(id)value方法为例看一看它的实现:

- (void)sendNext:(id)value {
    @synchronized (self) {
        void (^nextBlock)(id) = [self.next copy];
        if (nextBlock == nil) return;

        nextBlock(value);
    }
}

其实最核心的功能即时调用了自己的nextBlock并传入相应的参数而已。

UITextField创建信号的原理:

// UITextField (RACSignalSupport)
- (RACSignal *)rac_textSignal {
    @weakify(self);
    return [[[[[RACSignal
        defer:^{
            @strongify(self);
            return [RACSignal return:self];
        }]
        concat:[self rac_signalForControlEvents:UIControlEventAllEditingEvents]]
        map:^(UITextField *x) {
            return x.text;
        }]
        takeUntil:self.rac_willDeallocSignal]
        setNameWithFormat:@"%@ -rac_textSignal", self.rac_description];
}

// UIControl (RACSignalSupport)
- (RACSignal *)rac_signalForControlEvents:(UIControlEvents)controlEvents {
    @weakify(self);

    return [[RACSignal
        createSignal:^(id<RACSubscriber> subscriber) {
            @strongify(self);

            [self addTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents];
            [self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
                [subscriber sendCompleted];
            }]];

            return [RACDisposable disposableWithBlock:^{
                @strongify(self);
                [self removeTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents];
            }];
        }]
        setNameWithFormat:@"%@ -rac_signalForControlEvents: %lx", self.rac_description, (unsigned long)controlEvents];
}

UITextField用例:

    [[self.nameTextField.rac_textSignal distinctUntilChanged] subscribeNext:^(NSString *x) {
        @strongify(self);
        self.viewModel.nameText = x;
    }];

我们执行了subscribeNext方法创建了一个订阅者(Subscriber),这个订阅者的nextBlcok方法已经被赋值。而rac_textSignal这个信号的实现中,在每次text发生变化的时候,就会调用订阅者的sendNext方法,从而调用nextBlcok中的代码。

RACSignal的subscription过程

RACSignal的Subscription过程概括起来可以分为三个步骤:

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

信号(Signal)的各种操作

作为一个信号,我们关注它的两个方面:

处理逻辑
数据内容

处理逻辑指的是创建信号的时候,它是如何通知订阅者(Subscriber)并选择发送何种事件的。数据内容指的是信号会传递给订阅者(Subscriber)什么样的数据。

如果我们需要对这些内容进行自定义的修改,那么修改原信号显然是不可行的(信号已经被创建了)。因此,这就牵涉到信号之间的转换(Map)与组合(Combine)。

ReactiveCocoa提供了对信号的各种操作。这些操作几乎都用到了FlattenMap方法。意味着返回一个被修改之后的信号。同时,几乎每个操作还调用了return方法。

//这个return不是我们用于返回一个值的return,只是名字比较像。
+ (RACSignal *)return:(id)value {
    return [RACReturnSignal return:value];
}

该方法的主要作用是,返回一个新的信号,不过原始信号发送事件时的value将被新的value替换。
有了对绑定(Bind)方法、FlattenMap方法和return方法的理解,基本上就可以通过自己阅读源码搞定对信号(Signal)的各种操作了。这里列出几个常用的操作。

filter

filter方法返回一个新的signal。原始信号的value被替换为了符合要求的value,从而实现了筛选、过滤的目的。是否符合要求是由传入的block决定的。即原来的信号的value,如果传入block中返回YES,则新的信号也将输出这个value。

map

map方法返回一个新的signal。原始信号的value被替换为了经过block处理的value。

distinctUntilChanged

distinctUntilChanged方法返回一个新的signal。这个signal只在value和前一个value不同的时候才会发送事件。简记为求异存同。

ignore

这个方法需要传入一个value,当信号收到一个value时,会检查是否和传入的value相同,如果相同就不会发送事件给订阅者。

skip & take

顾名思义,就是跳过(只发送)前n条数据。这里的n就是传入的参数值。

doNext

创建一个新的信号,这个信号和原始信号一模一样,不过可以在创建的过程中调用传入的block。

combineLatest: reduce:

合并若干个信号,得到一个新的信号。把那些信号的value进行处理,得到一个处理过后的value作为新的信号的value。

throttle

throttle方法返回一个新的signal。只有在给定时间原始信号没有发送next事件,这个信号才会发送一个原始信号最近的一次next事件。

通过对信号的各种操作,我们把若干个水龙头连在一起,形成了一个水管。filter像是在两个水龙头之间加了一个过滤网,只有经过过滤网的水才能出现在下一个水龙头里。map像是在水龙头间加了一个转换器,前一个水龙头流出的水经过这个转换器就变成石油了。combineLatest: reduce:则是把若干个水龙头的水一起引入一个新的水龙头……

FRP ReactiveCocoa

ReactiveCocoa 试图解决什么问题

ReactiveCocoa 可以解决以下 3 个问题:

  • 传统 iOS 开发过程中,状态以及状态之间依赖过多的问题
  • 传统 MVC 架构的问题:Controller 比较复杂,可测试性差
  • 提供统一的消息传递机制

传统 iOS 开发过程中,状态以及状态之间依赖过多的问题

我们在开发 iOS 应用时,一个界面元素的状态很可能受多个其它界面元素或后台状态的影响。

例如,在用户帐户的登录界面,通常会有 2 个输入框(分别输入帐号和密码)和一个登录按钮。如果我们要加入一个限制条件:当用户输入完帐号和密码,并且登录的网络请求还未发出时,确定按钮才可以点击。通常情况下,我们需要监听这两个输入框的状态变化以及登录的网络请求状态,然后修改另一个控件的enabled状态。

传统的写法如下(该示例代码修改自 ReactiveCocoa 官网 ) :

static void *ObservationContext = &ObservationContext;

- (void)viewDidLoad {
    [super viewDidLoad];

    [LoginManager.sharedManager addObserver:self
                                 forKeyPath:@"loggingIn"
                                    options:NSKeyValueObservingOptionInitial
                                    context:&ObservationContext];
    [self.usernameTextField addTarget:self action:@selector(updateLogInButton)
                     forControlEvents:UIControlEventEditingChanged];
    [self.passwordTextField addTarget:self action:@selector(updateLogInButton)
                     forControlEvents:UIControlEventEditingChanged];
}

- (void)updateLogInButton {
    BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;
    BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
    self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
    if (context == ObservationContext) {
        [self updateLogInButton];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object
                               change:change context:context];
    }
}

RAC 通过引入信号(Signal)的概念,来代替传统 iOS 开发中对于控件状态变化检查的代理(delegate)模式或 target-action 模式。因为 RAC 的信号是可以组合(combine)的,所以可以轻松地构造出另一个新的信号出来,然后将按钮的enabled状态与新的信号绑定。如下所示:

RAC(self.logInButton, enabled) = [RACSignal
    combineLatest:@[
        self.usernameTextField.rac_textSignal,
        self.passwordTextField.rac_textSignal,
        RACObserve(LoginManager.sharedManager, loggingIn),
        RACObserve(self, loggedIn)
    ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
        return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
    }];

可以看到,在引入 RAC 之后,以前散落在action-target或 KVO 的回调函数中的判断逻辑被统一到了一起,从而使得登录按钮的enabled状态被更加清晰地表达了出来。

除了组合(combine)之外,RAC 的信号还支持链式(chaining)和过滤(filter),以方便将信号进行进一步处理。

[[self.textField.rac_textSignal filter:^BOOL(NSString*value) {  
    return [value length]>= 3;  
}] subscribeNext:^(NSString*value) {  
    NSLog(@"Text field has been updated: %@", value);  
}];  

试图解决 MVC 框架的问题

对于传统的 Model-View-Controller (MVC)的框架,Controller 很容易变得比较庞大和复杂。由于 Controller 承担了 Model 和 View 之间的桥梁作用,所以 Controller 常常与对应的 View 和 Model 的耦合度非常高,这同时也造成对其做单元测试非常不容易,对 iOS 工程的单元测试大多都只在一些工具类或与界面无关的逻辑类中进行。

MVC

RAC 的信号机制很容易将某一个 Model 变量的变化与界面关联,所以非常容易应用 Model-View-ViewModel(MVVM) 框架。通过引入 ViewModel 层,然后用 RAC 将 ViewModel 与 View 关联,View 层的变化可以直接响应 ViewModel 层的变化,这使得 Controller 变得更加简单,由于 View 不再与 Model 绑定,也增加了 View 的可重用性。

在MVVM体系中,Controller可以被看成View,所以它的主要工作是处理布局、动画、接收系统事件、展示UI。ViewModel直接与View绑定,而且对View一无所知。拿做菜打比方的话,ViewModel就是调料,它不关心做的到底是什么菜。当Model的API有变化,或者由本地存储变为远程API调用时,ViewModel的public API可以保持不变。

因为引入了 ViewModel 层,所以单元测试可以在 ViewModel 层进行,iOS 工程的可测试性也大大增强了。

MVVM

Github 开源ReactiveViewModel

统一消息传递机制

ReactiveCocoa is inspired by functional reactive programming. Rather than using mutable variables which are replaced and modified in-place, RAC offers “event streams,” represented by the Signal and SignalProducer types, that send values over time.

Event streams unify all of Cocoa’s common patterns for asynchrony and event handling, including:

  • Delegate methods

  • Callback blocks

  • NSNotifications

  • Control actions and responder chain events

  • Futures and promises

  • Key-value observing (KVO)

Because all of these different mechanisms can be represented in the same way, it’s easy to declaratively chain and combine them together, with less spaghetti code and state to bridge the gap.

iOS 开发中有着各种消息传递机制,包括 KVO、Notification、delegation、block 以及 target-action 方式。各种消息传递机制使得开发者在做具体选择时感到困惑。

RAC 将传统的 UI 控件事件进行了封装,使得以上各种消息传递机制都可以用 RAC 来完成。示例代码如下:


// KVO
[RACObserve(self, username) subscribeNext:^(id x) {
    NSLog(@" 成员变量 username 被修改成了:%@", x);
}];

// target-action
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
    NSLog(@" 按钮被点击 ");
    return [RACSignal empty];
}];

// Notification
[[[NSNotificationCenter defaultCenter]
    rac_addObserverForName:UIKeyboardDidChangeFrameNotification
                    object:nil]
    subscribeNext:^(id x) {
        NSLog(@" 键盘 Frame 改变 ");
    }
];

// Delegate
[[self rac_signalForSelector:@selector(viewWillAppear:)] subscribeNext:^(id x) {
    debugLog(@"viewWillAppear 方法被调用 %@", x);
}];

RAC 的RACSignal 类也提供了createSignal方法来让用户创建自定义的信号,如下代码创建了一个下载指定网站内容的信号。

-(RACSignal *)urlResults {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSError *error;
        NSString *result = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.devtang.com"]
                                                    encoding:NSUTF8StringEncoding
                                                       error:&error];
        NSLog(@"download");
        if (!result) {
            [subscriber sendError:error];
        } else {
            [subscriber sendNext:result];
            [subscriber sendCompleted];
        }
        return [RACDisposable disposableWithBlock:^{
            NSLog(@"clean up");
        }];
    }];

}

ReactiveCocoa缺点

InfoQ:使用ReactiveCocoa与直接使用 Cocoa框架相比,性能上(事件的响应速度、回调速度)是否会有影响?

花瓣网移动开发主管 李忠:

ReactiveCocoa底层的实现是比较复杂的,在性能上确实会有一定的影响。一个简单的 [signal subscribeNext: ^(id x){}] 就会有造成很深的callback stack(近40次的调用),相比纯KVO不到10次的调用,速度上慢了至少1个数量级。不过尽管如此,只要subscribe的次数不要过多,性能上还是可以接受的。

在事件响应上,RAC比KVO慢了大概5倍,不过问题不大,在iPhone5上测了下,也就1ms多一点,绝大多数的使用场景都不会有问题。

在开发Mac App时,可以使用Cocoa Bindings,但iOS却不支持,可能也是出于性能上的考虑。既然RAC的性能不如直接使用原生的高,还有必要用它么?我觉得还是有的,性能是我们选择框架的一个参考因素,但不是决定性的因素。开发者在足够了解RAC的情况下,RAC可以提高开发效率并帮助开发者编写更易维护的代码,这两点就值得我们去研究、使用它。

已知采用ReactiveCocoa的公司:
美团
Bilibili
花瓣网
ReactiveCocoa在花瓣客户端的实践

延伸…

ReativeCocoa vs. ReactiveX

ReactiveCocoa was originally inspired, and therefore heavily influenced, by Microsoft’s Reactive Extensions (Rx) library. There are many ports of Rx, including RxSwift, but ReactiveCocoa is intentionally not a direct port.

Where RAC differs from Rx, it is usually to:

  • Create a simpler API
  • Address common sources of confusion
  • More closely match Cocoa conventions

[Languages]

* RxJava
* RxJS
* Rx.NET
* RxScala
* RxClojure
* RxSwift
* Others

编程范式

面向代理
基于组件
    基于流
    渠道
连续式
并发计算
`声明式`(对比:命令式)
    `函数式`
        数据流
            面向细胞(电子表格)
            `响应式`
    面向图形
    目标导向
        约束
            逻辑
                回答集编程
                约束逻辑
                溯因逻辑
                归纳逻辑
事件驱动
    面向服务
    时间驱动
功能导向
函数级(对比:价值级)
`命令式`(对比:声明式)
    非结构化
        矢量(对比:标量)
        迭代式
    结构化
        过程式
            模块化
            递归式
        面向对象
            基于类
            基于原型
            自动机
            根据关注分离:
                `面向方面`
                面向主题
                面向角色
元编程
    面向属性
    自动
        泛型
            模板
                基于原则
        面向语言
            领域特定
            面向语法
                方言化
            意图
    反射式
不确定
并行计算
    面向过程
大规模编程与小规模编程
价值级(对比:函数级)

猜你喜欢

转载自blog.csdn.net/freeWayWalker/article/details/50876315
今日推荐