了解這些,學習RAC不再難

概述:

ReactiveCocoa是github開源的一個函數式響應式編程框架,是在iOS平臺上對FRP的實現。FRP的核心是信號,信號在ReactiveCocoa(以下簡稱RAC)中是通過RACSignal來表示的,信號是數據流,可以被綁定和傳遞。

ReactiveCocoa比較復雜,在正式開始介紹它的核心組件前,我們先來看看它的類圖,以便從宏觀上了解它的層次結構:


ReactiveCocoa類圖

ReactiveCocoa主要包含四個組件:

  • 信號源:RACStream 及其子類;
  • 訂閱者:RACSubscriber 的實現類及其子類;
  • 調度器:RACScheduler 及其子類;
  • 清潔工:RACDisposable 及其子類。

而信號源是最核心的部分,其它所有組件都是圍繞它運作的。
ReactiveCocoa最簡單的工作過程就是
創建信號——訂閱信號——發送信號。
所以首先我們就來介紹一下信號

一、信號源

冷信號與熱信號:

信號分為冷信號與熱信號,理解冷信號與熱信號的區別,對于RAC的理解有非常大的幫助,所以我們這篇文章也重點講解這里。

  • Hot Observable是主動的,盡管你并沒有訂閱事件,但是它會時刻推送,就像鼠標移動;而Cold Observable是被動的,只有當你訂閱的時候,它才會發布消息。
  • Hot Observable可以有多個訂閱者,是一對多,集合可以與訂閱者共享信息;而Cold Observable只能一對一,當有不同的訂閱者,消息是重新完整發送。

而在RAC中除了RACSubject及其子類是熱信號外,其它都是冷信號。subject類似“直播”,錯過了就不再處理。而signal類似“點播”,每次訂閱都會從頭開始。所以我們有理由認定subject天然就是熱信號。

Subject具備如下三個特點:

  • Subject是“可變”的。
  • Subject是非RAC到RAC的一個橋梁。
  • Subject可以附加行為,例如RACReplaySubject具備為未來訂閱者緩沖事件的能力。

我們平常使用RACSignal最簡單的步驟如下:

    //創建信號
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        //發送信號
        [subscriber sendNext:@"發送的數據"];
        [subscriber sendCompleted];
        return nil;
    }];
    
    //接收信號
    [signal subscribeNext:^(id x) {
        NSLog(@"這里是接收到的數據:%@",x);
    }];

為了了解熱信號與冷信號的區別,我們用兩段代碼來展示一下:

    //創建熱信號
    RACSubject *subject = [RACSubject subject];
    [subject sendNext:@1];    //立即發送1
    [[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
        [subject sendNext:@2];      //0.5秒后發送2
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
        [subject sendNext:@3];     //2秒后發送3
    }];
    [[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
        [subject subscribeNext:^(id x) {
            NSLog(@"subject1接收到了%@",x);    //0.1秒后subject1訂閱了
        }];
    }];
    [[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
        [subject subscribeNext:^(id x) {
            NSLog(@"subject2接收到了%@",x);        //1秒后subject2訂閱了
        }];
    }];
    //創建冷信號
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@1];
        [[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
            [subscriber sendNext:@2];
        }];
        [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
            [subscriber sendNext:@3];
        }];
        return nil;
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
        [signal subscribeNext:^(id x) {
            NSLog(@"signal1接收到了%@", x);
        }];
    }];
    [[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
        [signal subscribeNext:^(id x) {
            NSLog(@"signal2接收到了%@", x);
        }];
    }];

猜想一下上面兩段代碼的輸出會是什么

。。。
。。。
。。。
。。。
。。。

2018-02-20 11:02:24.980462+0800 RACTest[14912:16295891] subject1接收到了2
2018-02-20 11:02:26.480232+0800 RACTest[14912:16295891] subject1接收到了3
2018-02-20 11:02:26.480408+0800 RACTest[14912:16295891] subject2接收到了3
2018-02-20 11:20:53.952995+0800 RACTest[15075:16311621] signal1接收到了1
2018-02-20 11:20:54.456881+0800 RACTest[15075:16311621] signal1接收到了2
2018-02-20 11:20:54.457046+0800 RACTest[15075:16311621] signal1接收到了3
2018-02-20 11:20:54.853391+0800 RACTest[15075:16311621] signal2接收到了1
2018-02-20 11:20:55.356641+0800 RACTest[15075:16311621] signal2接收到了2
2018-02-20 11:20:55.356851+0800 RACTest[15075:16311621] signal2接收到了3

兩段代碼很簡單,我也做了注釋,就不再多做解釋,從輸出中我們可以發現:

  • 0.1秒后訂閱的subject1接收到了0.5秒后2秒后發送的信號,沒有接收到之前發送的新號。
  • 1秒后訂閱的subject2接收到了2秒后發送的信號,也沒有接收到之前發送的新號。
  • signal1和signal2都接收到了所有信號。

從中我們可以得出結論:

  1. 熱信號是主動的,即使你沒有訂閱事件,它仍然會時刻推送。如上面沒有接收到的信號都是因為在沒有訂閱者的時候,它也會推送出去。而冷信號是被動的,只有當你訂閱的時候,它才會發送消息。如第二段代碼,訂閱后才把信號推送出去。
  2. 熱信號可以有多個訂閱者,是一對多,信號可以與訂閱者共享信息。如第一段代碼,訂閱者1和訂閱者2是共享的,他們都能在同一時間接收到3這個值。而冷信號只能一對一,當有不同的訂閱者,消息會從新完整發送。如第一個例子,我們可以觀察到兩個訂閱者沒有聯系,都是基于各自的訂閱時間開始接收消息的。
將冷信號轉變為熱信號

RAC庫中對于冷信號轉化成熱信號有如下標準的封裝:

- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;

如上面的第一段代碼,我們可以用如下來達到同樣的效果:

    RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            [subscriber sendNext:@1];
        
        [[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
            [subscriber sendNext:@2];
        }];
        
        [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
            [subscriber sendNext:@3];
        }];
        return nil;
    }] publish];
    [connection connect];
    RACSignal *signal = connection.signal;
    
    [[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
        [signal subscribeNext:^(id x) {
            NSLog(@"這里是熱信號1,接收到了%@", x);
        }];
    }];
    
    [[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
        [signal subscribeNext:^(id x) {
            NSLog(@"這里是熱信號2,接收到了%@", x);
        }];
    }];

輸出如下:

2018-02-20 11:45:38.331464+0800 RACTest[15171:16331870] 這里是熱信號1,接收到了2
2018-02-20 11:45:39.830300+0800 RACTest[15171:16331870] 這里是熱信號1,接收到了3
2018-02-20 11:45:39.830870+0800 RACTest[15171:16331870] 這里是熱信號2,接收到了3

可以看到,現在已經是熱信號了,和前面的RACSubject相同。

感興趣的同學可以去看看- (RACMulticastConnection *)multicast:(RACSubject *)subject這個方法的實現,它是將冷信號轉換為熱信號的核心。其實它的本質就是使用一個Subject來訂閱原始信號,并讓其他訂閱者訂閱這個Subject,這個Subject就是熱信號。

使用信號中常見的問題:

1、多次訂閱

對RAC的信號進行轉換的時候,其實就是對原有的信號進行訂閱從而產生新的信號。如下代碼所示:

    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"來了");
        //網絡請求,產生model
        [subscriber sendNext:model];
        return nil;
    }];
    
    RACSignal *name = [signal flattenMap:^RACStream *(Person *model) {
        return [RACSignal return:model.name];
    }];
    RACSignal *age = [signal flattenMap:^RACStream *(Person *model) {
        return [RACSignal return:model.age];
    }];

    RAC(self.userNameTextFiled,text) = [[name catchTo:[RACSignal return:@"error"]] startWith:@"name:"];
    RAC(self.passwordTextField,text) = [[age catchTo:[RACSignal return:@"error"]] startWith:@"age:"];

上面分別對model進行了map,也就是產生了兩個新的信號,然后再對兩個信號進行訂閱,對這兩個信號訂閱的時候,也會對間接對原信號進行訂閱,從而造成對原信號的多次訂閱,如上所示來了就輸出了三次,如果是網絡請求的話,也會輸出三次,所以一定在信號轉換的時候一定要注意這些情況。

要解決也很簡單,把signal轉換成熱信號就行了

    RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"來了");
        [subscriber sendNext:model];
        return nil;
    }] replayLazily];     //轉換為熱信號
    
    RACSignal *name = [signal flattenMap:^RACStream *(Person *model) {
        return [RACSignal return:model.name];
    }];
    RACSignal *age = [signal flattenMap:^RACStream *(Person *model) {
        return [RACSignal return:model.age];
    }];

    RAC(self.userNameTextFiled,text) = [[name catchTo:[RACSignal return:@"error"]] startWith:@"name:"];
    RAC(self.passwordTextField,text) = [[age catchTo:[RACSignal return:@"error"]] startWith:@"age:"];
2、內存泄漏
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { //1
        Person *model = [[Person alloc] init];
        [subscriber sendNext:model];
        [subscriber sendCompleted];
        return nil;
    }];
    self.flattenMapSignal = [signal flattenMap:^RACStream *(Person *model) { //2
        return RACObserve(model, name);
    }];
    [self.flattenMapSignal subscribeNext:^(id x) { //3
        NSLog(@"recieve - %@", x);
    }];

如上代碼,看起來工作正常,但你使用內存檢測工具會發現,這里會造成內存泄漏,原因就是

#define RACObserve(TARGET, KEYPATH) \
    ({ \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
        __weak id target_ = (TARGET); \
        [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
        _Pragma("clang diagnostic pop") \
    })

這段代碼,所以這里的Block引用了self,就造成了循環引用。
解決辦法也很簡單,使用@weakify和@strongify即可:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
        Person *model = [[Person alloc] init];
        [subscriber sendNext:model];
        [subscriber sendCompleted];
        return nil;
    }];
    @weakify(self);
    self.flattenMapSignal = [signal flattenMap:^RACStream *(Person *model) {
        @strongify(self);
        return RACObserve(model, name);
    }];
    [self.flattenMapSignal subscribeNext:^(id x) {
        NSLog(@"recieve - %@", x);
    }];

本來還想介紹一下另外三個組件的,但是由于時間匆忙,馬上要去趕火車了,暫時就寫到這里。剩下三個我就簡單介紹一下吧

訂閱者:在 ReactiveCocoa 中,訂閱者是一個抽象的概念,所有實現了 RACSubscriber 協議的類都可以作為信號源的訂閱者。

@protocol RACSubscriber <NSObject>

@required

/// Sends the next value to subscribers.
///
/// value - The value to send. This can be `nil`.
- (void)sendNext:(id)value;

/// Sends the error to subscribers.
///
/// error - The error to send. This can be `nil`.
///
/// This terminates the subscription, and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendError:(NSError *)error;

/// Sends completed to subscribers.
///
/// This terminates the subscription, and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendCompleted;

/// Sends the subscriber a disposable that represents one of its subscriptions.
///
/// A subscriber may receive multiple disposables if it gets subscribed to
/// multiple signals; however, any error or completed events must terminate _all_
/// subscriptions.
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;

@end

即實現了這四個方法的類。

調度器:RACScheduler 在 ReactiveCocoa 中就是扮演著調度器的角色,本質上,它就是用 GCD 的串行隊列來實現的,并且支持取消操作。是的,在 ReactiveCocoa 中,并沒有使用到 NSOperationQueue 和 NSRunloop 等技術,RACScheduler 也只是對 GCD 的簡單封裝而已。

清潔工:RACDisposable 在 ReactiveCocoa 中就充當著清潔工的角色,它封裝了取消和清理一次訂閱所必需的工作。它有一個核心的方法 -dispose ,調用這個方法就會執行相應的清理工作,這有點類似于 NSObject 的 -dealloc 方法。

好了好了,趕火車去了~~~ 祝大家新年快樂!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容