ReactiveCocoa基础

首先来了解一下函数式反应型编程Functional Reactive Programming

函数式反应型编程是两个声明式编程的子范例(函数式+反应式)的组合

函数式编程

编程范式(Programming paradigm)分类

其实就是计算机编程所使用的方法,是设计程序结构所采用的设计风格。

目前主流的编程范式有:

  • 命令式编程(Imperative programming)
    • 如Pascal,C语言
    • First DO THIS and next DO THAT
    • 其他统称为声明式编程(Declarative Programming)
  • 函数式编程(Functional programming)
    • 如Haskell,Erlang, Lisp
    • Evaluate an expression and use the resulting value for something
  • 面向对象编程(Object-oriented programming)
    • 如Java,C++
    • Send messages between objects to simulate the temporal evolution of a set of real world phenomena
  • 逻辑编程(Logic Programming)
    • 如Prolog,Mercury,Logtalk
    • Answer a question via search for a solution
Resize icon
Resize icon

函数式编程特点

外链参考:函数式编程初探

  • 函数是"第一等公民"。指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
  • 只用"表达式"(expression),不用"语句"(statement)。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
  • 没有"副作用"(side effect)。意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
  • 不修改状态
  • 引用透明(Referential transparency)。指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

高阶函数

函数式编程的一个关键的概念是"高阶函数"。从维基百科的解释来看,一个高阶函数需要满足下面两个条件:

  • 一个或者多个函数作为输入。
  • 有且仅有一个函数输出。

在Objective-c中我们经常使用block作为函数。我们不需要跋山涉水地去寻找‘高阶函数’,实际上,Apple为我们提供的Foundation库中就有。考虑象下面这么简单的一个NSNumber 的数组:

NSArray * array = @[ @(1), @(2), @(3) ];

我们想要枚举这个数组的内容,可以用一个NSArray的高阶函数来实现:

[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop)
{
    NSLog(@"%@",number);
}];

函数式编程意义

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 更方便的代码管理
  • 易于"并发编程"

函数式编程和递归

外链参考:[函数式编程扫盲篇](http://www.cnblogs.com/kym/archive/2011/03/07/1976519.html

递归是函数式编程的一个重要的概念,循环可以没有,但是递归对于函数式编程却是不可或缺的。

循环是在描述我们该如何地去解决问题。

递归是在描述这个问题的定义。

考虑经典的斐波那契数列问题1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …,我们很容易从数列本身的定义得到一个递推式:f(n)=f(n-1)+f(n-2)

先看循环模型:

def Fib(n): 
    a=1 
    b=1 
    n = n - 1 
    while n>0: 
        temp=a 
        a=a+b 
        b=temp 
        n = n-1 
    return b        

递归模型:

def Fib(a): 
    if a==0 or a==1: 
        return 1 
    else: 
        return Fib(a-2)+Fib(a-1)

尾递归模型:

def Fib(a,b,n): 
    if n==0: 
        return b 
    else: 
        return Fib(b,a+b,n-1)

模拟

     a, b, n    
Fib (1, 1, 10)
Fib (1, 2, 9)
Fib (2, 3, 8)

什么是尾递归,用最通俗的话说:就是在最后一部单纯地去调用递归函数,这里我们要注意“单纯”这个字眼。

那么我们说下尾递归的原理,其实尾递归就是不要保持当前递归函数的状态,而把需要保持的东西全部用参数给传到下一个函数里,这样就可以自动清空本次调用的栈空间。这样的话,占用的栈空间就是常数阶的了。

在看尾递归代码之前,我们还是先来明确一下递归的分类,我们将递归分成“树形递归”和“尾递归”,什么是树形递归,就是把计算过程逐一展开,最后形成的是一棵树状的结构,比如之前的斐波那契数列的递归解法。

响应式编程

看下面一段代码

a = 2
b = 2
c = a + b // c 是 4
b = 3     // 现在c是多少?

在命令式编程语言中,c = a + b这个语句一旦执行完毕,a 和 b 再发生变化就和 c 无关了,c 并不会跟着变化。如果需要 c 变化时,我们一般会封装一个类似 update_c() 这样的函数,在 a 或 b 变化的时候调用一下,来更新c。

而在响应式编程的思想中,上面的语句实际上是建立了 c 和 a、b 的关联关系,这样,当 a 或 b 发生变化的时候,c 可以自动变化。

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

注意这只是一个思想,或者说是目标,我们可以使用个各种语言来实现这个目标,并不是说必须要有一种专门的响应型编程语言。当然,语言可以根据这个思路来设计,会让响应型编程的实现更为简单。

kvo, Cocoa Binding,

ReactiveCocoa

外链: ReactiveCocoa iOS的函数响应型编程 ReactiveCocoa for a better world ReactiveCocoa v2.5 源码解析之架构总览

ReactiveCocoa是函数式响应型编程的一个实现。它受 Functional Reactive Programming 的启发,是 Justin Spahr-Summers 和 Josh Abernathy 在开发 GitHub for Mac 过程中的一个副产品,它提供了一系列用来组合和转换值流的 API 。

ReactiveCocoa 的版本演进历程,简单介绍如下:

  • <= v2.5 :Objective-C ;
  • v3.x :Swift 1.2 ;
  • v4.x :Swift 2.x 。

本文所介绍的均为 ReactiveCocoa v2.5 版本中的内容,这是 Objective-C 最新的稳定版本。

ReactiveCocoa类图如下图所示

!
Resize icon

ReactiveCocoa 主要由以下四大核心组件构成:

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

RACStream

在ReactiveCocoa中,流RACStream代表的是随着时间而改变的值流(Streams of values over time)。
你可以把它想象成水龙头中的水,当你打开水龙头时,水源源不断地流出来;你也可以把它想象成电,当你插上插头时,电静静地充到你的手机上;你还可以把它想象成运送玻璃珠的管道,当你打开阀门时,珠子一个接一个地到达。这里的水、电、玻璃珠就是我们所需要的值,而打开水龙头、插上插头、打开阀门就是订阅它们的过程。

RACStream 是一个抽象类,通常情况下,我们并不会去实例化它,而是直接使用它的两个子类信号RACSignal和序列RACSequence

信号RACSignal

RACSignal代表的是未来将会被传送的值,它是一种push-driven的流。

信号又是最核心的部分,其他组件都是围绕它运作的。

对于一个应用来说,绝大部分的时间都是在等待某些事件的发生或响应某些状态的变化,比如用户的触摸事件、应用进入后台、网络请求成功刷新界面等等,而维护这些状态的变化,常常会使代码变得非常复杂,难以扩展。而 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 来说,我们可以毫不夸张地说,阻碍它发挥的瓶颈就只剩下你的想象力了。

RACSignal可以向订阅者发送三种不同类型的事件:

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

注意,ReactiveCocoa 中的值流只包含正常的值,即通过next事件传送的值,并不包括errorcompleted事件,它们需要被特殊处理。通常情况下,一个信号的生命周期是由任意个next事件和一个error事件或一个completed事件组成的。

从前面的类图中,我们可以看出,RACSignal并非只有一个类,事实上,它的一系列功能是通过类簇来实现的。除去我们将在下节介绍的 RACSubject及其子类外,RACSignal还有五个用来实现不同功能的私有子类:

  • RACEmptySignal:空信号,用来实现 RACSignal+empty方法;
  • RACReturnSignal:一元信号,用来实现 RACSignal+return:方法;
  • RACDynamicSignal:动态信号,使用一个block来实现订阅行为,我们在使用RACSignal+createSignal:方法时创建的就是该类的实例;
  • RACErrorSignal:错误信号,用来实现RACSignal+error:方法;
  • RACChannelTerminal:通道终端,代表RACChannel的一个终端,用来实现双向绑定。

对于RACSignal类簇来说,最核心的方法莫过于-subscribe:了,这个方法封装了订阅者对信号源的一次订阅过程,它是订阅者与信号源产生联系的唯一入口。因此,对于RACSignal的所有子类来说,这个方法的实现逻辑就代表了该子类的具体订阅行为,是区分不同子类的关键所在。同时,这也是为什么RACSignal中的-subscribe:方法是一个抽象方法,并且必须要让子类实现的原因:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    NSCAssert(NO, @"This method must be overridden by subclasses");
    return nil;
}

序列RACSequence

RACSequence代表的是一个不可变的值的序列,与RACSignal不同,它是pull-driven类型的流。从严格意义上讲,RACSequence并不能算作是信号源,因为它并不能像RACSignal那样,可以被订阅者订阅,但是它与RACSignal之间可以非常方便地进行转换。序列提供了Foundation没有的一些高阶函数如map, filter, fold等。

使用rac_sequeuece我们能够轻松地将数组转化为一个序列:

NSArray *array = @[ @1, @2, @3 ];
RACSequence * stream = [array rac_sequence];

我们可以将流应用在平方数映射上,然后转化回一个数组:

[stream map:^id (id value){
    return @(pow([value integerValue], 2));
}];
NSLog(@"%@",[stream array]);

当然,我们可以合并上面的方法调用来避免污染变量的作用域.

NSLog(@"%@",[[[array rac_sequence] map:^id (id value){
                return @(pow([value integerValue], 2));
            }] array]);

我们来看一下filtering。为了使用ReactiveCocoa来过滤我们的数组,我们需要再一次把它序列化以便于使用过滤。

NSLog(@"%@", [[[array rac_sequence] filter:^BOOL (id value){
                    return [value integerValue] % 2 == 0;
                }] array]);

最后看一下怎么让一个序列流合并为单个值(folding):

NSLog(@"%@",[[[array rac_sequence] map:^id (id value){
                return [value stringValue];
            }] foldLeftWithStart:@"" reduce:^id (id accumulator, id value){
                return [accumulator stringByAppendingString:value];
        }]);

因此,我们可以非常方便地使用 RACSequence 来实现集合的链式操作,直到得到你想要的最终结果为止。除此之外,使用 RACSequence 的另外一个主要好处是,RACSequence 中包含的值在默认情况下是懒计算的,即只有在真正用到的时候才会被计算,并且只会计算一次。也就是说,如果我们只用到了一个 RACSequence 中的部分值的时候,它就在不知不觉中提高了我们应用的性能。

同样的,RACSequence 的一系列功能也是通过类簇来实现的,它共有九个用来实现不同功能的私有子类。RACSequence 为类簇提供了统一的对外接口,对于使用它的客户端代码来说,完全不需要知道私有子类的存在,很好地隐藏了实现细节。另外,值得一提的是,RACSequence 实现了快速枚举的协议 NSFastEnumeration ,在这个协议中只声明了一个看上去非常抽筋的方法:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;

有兴趣的同学,可以看看 RACSequence 中的相关实现,我们将会在后续的文章中进行介绍。因此,我们也可以直接使用 for in 来遍历一个 RACSequence 。

  • BlockKit类似
  • 看源码感觉性能不是很好,有待验证。对于几个、几十个的小数组,应该问题不大

订阅者RACSubscriber

调度器RACScheduler

清洁工RACDisposable

RACCommand

ReactiveCocoa Essentials: Understanding and Using RACCommand

ReactiveCocoa对富途牛牛的一些启发

界面控件绑定

将控件emailTextField内容赋值给self.viewModel.email

RAC(self.viewModel, email) = self.emailTextField.rac_textSignal;  

将self.viewModel.statusMessage显示到statusLabel控件

RAC(self.statusLabel, text) =RACObserve(self.viewModel, statusMessage);  

统一处理所有异步的行为

使用信号来代表这些异步事件,提供了一种统一的方式来处理所有异步的行为,包括代理方法、block 回调、target-action 机制、通知、KVO

富途牛牛项目:

  • 解决KVO订阅忘记取消订阅的问题
  • 解决订阅通知忘记取消的问题

链式依赖的操作

依赖关系通常出现在网络请求中,如后一个请求应该等前一个请求完成后再创建,等等:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject   success:^(NSArray *nextMessages) {
         NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];

ReactiveCocoa 可以特别方便地处理这种逻辑模式:

[[[[client logIn]
    then:^{
        return [client loadCachedMessages];
    }]
    flattenMap:^(NSArray *messages) {
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeError:^(NSError *error) {
        [self presentError:error];
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

TCP短连接可以这样优化

[[[[service connect] flattenMap:^RACStream *(id value) {
    return [service doSomething1];
}] flattenMap:^RACStream *(id something1Value) {
    // if doSomething1 is successful,   'somethingValue' is passed via sendNext
    return [service disconnect];
}] subscribeError:^(NSError *error) {
    // Error occurred!  Handle "error" if necessary.
} completed:^{
    // Asynchronous chain of operations succeeded.
}];

在异步操作上使用signals信号,让通过链接和转换这些signal信号,构建更加复杂的行为成为可能.可以在一组操作完成后,来触发此操作即可:

// 执行两个网络操作,并在它们都完成后在控制台打印信息.
//
// +merge: 传入一组signal信号,并返回一个新的RACSignal信号对象.这个新返回的RACSignal信号对象,传递所有请求的值,并在所有的请求完成时完成.即:新返回的RACSignal信号,在每个请求完成时,都会发送个消息;在所有消息完成时,除了发送消息外,还会触发"完成"相关的block.
//
// -subscribeCompleted: signal信号完成时,将会执行block.
[[RACSignal 
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] 
    subscribeCompleted:^{
     NSLog(@"They're both done!");
    }];

富途牛牛项目,将很多异步操作做成独立的RACSigal, 方便单元测试,方便组织先后顺序、依赖关系。比如优化启动流程、登录流程。

网络请求避免过于频繁

股票网络搜索

 [[[[[[[[[self.textField.rac_textSignal
        filter:^BOOL(id value) { //过滤
            return [value length] > 0;
        }]
       throttle:0.3] //无任何输入0.3秒后继续
      logAll]
     flattenMap:^id(id value) { //flattenMap有新的搜索请求后,上一次网络请求结果会被忽略
         return [self search:value];
     }]
    logAll]
   map:^id(NSArray *array) {
       return [[[array rac_sequence] map:^id(id value) {
           Stock *stock = [[Stock alloc] init];
           stock.name = value[@"name"];
           stock.symbol = value[@"symbol"];
           stock.exch = value[@"exchDisp"];
           return stock;
       }] array];
   }]
  logAll]
 deliverOn:[RACScheduler mainThreadScheduler]]
 subscribeNext:^(id x) {
     self.stocks = x;
 } error:^(NSError *error) {
     
 } completed:^{
     
 }];

一些坑

block要记得用strongify/weakify

因为RAC很多操作都是在Block中完成的,这块最常见的问题就是在block直接把self拿来用,造成block和self的retain cycle。所以需要通过@strongify和@weakify来消除循环引用。

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

RACObserve(self, model.title) 与 RACObserve(self.model, title)

// 描述: self 本身有一个title属性和一个model属性,model本身也有一个title属性.
RAC(self, title, @"") = RACObserve(self, model.title);
RAC(self, title, @"") = RACObserve(self.model, title);

这两行代码,有着质的不同!

RAC(self, title, @"") = RACObserve(self, model.title);

适用场景: self的model属性改变时,动态改变self自身title属性的值,其值为新model的title属性.

RAC(self, title, @"") = RACObserve(self.model, title);

适用场景: self的model属性的title属性改变时,动态改变self自身title属性的值,其值为原有model的title属性.

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容