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源源不斷地傳送過來,舉個簡單的例子:
- [RACObserve(self, username) subscribeNext: ^(NSString *newName){
- NSLog(@"newName:%@", newName);
- }];
RACObserve使用了KVO來監聽property的變化,只要username被本身或外部改變,block就會被執行。但不是全部的property均可以被RACObserve,該property必須支持KVO,好比NSURLCache的currentDiskUsage就不能被RACObserve。
Signal是很靈活的,它能夠被修改(map),過濾(filter),疊加(combine),串聯(chain),這有助於應對更加複雜的狀況,好比:
- RAC(self.logInButton, enabled) = [RACSignal
- combineLatest:@[
- self.usernameTextField.rac_textSignal,
- self.passwordTextField.rac_textSignal,
- RACObserve(LoginManager.sharedManager, loggingIn),
- RACObserve(self, loggedIn)
- ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
- return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
- }];
這段代碼看起來有點複雜,來細細說一下,首先是左邊的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)
上面提到過這兩個概念,冷信號默認什麼也不幹,好比下面這段代碼
- RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
- NSLog(@"triggered");
- [subscriber sendNext:@"foobar"];
- [subscriber sendCompleted];
- return nil;
- }];
咱們建立了一個Signal,但由於沒有被subscribe,因此什麼也不會發生。加了下面這段代碼後,signal就處於Hot的狀態了,block裏的代碼就會被執行。
- [signal subscribeCompleted:^{
- NSLog(@"subscription %u", subscriptions);
- }];
或許你會問,那若是這時又有一個新的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了。
- UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"" message:@"Alert" delegate:nil cancelButtonTitle:@"YES" otherButtonTitles:@"NO", nil];
- [[alertView rac_buttonClickedSignal] subscribeNext:^(NSNumber *indexNumber) {
- if ([indexNumber intValue] == 1) {
- NSLog(@"you touched NO");
- } else {
- NSLog(@"you touched YES");
- }
- }];
- [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,下面這段代碼就能夠很方便地解決這個問題:
- [[[self.cancelButton
- rac_signalForControlEvents:UIControlEventTouchUpInside]
- takeUntil:self.rac_prepareForReuseSignal]
- subscribeNext:^(UIButton *x) {
-
- }];
還有一個很經常使用的category就是UIButton+RACCommandSupport.h,它提供了一個property:rac_command,就是當button被按下時會執行的一個命令,命令被執行完後能夠返回一個signal,有了signal就有了靈活性。好比點擊投票按鈕,先判斷一下有沒有登陸,若是有就發HTTP請求,沒有就彈出登錄框,能夠這麼實現。
- voteButton.rac_command = [[RACCommand alloc] initWithEnabled:self.viewModel.voteCommand.enabled signalBlock:^RACSignal *(id input) {
-
- RACSignal *authSignal = [RACSignal empty];
-
- if ([[PXRequest apiHelper] authMode] == PXAPIHelperModeNoAuth) {
-
- authSignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
- @strongify(self);
-
- FRPLoginViewController *viewController = [[FRPLoginViewController alloc] initWithNibName:@"FRPLoginViewController" bundle:nil];
- UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
-
- [self presentViewController:navigationController animated:YES completion:^{
- [subscriber sendCompleted];
- }];
-
- return nil;
- }]];
- }
-
- return [authSignal then:^RACSignal *{
- @strongify(self);
- return [[self.viewModel.voteCommand execute:nil] ignoreValues];
- }];
- }];
- [voteButton.rac_command.errors subscribeNext:^(id x) {
- [x subscribeNext:^(NSError *error) {
- [SVProgressHUD showErrorWithStatus:[error localizedDescription]];
- }];
- }];
這段代碼節選自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的問題。
- [[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"MyNotification" object:nil] subscribeNext:^(NSNotification *notification) {
- NSLog(@"Notification Received");
- }];
NSObject Categories
NSObject有很多的Category,我以爲比較有用的有這麼幾個
NSObject+RACDeallocating.h
顧名思義就是在一個object的dealloc被觸發時,執行的一段代碼。
- NSArray *array = @[@"foo"];
- [[array rac_willDeallocSignal] subscribeCompleted:^{
- NSLog(@"oops, i will be gone");
- }];
- array = nil;
NSObject+RACLifting.h
有時咱們但願知足必定條件時,自動觸發某個方法,有了這個category就能夠這麼辦
- - (void)test
- {
- RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
- double delayInSeconds = 2.0;
- dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
- dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
- [subscriber sendNext:@"A"];
- });
- return nil;
- }];
-
- RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
- [subscriber sendNext:@"B"];
- [subscriber sendNext:@"Another B"];
- [subscriber sendCompleted];
- return nil;
- }];
-
- [self rac_liftSelector:@selector(doA:withB:) withSignals:signalA, signalB, nil];
- }
-
- - (void)doA:(NSString *)A withB:(NSString *)B
- {
- 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。
其餘
當一個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相關資源