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.      
  5.     if ([[PXRequest apiHelper] authMode] == PXAPIHelperModeNoAuth) { 
  6.         // Not logged in. Replace signal. 
  7.         authSignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  8.             @strongify(self); 
  9.              
  10.             FRPLoginViewController *viewController = [[FRPLoginViewController alloc] initWithNibName:@"FRPLoginViewController" bundle:nil]; 
  11.             UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; 
  12.              
  13.             [self presentViewController:navigationController animated:YES completion:^{ 
  14.                 [subscriber sendCompleted]; 
  15.             }]; 
  16.              
  17.             return nil; 
  18.         }]]; 
  19.     } 
  20.      
  21.     return [authSignal then:^RACSignal *{ 
  22.         @strongify(self); 
  23.         return [[self.viewModel.voteCommand execute:nil] ignoreValues]; 
  24.     }]; 
  25. }]; 
  26. [voteButton.rac_command.errors subscribeNext:^(id x) { 
  27.     [x subscribeNext:^(NSError *error) { 
  28.         [SVProgressHUD showErrorWithStatus:[error localizedDescription]]; 
  29.     }]; 
  30. }]; 
 
这段代码节选自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.     RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  3.         double delayInSeconds = 2.0; 
  4.         dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
  5.         dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 
  6.             [subscriber sendNext:@"A"]; 
  7.         }); 
  8.         return nil; 
  9.     }]; 
  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.      
  18.     [self rac_liftSelector:@selector(doA:withB:) withSignals:signalA, signalB, nil]; 
  19.  
  20. - (void)doA:(NSString *)A withB:(NSString *)B 
  21.     NSLog(@"A:%@ and B:%@", A, B); 
这里的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,而后继续发送请求。
相关文章
相关标签/搜索