ReactiveCocoa & MVVM 学习总结一

主要是为了总结学习RAC的过程当中,遇到的一些困惑点,一些阅读的参考资料,文笔也不是很好。建议你们学习RAC参考文章:html

https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/Documentation以及花瓣工程师的一篇很棒的文章: http://limboy.me/ios/2014/06/06/deep-into-reactivecocoa2.htmlreact

把本身的学习心得写了一个小demo,放在了github上面,欢迎一块儿学习交流:https://github.com/lihei12345/RACNetwokDemoios

=====================================================================git

一. ReactiveCocoagithub

monad术语:  “It’s a specific way of chaining operations together. ” ,  http://stackoverflow.com/questions/44965/what-is-a-monadobjective-c

1. RACSignal / RACSequence: sql

RACSignal与RACSequence是能够相互转换的。RACSignal是push-driven的,RACSequence是pull-driven的。push-driven的意思是在signal建立的时候,signal的values尚未定义,在稍后的某个时间点上,values才能准备好,好比,网络请求结果或者用户输入产生的values。而pull-driven的意思是在sequence建立的时候values就已经被定义了,能够从sequence中把这些values one-by-one 查询出来。缓存

1). RACSequence -> RACSignal :网络

      [sequence signal]或者[sequence signalWithScheduler:]app

2). RACSignal -> RACSequence :

     [[signal toArray] rac_sequence],注意-toArray方法是阻塞式的,一直到signal completes以后才会继续。或者使用[signal sequence]方法,这个方法尽管不会等待signal completes才会继续,可是须要signal至少有一个value,因此当signal一个value都没有的时候,仍然会阻塞。

在实际中,RACSequence使用的并很少,通常就是用来操做Cocoa collections,好比NSArray,NSSet, NSDictionary,NSIndexSet。咱们最感兴趣和最经常使用的仍是RACSignal,由于signals表明着将来的values,这个才是咱们所须要的。

参考: http://rcdp.io/Signal.html

2. RACSubject / RACReplaySubject: 

RACSubject用来衔接RAC代码与非RAC代码,RACReplaySubject,“A replay subject saves the values it is sent (up to its defined capacity) and resends those to new subscribers. It will also replay an error or completion.”。与RACSubject不一样的是,RACReplaySubject会缓存它send的值,新的subscribers能够收到subscribe以前已经产生的值。而且能够经过设置RACReplaySubject的capacity数量来限制缓存的value的数量,即只缓充最新的几个值。

3. RACMulticastConnection:

"The main purpose of RACMulticastConnection is to subscribe to a base signal, and then multicast that subscription to any number of other subscribers, without triggering the base signal's side effects multiple times. RACMulticastConnection accomplishes this by sending values to a private RACSubject, which is exposed via the connection's signal property. Subscribers attach to the subject (which doesn't cause any side effects), and the connection forwards all of the base signal's events there."

注意没有RACMulticastConnection的状况下,每次subscribe发生时,都会连续触发到base signal(即源signal)发生side effect,订阅是一级一级向上传递的,直到base signal,能够参考RACSignal的操做符的实现。

4. RACSignal replay / replayLast / replayLazily:

用于避免subscribe产生屡次side effect,查看源代码这三个方法的大体调用过程:

生成[RACReplaySubject subject] -> 使用subject调用[RACSignal multicast:]  -> 使用multicast:返回的connection来调用 [RACMulticastConnection connect]  -> 返回[RACMulticastConnection signal]

与publish的区别,publish在调用[RACSignal multicast:]时使用的是subject是RACSubject,即不会产生value的缓存。

1). replay -> hot signal,会当即subscribe,而且不限制RACReplaySubject的capacity数量

2). replayLast -> hot signal,会当即subscribe,与replay的区别,只是限制RACReplaySubject缓存的capacity为1,即只保留最新的一个value。

3). replayLazily -> cold signal,"replayLazily does not subscribe to the signal immediately – it lazily waits until there is a “real” subscriber. But replay subscribes immediately.",不会当即subscribe,只有真实的subscriber订阅的时候才会subscribe,即调用subscribeNext:等的时候。同时,与replayLast不一样,这里不会限制RACReplaySubject的capacity,即会保留全部的value。

参考:  http://spin.atomicobject.com/2014/06/29/replay-replaylast-replaylazily/

5. RACCommand

“A command, represented by the RACCommand class, creates and subscribes to a signal in response to some action. This makes it easy to perform side-effect work as the user interacts with the app” — FrameworkOverview

“A command is a signal triggered in response to some action, typically UI-related” — header file

RACComand是利用相似side effect的方式来实现的,触发RACCommand的时候,执行command的execute:方法,内部会调用建立RACCommand时传入的signalBlock()来得到一个signal对象,而后subscribe这个signal来改变RACCommand的各个状态。

RACCommand有几个很重要的属性: executionSignals/errors/executing。

1) 判断command是否正在运行状态:

BOOL commandIsExecuting = [[command.executing first] boolValue]; 或者 订阅command的 [command.executing subscribeNext:]

2) 建立一个cancelable command: 

_twitterLoginCommand = [[RACCommand alloc] initWithSignalBlock:^(id _) {

      @strongify(self);

      return [[self 

          twitterSignInSignal] 

          takeUntil:self.cancelCommand.executionSignals];

    }];

RAC(self.authenticatedUser) = [self.twitterLoginCommand.executionSignals switchToLatest];

具体能够详细参考下面两篇文章以及前文中提到的github上面的demo:

(1) http://codeblog.shape.dk/blog/2013/12/05/reactivecocoa-essentials-understanding-and-using-raccommand/,这篇文章中对RACCommand的使用讲解很是不错

(2) dispose RACCommand: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1326https://github.com/ReactiveCocoa/ReactiveCocoa/issues/963

6. Subscription & Side Effect

1) 能够理解 subscription block 就是经过 createSignal : 建立RACDynamicSignal时传入的block,在RACDynamicSignal中全局变量是didSubscribe block,当咱们调用signal的subscribe:方法的时候,核心操做就是调用didSubscribeBlock。这个block被调用的时候,就是Side Effect发生的时候。

2) From:  https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md#side-effects-occur-for-each-subscriptionhttps://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md#make-the-side-effects-of-a-signal-explicit

RACSequence: side effects occur only once. RACSignal: side effects occur for each subscription. 

3) From: http://rcdp.io/Signal.html

signals是经过subscription链接在一块儿的,在subscription block中定义的actions只有在subscription发生的时候才会发生。具体来讲,好比咱们使用createSignal:建立的dynamic signal只有当被订阅的时候,subscription block才会被真正触发,若是咱们在这个block中定义了网络请求,这个时候网络请求才会真正地被触发。注意不管中间通过signal多少变换,source signal的subscription block在最终的signal被subscribe的时候都会被处罚。

4) From: http://spin.atomicobject.com/2015/03/19/reactivecocoa-asynchronous-libraries/ 

RACSignals can be a great way to manage stateful interactions with iOS framework and libraries. To do this properly, it is very important to understand that with a RACSignal, side effects occur for each subscription:

"Each new subscription to a RACSignal will trigger its side effects. This means that any side effects will happen as many times as subscriptions to the signal itself."

But as mentioned in the linked documentation, there are ways to suppress that behavior. For example, a signal can be multicasted.It's also recommended that you make the side effects of a signal explicit when possible: “ the use of -doNext:, -doError:, and -doCompleted: will make side effects more explicit and self-documenting “.

To follow this guideline, I try to put all of my calls to external libraries into one of the "do" operators. Sometimes that doesn't make sense though, like in the case of a signal that does nothing besides call an external library. In that case, I'll often use the +defer operator or +createSignal: to wrap the call with side-effects.

5) side effect,指的是RACSignal被subscribe的时候,signal的subscription block就会被执行。

好比,base signal的didSubscribe block内执行一个异步网络请求等操做,而后在异步网络请求完成以后,subscriber就会调用相应的步骤,好比[subscriber sendNext:] / [subscriber sendCompletion]等。因此说,每次subscribe产生side effect的话,实际上就会从新建立一个signal(例如发起一个网络请求)。signal在最终subscribe发生以前,可能会通过一系列的变换,好比,base signal --operator--> A siganl --operator--> B siganl,但不管是A signal仍是B signal被subscribe,base signal的didSubscribe block都会执行,即side effect都会发生。

这里能够查看operator的源代码来查看了解更多细节,每一个operator内部通常来讲也是经过[RACSignal createSignal:]以及[self subscribeNext:error:completion:]来生成变换后的新的signal的,这里createSignal:方法的didSubscribe block也不会当即被调用,只有在这个新的signal被subscribe的时候,才会执行这个didSubscribe block,而后这个新signal会按subscribe上一级的signal,这样就实现了signal的链式传递subscribe,最终subscribe base signal。

这里还有一点比较容易有误区的地方,实际上也是我一直比较困惑的地方,就是每次subscribeNext的时候,其实并不会从新生成RACSignal。只是生成一个RACSubscriber保存subcribe时候传入的block,具体实现来讲,好比对于RACDynamicSignal,又会生成一个RACPassRACPassthroughSubscriber用来保持刚生成的RACSubscriber对象以及signal对象(弱引用)。这个一系列的subscribe调用过程,实际上只是生成了一系列的subscriber,并不会对RACSignal的内存有什么影响,若是最顶部的subscriber在base signal的didSubscribe block中没有被capture的话,当base signal的didSubscribe block执行完成以后,这一系列的subscriber以及didSubscribe block会当即被释放。例如 base signal didSubscriber --> subscriber --> (operator)didSubscribe --> subscriber --> didSubscribe...

7. @weakify, @strongify的原理,注意能够只使用一次@weakify便可,但必须屡次使用@strongify,每一个用到self的block层次都须要使用@strongify来修修饰才能保证不出现retain cycle:http://stackoverflow.com/questions/21716982/explanation-of-how-weakify-and-strongify-work-in-reactivecocoa-libextobjc。这里有一个须要注意的地方,在block内使用全局变量,也会capture self,也须要使用@strongify来避免内存问题。

8. [RACMulticastConnection  autoConnect:] : cold signal.  [RACMulticastConnection connect:] : hot signal.

使用[RACMulticastConnection connect]时,signal没法进行dispose,必须使用[RACMulticastConnection autoConnect]才能够进行dispose ;因为全部的replay*默认都是使用connection,因此,全部的replay*没法进行dispose,side effect中返回的dispose根本不会被调用。

参考: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1110

9. switchToLatest / flattenMap:


-flattenMap:将stream的每一个value都转换为一个新的stream,而后全部这些新生成的streams会被flatten合成为一个新的stream返回。换句话说,这个过程当中,先执行-map:,再执行-flatten。虽然咱们平时并不会常用这个operator,就像官方文档说的。这个operator最有趣的地方不是它自身,咱们平常用到这个操做符的场景并很少,而是须要理解它是如何工做的。下面这段翻译来自于: http://rcdp.io/flattenMap-vs-map-switchToLatest.html,对-flattenMap: 的描述很是清晰。

在FRP理论中,具体一些关于Monad的概念中,-flattenMap: operator 是驱动整个signal链式调用的核心机制。-flattenMap: 将每一个来自于source signal的value变换为另外新的signal,所以会建立一个新的signal-of-signals类型的signal。而后这个signal-of-singles会被flatten,最终返回一个包含全部nested signals中的values的signal。当看到 -map: 方法的实现时,就会发现是经过调用 -flattenMap: 方法来实现的,这个可能会有些困惑。可是当你想起 RACStream 是一个 Monad的时候,这句话就有意义了:全部两个Monads(RACStream)之间的connections都必须是经过 -flattenMap: 进行表达的。可是注意的是,RACSignal中并非全部操做都是经过-flattenMap:实现的。

虽然-flattenMap:咱们平常场景中使用并很少,主要是用于被其余操做符使用,可是这篇文章中仍是总结了一些颇有意思的使用场景能够参考一下(http://spin.atomicobject.com/2014/12/22/reactivecocoa-flattenmap-operator/):

1). Incremental Loading

不少应用为了加强用户体验,一个页面的数据是被拆分多个请求返回的,等待第一个请求返回一些基本数据以后,才会发起后续的请求,这样的小请求返回更快,能够提早渲染部分UI给用户,让用户不用等待全部的数据返回才能操做。同时也能减轻后台的开发难度,不用维护很大的接口很复杂的sql。使用-flattenMap:能够很容易解决这个问题:


2). Mapping Bad Values to Errors

有的状况下,HTTP请求正常,可是返回的数据是无效的,这个时候,咱们须要本身在subscribeNext:的时候进行判断,好比下面:


不过,能够经过-flattenMap:操做符在处理网络数据的时候,直接把无效的数据直接映射为error,这样就不用在写业务代码的时候作判断了,好比:


而后在业务层处理的时候,就不用考虑数据无效的问题了



注意,-switchToLatest:,必须做用于signal-of-signals类型的signal (它全部的value都是signal,好比,调用sendNext:时发送value必须是signal)。这个操做符会自动切换到最后一个signal,并将前一个signal dispose。通常和-map:结合使用较多,比-flattenMap:更为经常使用。-switchToLatest:的代码比较简单,内部也是利用-flattenMap: 和 takeUntil:结合实现的,内部也是调用flattenMap:,可是结合takeUntile:以后,在有新的signal时,就会将以前的signal dispose,这样就避免了会将多个signal进行合并的问题。

用法参考,http://spin.atomicobject.com/2014/05/21/reactivecocoa-understanding-switchtolatest/


From: http://rcdp.io/flattenMap-vs-map-switchToLatest.html

-map:  + -switchToLatest 与 -flattenMap: 的功能很是接近,主要的一个区别是,前者映射 incoming events 得到的signals不会像后者同样被合并为一个signal。反而在-switchToLates,这一些列signals会被按顺序处理,一旦收到incoming events映射得到的新signal,当前被subscribes的signal就会被unsubscribes,即被dispose,而后subscribe这个新得到的signal。因此,最终咱们只会看到最新后的这个signal的输出,以前的signal都会被dispose。

作了一段代码测试,在 github 上面 https://github.com/lihei12345/RACNetwokDemo

-flattenMap:


输出以下:


-map: + -switchToLatest: 


输出以下:


经过上面两段代码能够看出,switchToLatest会把以前的给dispose的。

参考:

1). http://spin.atomicobject.com/2014/05/21/reactivecocoa-understanding-switchtolatest/

2). http://spin.atomicobject.com/2014/12/22/reactivecocoa-flattenmap-operator/

3). http://rcdp.io/flattenMap-vs-map-switchToLatest.html

4). https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/BasicOperators.md#mapping-and-flattening

5). https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/BasicOperators.md#switching

10. -materialize

这个operator的实现代码很是简单,可是仍是比较有用的,返回一个signal,这个新的signal将receiver的每一个event都转换为RACEvent对象而后发送,即便当receiver sendError:和sendComplete的时候,这个signal也是先发送一个RACEvent对象,而后才会sendError:或者sendComplete。因此对于这个新的signal,只须要subscribeNext:便可,判断收到的RACEvent的type就行,再也不须要再分别在next/error/block内处理不一样的逻辑。

11. -bind: 


bind:的源代码仍是比较清晰易懂的,可是这个operator没有具体的应用场景。bind:主要作的事情,按我本身的理解就是实现了多个signals的flatten操做,也就是将多个signals合并为一个新的signal-of-signals类型的signal,以后每一个signal的next/error event都能被send到这个新的signal-of-signals之中。可是只有当全部的signal都complete的时候,signal-of-signals的complete event才会被send。包括flattenMap:在内的不少operator内部都是使用这个bind:实现的,因此这个operator是一个很是核心的operator,目前我写RAC代码并很少,可是感受这个操做符是使用signal-of-signals的核心。不过,这个operator的代码其实是很是简单的,值得阅读。

参考资料:

相关文章
相关标签/搜索