ReactiveCocoa小总结

我的Github地址 : Jerry4me, 本文章的demo链接 : JRReactiveCocoa

RAC与MVVM如今已经不是一个新鲜的玩意了, 对于介绍他们两的精品文章更是大把, 这篇文章主要是用来记录自己学习RAC的过程以及RAC的一些用法, 以防以后要用到的时候却记不起来了.

具体RAC的用法以及本文出现的代码均能在我的 Github上, 另外附有2个MVVM的小demo. 欢迎大家查看, 赏脸的给个star~


RAC编程思想

编程学的是思想, 学一样东西最主要是学会它的思想, 那才是它的灵魂, 而不是学习调用方法而已.

RAC又被称为FRP, 函数响应式编程.

何为函数式? 把操作写成一系列嵌套的函数或者方法调用

[[[[Person getup] eat] run] goHome]; 

何为响应式? 不需要考虑调用顺序, 只考虑结果. 一个属性, 一个请求改变马上引发一系列改变.

data stream -> (filter, combine, map, ...) -> another stream
stream是基于时间上的事件流

所以RAC即糅合了函数式和响应式编程的优点, 使用RAC编程不需要考虑代码调用顺序, 只需要考虑结果. 把每一个操作都写成一系列的嵌套的方法, 使代码变得高内聚, 低耦合.


RAC使用场景

数据随着时间而产生, 例如以下三点 :

  1. UI操作, 连续的动作和动画部分, 例如某些控件跟随滚动
  2. 网络库, 因为数据是在一定时间后才返回回来, 不是立刻返回的
  3. 刷新的业务逻辑, 当触发点是多种的时候, 业务往往会变得很复杂, 用delegate, notification, observe混用, 难以统一. 这时用RAC能保证上层的高度一致性, 从而简化逻辑上分层.

RAC类关系图

RAC类的关系图如下, 下面会抽出一部分类进行讲解, 另外有部分类与用法会在github上的demo上看得到, 还有部分类将不在本文中出现, 本文(demo)只说明了一些常用的类与方法.

ReactiveCocoa类图.png

信号源

RACStream.ng

RACSignal

RACSignal只会向订阅者发送三种事件 : next, errorcompleted.

RACSignal的一系列功能是通过类簇来实现的. 如 :

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

核心方法 : -subscribe:.

RACSubject

继承自RACSignal, 是可以手动控制的信号, 相当于RACSignal的可变版本.

能作为信号源被订阅者订阅, 又能作为订阅者订阅其他信号源(实现了RACSubscriber协议).

RACSubject有三个用来实现不同功能的子类 :

RACGroupedSignal :分组信号,用来实现 RACSignal 的分组功能;
RACBehaviorSubject :重演最后值的信号,当被订阅时,会向订阅者发送它最后接收到的值;
RACReplaySubject :重演信号,保存发送过的值,当被订阅时,会向订阅者重新发送这些值。

RACSequence

代表的是一个不可变的值的序列. 不能被订阅者订阅, 但是能与RACSignal之间非常方便地进行转换.

RACSequence由两部分组成 : headtail, head是序列中的第一个对象, tail则是其余的全部对象.

RACSequence存在的最大意义就是简化OC中的集合操作. 并且RACSequence所包含的值默认是懒计算的, 所以不知不觉中提高了我们应用的性能.

push-driven与pull-driven

  • RACSignal : push-driven, 生产一个吃一个, 类似于工厂的主动生产模式, 生产出产品就push给供销商.

  • RACSequence : pull-driven, 吃一个生产一个, 类似于工厂的被动生产模式, 供销商过来pull的时候才现做产品.

对于RACSignal的push-driven模式来说, 没有供销商(subscriber)签合同要产品, 当然就不生产了. 只有一个以上准备收货的供销商时, 工厂才开始生产. 这就是RACSignal的休眠(cold)和激活(hot)状态, 也就是冷信号热信号. 一般情况下RACSignal创建以后都处于cold状态, 当有人去subscribe才变成hot状态.

冷信号与热信号

热信号 : 主动, 即使你没有订阅事件, 仍然会时刻推送. 热信号可以有多个订阅者, 是一对多的关系, 信号可以与订阅者共享信息.

冷信号 : 被动, 只有当你订阅的时候, 它才会发布消息. 冷信号只能一对一, 当有不同的订阅者, 消息是重新完整发送的.

ps : 任何的信号转换即是对原有信号进行订阅从而产生新的信号. (例如 : Map, FlattenMap等等)

如何区分热信号和冷信号

Subject类似于直播, 错过了就不再处理, 而Signal类似于点播, 每次订阅都从头开始重新发送.

我们能得出 :

  1. RACSubject及其子类是热信号
  2. RACSignal排除RACSubject类以外的都是冷信号

将冷信号转化成热信号

RAC帮我们封装了一套可以轻松将冷信号转换成热信号的API :

- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily; // 跟replay的区别是replayLazily会在第一次订阅的时候才订阅sourceSignal

其中最重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;, 其他几个方法都是间接调用它的.

本质 : 使用一个Subject来订阅原始信号, 并让其他订阅者订阅这个Subject, 由于RACSubject本身为热信号, 所以源信号此时就像由冷信号变成了热信号.


订阅者

RACSubscriber

其中 -sendNext:, -sendError:-sendCompleted 分别用来从RACSignal接收 next, errorcompleted 事件, 而-didSubscribeWithDisposable:则用来接收代表某次订阅的disposable对象.

一个RACDisposable对象就代表这一次订阅, 并且我们可以用它来取消这次订阅.

RACSubscriber就是真正的订阅者, 而RACPassthroughSubscriber可以使得一个订阅者可以订阅多个信号源, 即拥有多个RACDisposable对象, 并能随时取消其中的任何一次订阅. 为了实现这个功能, RAC就引入了RACPassthroughSubscriber类, 它是RACSubscriber类的一个装饰器, 封装了一个真正的订阅者 RACSubscriber 对象, 它负责转发所有事件给这个真正的订阅者, 而当此次订阅被取消时, 它就会停止转发

RACMulticastConnection

RACMulticastConnection.png

使得不管外面有多少个订阅者, 对源信号的订阅只会有一次. 为了防止副作用的产生, 使用的便是multicast机制

multicast的机制

机制一 : 能防止某信号被多次订阅时调用多次didSubscribe block产生副作用.

机制二 : 实现replay, 即每当有订阅者订阅时, 会将之前缓存中的sendNext重新发送给该订阅者.

副作用

  • 函数的处理过程中, 修改了外部的变量(例如 : 全局变量, 成员变量等)
  • 函数的处理过程中, 出发了一些额外的动作(例如 : 发送了一个全局的Notification, 在console打印了一行信息, 保存了文件, 触发了网络, 更新了屏幕等)
  • 函数的处理过程中, 受到外部变量的影响(例如 : 全局变量, 成员变量等, block中捕获到的外部变量也算)
  • 函数的处理过程中, 受到线程锁的影响

以上都算副作用. 然而冷信号有可能因为有多个订阅者订阅而产生极大的副作用, 例如发送了同一个网络请求若干次, 同一个计算做了若干次等等, 这些问题都可以通过把这个冷信号转化成热信号得以解决.

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"创建");
        
/* 发送网络请求 */
[subscriber sendNext:@"data"];
[subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
       NSLog(@"销毁");
    }];
}];
    
[signal subscribeNext:^(id x) { // 第一个订阅者
    NSLog(@"id = %@", x);
}];
    
[signal subscribeNext:^(id x) { // 第二个订阅者
    NSLog(@"id2 = %@", x);
}];
    
/*
    控制台输出为 :
     2017-03-13 15:48:09.632 使用cocoapods[41347:10397774] 创建
     2017-03-13 15:48:09.634 使用cocoapods[41347:10397774] id = data
     2017-03-13 15:48:09.636 使用cocoapods[41347:10397774] 销毁
     2017-03-13 15:48:09.637 使用cocoapods[41347:10397774] 创建
     2017-03-13 15:48:09.638 使用cocoapods[41347:10397774] id2 = data
     2017-03-13 15:48:09.639 使用cocoapods[41347:10397774] 销毁
       
     由此可见有多个订阅者订阅了该信号源的话, 就会多次调用信号源block中的方法, 产生副作用
*/

调度器

RACScheduler

RAC中对GCD的简单封装. 子类如下 :

RACImmediateScheduler :立即执行调度的任务,这是唯一一个支持同步执行的调度器;
RACQueueScheduler :一个抽象的队列调度器,在一个 GCD 串行列队中异步调度所有任务;
RACTargetQueueScheduler :继承自 RACQueueScheduler ,在一个以一个任意的 GCD 队列为 target 的串行队列中异步调度所有任务;
RACSubscriptionScheduler :一个只用来调度订阅的调度器。

清洁工

RACDisposable

在订阅者订阅信号源的过程中, 可能会产生副作用或者消耗一定的资源, 所以在取消订阅或完成订阅的时候我们就需要做一些资源回收和辣鸡清理的工作. 核心方法为-dispose

RACSerialDisposable :作为 disposable 的容器使用,可以包含一个 disposable 对象,并且允许将这个 disposable 对象通过原子操作交换出来;
RACKVOTrampoline :代表一次 KVO 观察,并且可以用来停止观察;
RACCompoundDisposable :跟 RACSerialDisposable 一样,RACCompoundDisposable 也是作为 disposable 的容器使用。不同的是,它可以包含多个 disposable 对象,并且支持手动添加和移除 disposable 对象,有点类似于可变数组 NSMutableArray 。而当一个 RACCompoundDisposable 对象被 disposed 时,它会调用其所包含的所有 disposable 对象的 -dispose 方法,有点类似于 autoreleasepool 的作用;
RACScopedDisposable :当它被 dealloc 的时候调用本身的 -dispose 方法。

总的来说就是在适当的时机调用disposable对象的-dispose方法而已.


RAC常见宏

用法在demo中

1. RAC(TARGET, [KEYPATH, [NIL_VALUE]])  -> 总是出现在等号左边, 等号右边是一个RACSignal  
2. RACObserve(TARGET, KEYPATH)  -> 产生一个RACSignal    
3. @weakify(self) 和 @strongify(self)    
4. RACTuplePack 和 RACTupleUnpack  -> 压包与解包
5. @keypath(self.property)  -> 产生一个字符串@"property"

RAC中潜在的内存泄漏及解决方法

RACObserve

如果在block中使用到了RACObserve, 则必须加上@weakify@strongify, 尽管没有显示使用到了self. 文档事例如下 :

@weakify(self);
RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
    // Avoids a retain cycle because of RACObserve implicitly referencing self
    @strongify(self);
    return RACObserve(arrayController, items);
}];

RACSubject

RACSubject实例进行map操作之后, 发送完毕一定要调用-sendCompleted, 否则会出现内存泄漏; 而RACSignal实例不管是否进行map操作, 不管是否调用-sendCompleted, 都不会出现内存泄漏.

原因 : 因为RACSubject是热信号, 为了保证未来有事件发生的时候, 订阅者可以收到信息, 所以需要对持有订阅者!

ps : 几乎所有操作底层都会调用bind这样一个方法, 包括但不限于以下方法 : map, filter, merge, combineLatest, flattenMap...

map : 
    map -> flattenMap -> bind
filter : 
    filter -> flattenMap -> bind

所以 : 对信号操作完成记得发送-sendCompleted. (或者-sendError).

线程安全

Signal events是线性的, 不会出现并发的情况, 除非显示地指定Scheduler. 所以-subscribeNext:里的block不需要加锁, 其他的events会依次排队, 直到block处理完成.

为了方便调试, 最好给信号指定Name : -setNameWithFormat:


参考文章 :

ReactiveCocoa 和 MVVM 入门

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

推荐阅读更多精彩内容