ReactiveCocoa & MVVM 学习总结一

▼魔方 西西 提交于 2019-12-01 16:57:08

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

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

把自己的学习心得写了一个小demo,放在了github上面,欢迎一起学习交流:https://github.com/lihei12345/RACNetwokDemo

=====================================================================

一. ReactiveCocoa

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

1. RACSignal / RACSequence: 

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:]

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的代码实际上是非常简单的,值得阅读。

参考资料:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!