主要是为了总结学习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,这个才是我们所需要的。
2. RACSubject / RACReplaySubject:
RACSubject用来衔接RAC代码与非RAC代码,RACReplaySubject,“Areplaysubjectsavesthevaluesitissent(uptoitsdefinedcapacity)andresendsthosetonewsubscribers.Itwillalsoreplayanerrororcompletion.”。与RACSubject不同的是,RACReplaySubject会缓存它send的值,新的subscribers可以收到subscribe之前已经产生的值。并且可以通过设置RACReplaySubject的capacity数量来限制缓存的value的数量,即只缓充最新的几个值。
3. RACMulticastConnection:
"ThemainpurpoSEOfRACMulticastConnectionistosubscribetoabasesignal,andthenmulticastthatsubscriptiontoanynumberofothersubscribers,withouttriggeringthebasesignal'ssideeffectsmultipletimes.RACMulticastConnectionaccomplishesthisbysendingvaluestoaprivateRACSubject,whichisexposedviatheconnection'ssignalproperty.Subscribersattachtothesubject(whichdoesn'tcauseanysideeffects),andtheconnectionforwardsallofthebasesignal'seventsthere."
注意没有RACMulticastConnection的情况下,每次subscribe发生时,都会连续触发到base signal(即源signal)发生side effect,订阅是一级一级向上传递的,直到base signal,可以参考RACSignal的操作符的实现。
4. RACSignal replay / replayLast / replayLazily:
生成[RACReplaySubject subject] -> 使用subject调用[RACSignal multicast:] -> 使用multicast:返回的connection来调用[RACMulticastConnection connect] ->返回[RACMulticastConnection signal]
与publish的区别,publish在调用[RACSignal multicast:]时使用的是subject是RACSubject,即不会产生value的缓存。
1). replay->hotsignal,会立即subscribe,并且不限制RACReplaySubject的capacity数量
2). replayLast->hotsignal,会立即subscribe,与replay的区别,只是限制RACReplaySubject缓存的capacity为1,即只保留最新的一个value。
3). replayLazily->cold signal,"replayLazilydoesnotsubscribetothesignalimmediately–itlazilywaitsuntilthereisa“real”subscriber.Butreplaysubscribesimmediately.",不会立即subscribe,只有真实的subscriber订阅的时候才会subscribe,即调用subscribeNext:等的时候。同时,与replayLast不同,这里不会限制RACReplaySubject的capacity,即会保留所有的value。
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是否正在运行状态:
BOOLcommandIsExecuting=[[command.executingfirst]boolValue]; 或者 订阅command的 [command.executing subscribeNext:]
2) 创建一个cancelable command:
_twitterLoginCommand=[[RACCommandalloc]initWithSignalBlock:^(id_){
@strongify(self);
return[[self
twitterSignInSignal]
takeUntil:self.cancelCommand.executionSignals];
}];
RAC(self.authenticatedUser)=[self.twitterLoginCommand.executionSignalsswitchToLatest];
具体可以详细参考下面两篇文章以及前文中提到的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/1326,
https://github.com/ReactiveCocoa/ReactiveCocoa/issues/963
6. Subscription & SideEffect
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-subscription,
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md#make-the-side-effects-of-a-signal-explicit
RACSequence:sideeffectsoccuronlyonce.RACSignal:sideeffectsoccurforeachsubscription.
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的时候都会被处罚。
RACSignalscanbeagreatwaytomanagestatefulinteractionswithiOSframeworkandlibraries.Todothisproperly,itisveryimportanttounderstandthatwithaRACSignal,sideeffectsoccurforeachsubscription:
"EachnewsubscriptiontoaRACSignalwilltriggeritssideeffects.Thismeansthatanysideeffectswillhappenasmanytimesassubscriptionstothesignalitself."
Butasmentionedinthelinkeddocumentation,therearewaystosuppressthatbehavior.Forexample,asignalcanbemulticasted.It'salsorecommendedthatyoumakethesideeffectsofasignalexplicitwhenpossible:“ theuSEOf-doNext:,-doError:,and-doCompleted:willmakesideeffectsmoreexplicitandself-documenting “.
Tofollowthisguideline,Itrytoputallofmycallstoexternallibrariesintooneofthe"do"operators.Sometimesthatdoesn'tmakesensethough,likeinthecaSEOfasignalthatdoesnothingbesidescallanexternallibrary.Inthatcase,I'lloftenusethe+deferoperatoror+createSignal:towrapthecallwithside-effects.
5)sideeffect,指的是RACSignal被subscribe的时候,signal的subscription block就会被执行。
比如,basesignal的didSubscribeblock内执行一个异步网络请求等操作,然后在异步网络请求完成之后,subscriber就会调用相应的步骤,比如[subscribersendNext:] / [subscribersendCompletion]等。所以说,每次subscribe产生sideeffect的话,实际上就会重新创建一个signal(例如发起一个网络请求)。signal在最终subscribe发生之前,可能会经过一系列的变换,比如,basesignal--operator-->Asiganl--operator-->Bsiganl,但无论是Asignal还是Bsignal被subscribe,basesignal的didSubscribeblock都会执行,即sideeffect都会发生。
这里可以查看operator的源代码来查看了解更多细节,每个operator内部一般来说也是通过[RACSignalcreateSignal:]以及[selfsubscribeNext:error:completion:]来生成变换后的新的signal的,这里createSignal:方法的didSubscribeblock也不会立即被调用,只有在这个新的signal被subscribe的时候,才会执行这个didSubscribeblock,然后这个新signal会按subscribe上一级的signal,这样就实现了signal的链式传递subscribe,最终subscribebasesignal。
这里还有一点比较容易有误区的地方,实际上也是我一直比较困惑的地方,就是每次subscribeNext的时候,其实并不会重新生成RACSignal。只是生成一个RACSubscriber保存subcribe时候传入的block,具体实现来说,比如对于RACDynamicSignal,又会生成一个RACPassRACPassthroughSubscriber用来保持刚生成的RACSubscriber对象以及signal对象(弱引用)。这个一系列的subscribe调用过程,实际上只是生成了一系列的subscriber,并不会对RACSignal的内存有什么影响,如果最顶部的subscriber在basesignal的didSubscribeblock中没有被capture的话,当basesignal的didSubscribeblock执行完成之后,这一系列的subscriber以及didSubscribeblock会立即被释放。例如basesignaldidSubscriber-->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根本不会被调用。
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进行合并的问题。
-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的。
参考:
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的代码实际上是非常简单的,值得阅读。
参考资料:
- <FunctionalReactiveProgrammingoniOS>
- https://github.com/ReactiveCocoa/ReactiveCocoa
- https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/Documentation
- http://limboy.me/ios/2014/06/06/deep-into-reactivecocoa2.html
- http://rcdp.io/Signal.html(翻译:http://noark9.github.io/2015/01/25/rac-signal-from-rcdio/)
- http://spin.atomicobject.com/2015/03/19/reactivecocoa-asynchronous-libraries/
- http://codeblog.shape.dk/blog/2013/12/05/reactivecocoa-essentials-understanding-and-using-raccommand/
- http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/
- http://www.sprynthesis.com/2014/06/15/why-reactivecocoa/
- http://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1,http://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2
- http://marksands.github.io/2015/1/7/reacting-to-reactive-cocoa-part-iii.html
- http://spin.atomicobject.com/2014/02/03/objective-c-delegate-pattern/