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,而後繼續發送請求。
相關文章
相關標籤/搜索