ReactiveCocoa 信号1

Native app有很大一部分的时间是在等待事件发生,然后响应事件,比如等待网络请求完成,等待用户的操作,等待某些状态值的改变等等,等这些事件发生后,再做进一步处理。 但是这些等待和响应,并没有一个统一的处理方式。Delegate, Notification, Block, KVO, 常常会不知道该用哪个最合适。有时需要chain或者compose某几个事件,就需要多个状态变量,而状态变量一多,复杂度也就上来了。为了解决这些问题,Github的工程师们开发了ReactiveCocoa。

几个常见的概念

在阅读ReactiveCocoa(以下简称RAC)的相关文章或代码时,经常会出现一些名词,理解它们对于理解RAC有很大的帮助,下面就简要来说说这些常见的概念。

Signal and Subscriber

这是RAC最核心的内容,这里我想用插头和插座来描述,插座是Signal,插头是Subscriber。想象某个遥远的星球,他们的电像某种物质一样被集中存储,且很珍贵。插座负责去获取电,插头负责使用电,而且一个插座可以插任意数量的插头。当一个插座(Signal)没有插头(Subscriber)时什么也不干,也就是处于冷(Cold)的状态,只有插了插头时才会去获取,这个时候就处于热(Hot)的状态。

Signal获取到数据后,会调用Subscriber的sendNext, sendComplete, sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。这样只要没有sendComplete和sendError,新的值就会通过sendNext源源不断地传送过来,举个简单的例子:

  1. [RACObserve(self, username) subscribeNext: ^(NSString *newName){
  2. NSLog(@"newName:%@", newName);
  3. }];

RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。

Signal是很灵活的,它可以被修改(map),过滤(filter),叠加(combine),串联(chain),这有助于应对更加复杂的情况,比如:

  1. RAC(self.logInButton, enabled) = [RACSignal
  2. combineLatest:@[
  3. self.usernameTextField.rac_textSignal,
  4. self.passwordTextField.rac_textSignal,
  5. RACObserve(LoginManager.sharedManager, loggingIn),
  6. RACObserve(self, loggedIn)
  7. ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
  8. return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
  9. }];

这段代码看起来有点复杂,来细细说一下,首先是左边的RAC(...),它的作用是将self.logInButton.enabled属性与右边的signal的sendNext值绑定。也就是如果右边的reduce的返回值为NO,那么enabled就为NO。右边的combineLatest是获取这4个signal的next值。其中可以看到self.usernameTextField.rac_textSignal这么个东东,rac_textSignal是RAC为UITextField添加的category,只要usernameTextField的值有变化,这个值就会被返回(sendNext)。combineLatest需要每个signal至少都有过一次sendNext。reduce的作用是根据接收到的值,再返回一个新的值,这里是@(YES)和@(NO),必须是object。

上面这段代码用到了Signal的组合,想象一下,如果是传统的方式,写起来还是挺复杂的,而且随着功能的增加,调整起来会更加麻烦。

冷信号(Cold)和热信号(Hot)

上面提到过这两个概念,冷信号默认什么也不干,比如下面这段代码

  1. RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
  2. NSLog(@"triggered");
  3. [subscriber sendNext:@"foobar"];
  4. [subscriber sendCompleted];
  5. return nil;
  6. }];

我们创建了一个Signal,但因为没有被subscribe,所以什么也不会发生。加了下面这段代码后,signal就处于Hot的状态了,block里的代码就会被执行。

  1. [signal subscribeCompleted:^{
  2. NSLog(@"subscription %u", subscriptions);
  3. }];

或许你会问,那如果这时又有一个新的subscriber了,signal的block还会被执行吗?这就牵扯到了另一个概念:Side Effect

Side Effect

还是上面那段代码,如果有多个subscriber,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是你想要的,或许不是。如果要避免这种情况的发生,可以使用 replay 方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。

Cocoa Categories

为了更加方便地使用RAC,RAC给Cocoa添加了很多category,与系统集成地越紧密,使用起来自然也就越方便。下面是我认为比较常用的categories。

UIView Categories

上面看到的rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其他常用的UIView也都有添加相应的category,比如UIAlertView,就不需要再用Delegate了。

  1. UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"" message:@"Alert" delegate:nil cancelButtonTitle:@"YES" otherButtonTitles:@"NO", nil];
  2. [[alertView rac_buttonClickedSignal] subscribeNext:^(NSNumber *indexNumber) {
  3. if ([indexNumber intValue] == 1) {
  4. NSLog(@"you touched NO");
  5. } else {
  6. NSLog(@"you touched YES");
  7. }
  8. }];
  9. [alertView show];

有了这些Category,大部分的Delegate都可以使用RAC来做。或许你会想,可不可以subscribe NSMutableArray.rac_sequence.signal,这样每次有新的object或旧的object被移除时都能知道,UITableViewController就可以根据dataSource的变化,来reloadData。但很可惜这样不行,因为RAC是基于KVO的,而NSMutableArray并不会在调用addObject或removeObject时发送通知,所以不可行。不过可以使用NSArray作为UITableView的dataSource,只要dataSource有变动就换成新的Array,这样就可以了。

说到UITableView,再说一下UITableViewCell,RAC给UITableViewCell提供了一个方法:rac_prepareForReuseSignal,它的作用是当Cell即将要被重用时,告诉Cell。想象Cell上有多个button,Cell在初始化时给每个button都addTarget:action:forControlEvents,被重用时需要先移除这些target,下面这段代码就可以很方便地解决这个问题:

  1. [[[self.cancelButton
  2. rac_signalForControlEvents:UIControlEventTouchUpInside]
  3. takeUntil:self.rac_prepareForReuseSignal]
  4. subscribeNext:^(UIButton *x) {
  5. // do other things
  6. }];

还有一个很常用的category就是UIButton+RACCommandSupport.h,它提供了一个property:rac_command,就是当button被按下时会执行的一个命令,命令被执行完后可以返回一个signal,有了signal就有了灵活性。比如点击投票按钮,先判断一下有没有登录,如果有就发HTTP请求,没有就弹出登陆框,可以这么实现。

  1. voteButton.rac_command = [[RACCommand alloc] initWithEnabled:self.viewModel.voteCommand.enabled signalBlock:^RACSignal *(id input) {
  2. // Assume that we‘re logged in at first. We‘ll replace this signal later if not.
  3. RACSignal *authSignal = [RACSignal empty];
  4. if ([[PXRequest apiHelper] authMode] == PXAPIHelperModeNoAuth) {
  5. // Not logged in. Replace signal.
  6. authSignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
  7. @strongify(self);
  8. FRPLoginViewController *viewController = [[FRPLoginViewController alloc] initWithNibName:@"FRPLoginViewController" bundle:nil];
  9. UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
  10. [self presentViewController:navigationController animated:YES completion:^{
  11. [subscriber sendCompleted];
  12. }];
  13. return nil;
  14. }]];
  15. }
  16. return [authSignal then:^RACSignal *{
  17. @strongify(self);
  18. return [[self.viewModel.voteCommand execute:nil] ignoreValues];
  19. }];
  20. }];
  21. [voteButton.rac_command.errors subscribeNext:^(id x) {
  22. [x subscribeNext:^(NSError *error) {
  23. [SVProgressHUD showErrorWithStatus:[error localizedDescription]];
  24. }];
  25. }];

这段代码节选自AshFurrow的FunctionalReactivePixels,有删减。

Data Structure Categories

常用的数据结构,如NSArray, NSDictionary也都有添加相应的category,比如NSArray添加了rac_sequence,可以将NSArray转换为RACSequence,顺便说一下RACSequence, RACSequence是一组immutable且有序的values,不过这些values是运行时计算的,所以对性能提升有一定的帮助。RACSequence提供了一些方法,如array转换为NSArray,any:检查是否有Value符合要求,all:检查是不是所有的value都符合要求,这里的符合要求的,block返回YES,不符合要求的就返回NO。

NotificationCenter Category

NSNotificationCenter, 默认情况下NSNotificationCenter使用Target-Action方式来处理Notification,这样就需要另外定义一个方法,这就涉及到编程领域的两大难题之一:起名字。有了RAC,就有Signal,有了Signal就可以subscribe,于是NotificationCenter就可以这么来处理,还不用担心移除observer的问题。

  1. [[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"MyNotification" object:nil] subscribeNext:^(NSNotification *notification) {
  2. NSLog(@"Notification Received");
  3. }];

NSObject Categories

NSObject有不少的Category,我觉得比较有用的有这么几个

NSObject+RACDeallocating.h

顾名思义就是在一个object的dealloc被触发时,执行的一段代码。

  1. NSArray *array = @[@"foo"];
  2. [[array rac_willDeallocSignal] subscribeCompleted:^{
  3. NSLog(@"oops, i will be gone");
  4. }];
  5. array = nil;

NSObject+RACLifting.h

有时我们希望满足一定条件时,自动触发某个方法,有了这个category就可以这么办

  1. - (void)test
  2. {
  3. RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
  4. double delayInSeconds = 2.0;
  5. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
  6. dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
  7. [subscriber sendNext:@"A"];
  8. });
  9. return nil;
  10. }];
  11. RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
  12. [subscriber sendNext:@"B"];
  13. [subscriber sendNext:@"Another B"];
  14. [subscriber sendCompleted];
  15. return nil;
  16. }];
  17. [self rac_liftSelector:@selector(doA:withB:) withSignals:signalA, signalB, nil];
  18. }
  19. - (void)doA:(NSString *)A withB:(NSString *)B
  20. {
  21. NSLog(@"A:%@ and B:%@", A, B);
  22. }

这里的rac_liftSelector:withSignals 就是干这件事的,它的意思是当signalA和signalB都至少sendNext过一次,接下来只要其中任意一个signal有了新的内容,doA:withB这个方法就会自动被触发。

如果你有兴趣,可以想想上面这段代码会输出什么。

NSObject+RACSelectorSignal.h

这个category有rac_signalForSelector:和rac_signalForSelector:fromProtocol: 这两个方法。先来看前一个,它的意思是当某个selector被调用时,再执行一段指定的代码,相当于hook。比如点击某个按钮后,记个日志。后者表示该selector实现了某个协议,所以可以用它来实现Delegate。

MVVM

RAC带来的变化还不仅仅是这些,它还带来了架构层面的变化。我们都知道苹果推荐的是MVC架构,那MVVM又是什么呢?

跟MVC最大的区别是多了个ViewModel,它直接与View绑定,而且对View一无所知。拿做菜打比方的话,ViewModel就是调料,它不关心做的到底是什么菜。这不是跟Model很像吗?是的,它可以扮演Model的职责,但其实它是Model的中介,这样当Model的API有变化,或者由本地存储变为远程API调用时,ViewModel的public API可以保持不变。

使用ViewModel的好处是,可以让Controller更加简单和轻便,而且ViewModel相对独立,也更加方便测试和重用。那Controller这时又该做哪些事呢?在MVVM体系中,Controller可以被看成View,所以它的主要工作是处理布局、动画、接收系统事件、展示UI。

MVVM还有一个很重要的概念是 data binding,view的呈现需要data,这个data就是由ViewModel提供的,将view的data与ViewModel的data绑定后,将来双方的数据只要一方有变化,另一方就能收到。这里有Github 开源的一个ViewModel Base Class。

其他

RAC在使用时有一些注意事项,可以参考官方的DesignGuildLines,这里简单说一下。

当一个signal被一个subscriber subscribe后,这个subscriber何时会被移除?答案是当subscriber被sendComplete或sendError时,或者手动调用[disposable dispose]。

当subscriber被dispose后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。

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

Errors有优先权,如果有多个signals被同时监听,只要其中一个signal sendError,那么error就会立刻被传送给subscriber,并导致signals终止执行。相当于Exception。

生成Signal时,最好指定Name, -setNameWithFormat: 方便调试。

block代码中不要阻塞。

小结

尽管洋洋洒洒写了这么多,也只是对RAC有了个大概的了解,如果要更深入地了解RAC还是需要多读文档、代码和相关项目。

RAC学习起来稍显吃力,且相关的文章目前还不多,中文的就更少了,希望这篇文章能带给你些帮助。

以下是我觉得还不错的RAC相关资源

FunctionalReactivePixels 作者同时还出了一本FRP相关的书,个人觉得看源码就足够了。

GroceryList RAC的作者之一 jspahrsummers 的一个项目

ReactiveCocoa Essentilas: Understanding and Using RACCommand 介绍了RACCommand的使用,同时也涉及了RAC相关的一些点。

Transparent OAuth Token Refresh Using ReactiveCocoa 这篇文章讲了如何使用RAC来透明地获取Access Token,然后继续发送请求。

BNR: An Introduction to ReactiveCocoa(视频)

时间: 2024-10-14 03:48:22

ReactiveCocoa 信号1的相关文章

ReactiveCocoa信号使用方法

最近研究RAC时都是基于UI控件来使用,对单独的signal没有使用过,最近在网上看到一篇文章是关于RAC单独signal的使用.在学习整理后将个人觉得能帮助用于UI控件的一些signal使用方法记录如下(也许能从中思考出用于UI控件信号组合的方法): 1.基本的signal创建使用 1 //创建一个signal,并直接发送next事件对象 2 RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscrib

ReactiveCocoa的冷信号与热信号 探讨

背景 ReactiveCocoa(简称RAC)是最初由GitHub团队开发的一套基于Cocoa的FRP框架.FRP即Functional Reactive Programming(函数式响应式编程),其优点是用随时间改变的函数表示用户输入,这样就不需要可变状态了.我们之前的文章“RACSignal的Subscription深入分析”里曾经详细讲解过RAC核心概念之一RACSignal的实现原理.在美团客户端中,我们大量使用了这个框架.冷信号与热信号的概念很容易混淆并造成一定的问题.鉴于这个问题具

ReactiveCocoa框架菜鸟入门——信号(Signal)与订阅者(Subscriber)

上一篇文章已经简单的介绍了ReactiveCocoa框架的思想和优势.本文初步研究一下ReactiveCocoa框架的使用方法. 写在开始前 传统的编程思想,大概是用户产生某个事件,然后得到相应的参数,传入事先已经实现的方法中,处理完成后把结果在UI界面上反馈出来.ReactiveCocoa框架中大量的使用了block,这意味着,很多block内的代码,是在将来某一个合适的时刻被执行的.如果你看到block里某个参数并没有被赋值,也没有传入参数,不要奇怪,程序运行到这里的时候还不会执行这个blo

ReactiveCocoa框架菜鸟入门——信号(Signal)详解

基础知识 在阅读本文之前,请确保你已成功导入ReactiveCocoa框架并对信号(Signal)和订阅者(Subscriber)有基本了解.或者尝试着完全理解以下一段内容: 信号是数据流,可以被绑定和传递.可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况).水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开.这样只要有新的玻璃球进来,就会自动传送

ReactiveCocoa之理解sample信号

sample信号从名字上就可以看出是取样例的意思. 因此,我们可以这样理解 假设有一个工厂,这个工厂每生产了一种新商品,都会发送一个有新样品的信号signalA. 在一个商店,有一个理货员,每当他要整理货柜的时候,都会产生一个整理货柜的信号signalB. 然而这个理货员在展示样品的时候: 可能由于货柜容量有限的原因,只会展示最新的样片, 而且这个理货员很懒,如果在他上次整理货柜到这次整理货柜时,没有接到新样品,那么这个理货员就不再整理货柜了,直到他下次想起整理货柜时有新样品,新的样片才会被上架

RAC(ReactiveCocoa)

什么是 ReactiveCocoa ReactiveCocoa(其简称为 RAC)是由 Github 开源的一个应用于 iOS 和 OS X 开发的新框架.RAC 具有函数式编程和响应式编程的特性.它主要吸取了 .Net 的 Reactive Extensions 的设计和实现. ReactiveCocoa 试图解决什么问题 经过一段时间的研究,我认为 ReactiveCocoa 试图解决以下 3 个问题: 传统 iOS 开发过程中,状态以及状态之间依赖过多的问题 传统 MVC 架构的问题:Co

ReactiveCocoa 谈谈concat

今天的一个业务流程,业务流程大概就是这样的 1.从CoreData中获取之前的数据 2.更新界面 3.从网络获取数据 4.判断获取结果 5.处理错误判断 6.更新界面 7.判断结果numberOfNews字段 8.现实numberOfNews信息 这种顺序行的处理,正正是ReactiveCocoa的擅长解决的问题,那么问题来了,怎么才能通过Signal,将if else 转换数据,要知道,很多地方都在block里面 这就需要用到flattenMap 和 then 这两个东西 来看看React的玩

Reactivecocoa初级使用

一直听闻ReactiveCocoa(以下简称RAC)的大名,但始终没有使用.最近时间比较空闲就决定研究一下. 在配置RAC时候遇到了一个小麻烦需要说明本人用cocoapods管理第三方框架,于是按照正常流程在终端中 输入pod search ReactiveCocoa,找到搜索结果 于是vim 打开Podfile 录入 pod 'ReactiveCocoa', '~> 3.0.0-alpha.1' 结果cocoapods始终安装不了 原因竟是3.0.0支持的是swift 于是重新录入pod 'R

ReactiveCocoa内存管理

1.我们创建的管道是如何被保存的么? ReactiveCocoa设计的目的之一是允许这样一种编程样式,即管道可以匿名创建.到目前为止,我们的管道都是这么处理的.为了支持这种模式,ReactiveCocoa维护了一个全局的信号集合.如果信号有一个或多个订阅者,它就是可用的.如果所有订阅者都被移除了,信号就被释放了. 2.如何取消对信号的订阅? 在一个completed事件或error事件后,一个订阅者会自动将自己移除.手动移除可能通过RACDisposable来完成.RACSignal的所有订阅方