ReactiveCocoa 設(shè)計規(guī)范

Design Guidelines【譯】

RACSequence 的一些約定

  1. 默認(rèn)情況延遲發(fā)生
  2. 默認(rèn)會阻塞調(diào)用者
  3. 副作用只發(fā)生一次

RACSignal 的約定

  1. 信號事件有序串行執(zhí)行刻两,保證不會同時到達兩個或多個信號绰沥,但是可以運行在不同的scheduler之上矫限。
  2. 訂閱事件總會在一個 scheduler 上
  3. 錯誤會立即傳播(優(yōu)先級最高)
  4. 訂閱事件會產(chǎn)生副作用
  5. 訂閱關(guān)系總會在complete或error時自動銷毀
  6. 銷毀會取消正在執(zhí)行的任務(wù)并且清理相關(guān)的資源

常用練習(xí)

  • 為返回信號的方法或?qū)傩允褂妹枋鲂月暶?br> 聲明語義有三個維度

    1. 冷信號(是否在此時被激活)還是熱信號(需要訂閱后激活)
    2. 信號返回值的個數(shù)是0個一個或更多法绵?
    3. 信號是否有副作用
  • 舉個栗子

    • 沒有副作用的熱信號:通常聲明為屬性來代替方法。暗示訂閱之前不需要任何初始化涧狮,并且之后附加的任何訂閱不會影響當(dāng)前的訂閱語義(沒有副作用且每次訂閱都是獨立的語義)炕矮。信號屬性通常命名在事件之后(eg: textChangedSignal
    • 沒有副作用的冷信號:通常使用一個方法返回信號,并且通常使用名詞命名(eg:-currentText)者冤。方法聲明暗示信號不會被保存(持有)肤视,暗示只有在訂閱時才會執(zhí)行任務(wù)。如果信號發(fā)送多個值涉枫,該名詞必須是復(fù)數(shù)的邢滑。(-currentModels
    • 有副作用的冷信號:通常使用方法返回,并且使用動詞命名(eg:-logIn).動詞命名暗示該方法的執(zhí)行不是冪等得并且使用該方法的調(diào)用者需要小心清楚副作用的影響愿汰,除非副作用是被需要的或允許范圍內(nèi)的困后。如果該信號包含一個或多個值,需要用一個名詞命名衬廷,并且使用對應(yīng)的單復(fù)數(shù)形式摇予。

縮進流操作一致

如果不適當(dāng)?shù)母袷交兞魇降拇a將會變得很密集并且混亂吗跋。使用一致的縮進來高亮鏈?zhǔn)搅鞯拈_始和結(jié)束侧戴。

當(dāng)在單個流上調(diào)用方法時,不需要額外的縮進跌宛。

RACStream *result = [stream startWith:@0];

RACStream *result2 = [stream map:^(NSNumber *value) {
    return @(value.integerValue + 1);
}];

當(dāng)多次轉(zhuǎn)換相同的流時救鲤,請確保所有步驟都對齊。 復(fù)雜運算符+ zip:reduce:+ combineLatest:reduce:可以拆分為多行以提高可讀性:

RACStream *result = [[[RACStream
    zip:@[ firstStream, secondStream ]
    reduce:^(NSNumber *first, NSNumber *second) {
        return @(first.integerValue + second.integerValue);
    }]
    filter:^ BOOL (NSNumber *value) {
        return value.integerValue >= 0;
    }]
    map:^(NSNumber *value) {
        return @(value.integerValue + 1);
    }];

當(dāng)然秩冈,使用block參數(shù)嵌套的流需要從block的自然縮進開始。

[[signal
    then:^{
        @strongify(self);

        return [[self
            doSomethingElse]
            catch:^(NSError *error) {
                @strongify(self);
                [self presentError:error];

                return [RACSignal empty];
            }];
    }]
    subscribeCompleted:^{
        NSLog(@"All done.");
    }];

單個流中的所有值類型必須保持一致斥扛。

使用不同類型需要使用復(fù)雜的操作流程入问,且會給使用流的消費者增加額外的負(fù)擔(dān)。

避免過久的持有流

如果持有流的時間超過他本身需要存活的時間時稀颁,將會導(dǎo)致流本身持有的或依賴的資源無法釋放芬失,潛在的造成內(nèi)存使用過高。

只處理需要的信號

除了會增加內(nèi)存使用匾灶,不必要的持有流或者 RACSignal 訂閱關(guān)系會導(dǎo)致 CPU 使用增加, 因為不必要的任務(wù)執(zhí)行所得出的結(jié)果永遠(yuǎn)不會被使用到棱烂。一般可以使用-take-takeUntil可以自動在不必要的時候取消流執(zhí)行

在已知的 Scheduler 上傳送信號事件

流信號返回的值可能會來自很多復(fù)雜的場景,很可能是在底層線程中造作得出阶女,所以在給UI元素這是返回的信號值時颊糜,一定要保證其賦值造作發(fā)成在mainScheduler (主線程)只上哩治。

盡可能少的切換調(diào)度者(scheduler

非必須的情況下,務(wù)必讓流信號的執(zhí)行發(fā)生在同一 Scheduler 之上衬鱼。因為切換 Scheduler 會引入不必要的延遲并且會增加CPU資源的消耗业筏。

通常 -deliverOn:嚴(yán)格限制只會在信號鏈的尾部使用,比如在訂閱之前鸟赫,或為屬性綁定值時

明確信號的副作用

通常一定要避免副作用的發(fā)生蒜胖,但是有一些副作用是我們所期望的,一般應(yīng)用在一些hook方法進行調(diào)試,例如 -doNext:抛蚤、-doError:台谢、-doCompleted:

NSMutableArray *nexts = [NSMutableArray array];
__block NSError *receivedError = nil;
__block BOOL success = NO;
RACSignal *bookkeepingSignal = [[[valueSignal
    doNext:^(id x) {
        [nexts addObject:x];
    }]
    doError:^(NSError *error) {
        receivedError = error;
    }]
    doCompleted:^{
        success = YES;
    }];

RAC(self, value) = bookkeepingSignal;

使用組播分享帶有副作用的信號

使用組播可以允許單個信號擁有任意數(shù)量的訂閱關(guān)系。

// This signal starts a new request on each subscription.
RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    AFHTTPRequestOperation *operation = [client
        HTTPRequestOperationWithRequest:request
        success:^(AFHTTPRequestOperation *operation, id response) {
            [subscriber sendNext:response];
            [subscriber sendCompleted];
        }
        failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            [subscriber sendError:error];
        }];

    [client enqueueHTTPRequestOperation:operation];
    return [RACDisposable disposableWithBlock:^{
        [operation cancel];
    }];
}];

// Starts a single request, no matter how many subscriptions `connection.signal`
// gets. This is equivalent to the -replay operator, or similar to
// +startEagerlyWithScheduler:block:.
RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]];
[connection connect];

[connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber one: %@", response);
}];

[connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber two: %@", response);
}];

通過給信號命名進行調(diào)試

RACSignal *signal = [[[RACObserve(self, username)
    distinctUntilChanged]
    take:3]
    filter:^(NSString *newUsername) {
        return [newUsername isEqualToString:@"joshaber"];
    }];

NSLog(@"%@", signal);
// 打印結(jié)果 [[[RACObserve(self, username)] -distinctUntilChanged] -take: 3] -filter:

使用 -setNameWithFormat: 對信號自定義信號名岁经。
此外朋沮,RACSignal 還提供了 -logNext, -logError,-logCompleted, -logAll方法,當(dāng)信號事件執(zhí)行時自動打印信號的信息(包括 name)蒿偎,更方便檢測到信號的實時狀況朽们。

避免顯式的訂閱和銷毀

雖然-subscribeNext:error:completed:和它的變體是處理信號的最基本的方式,但是它們較少的聲明性诉位,鼓勵使用副作用和潛在地復(fù)制內(nèi)置功能而使代碼復(fù)雜化骑脱。

同時,顯式的調(diào)用 RACDisposable 類會快速導(dǎo)致一堆亂七八糟的資源管理并且清理代碼苍糠。

  • 解決方案 :使用較高級別的模式來替換手動訂閱和銷毀
  • 使用 RAC() 或者 RACChannelTo 宏定義可以用來綁定信號或者屬性叁丧,代替在狀態(tài)變化時手動更新。
  • 例如 -takeUntil: 等操作符可以用來當(dāng)參數(shù)中的事件發(fā)生時自動銷毀一段訂閱關(guān)系(比如 取消按鈕 被點擊時)岳瞭。

通常 拥娄,相比于使用訂閱的回調(diào),使用內(nèi)置的 streamsignal 操作符將會讓代碼變得更簡潔并且能產(chǎn)生更少的易錯代碼瞳筏。

盡可能避免使用 subjects

Subject 是一個用來橋接代碼到 signal 世界的強大工具稚瘾,但是就像 RAC 中的可變變量一樣,當(dāng)發(fā)生濫用時姚炕,他們會更快的導(dǎo)致程序變得復(fù)雜摊欠。

Subjects 通常會被 ReactiveCocoa 的其他模式所代替:

  • 相比于給 Subject 提供初始值,不如考慮使用 +createSignal: block 產(chǎn)生值代替柱宦。
  • 相比于將一些中間值發(fā)送給發(fā)送給 subject些椒,不如使用 +combineLatest: 或者 +zip:操作符組合多個信號的輸出值。
  • 基于一個 Signal 使用 multicast (組播)掸刊,而不是使用多個 訂閱者共享 subjects 的結(jié)果免糕。
  • 使用 command 或者 -rac_signalForSelector: ,而不是為了簡單的控制一個 subject 實現(xiàn)一個事件方法。

當(dāng)不得不使用 subject 時石窑,他們必須是信號鏈中最基本的輸入牌芋,而不是其中的一個。

支持自定義操作符

推薦使用 RACStream 的方法

盡可能使用現(xiàn)有的操作符進行構(gòu)思

盡量避免引入并發(fā)

盡量使用 RACScheduler 代替

在 disposable 中取消任務(wù)和清理所有關(guān)聯(lián)資源尼斧。

當(dāng)使用 +createSignal: 創(chuàng)建信號是姜贡,通常會返回一個 RACDisposable 對象。而這個 disposable 對象應(yīng)該:

  • 盡可能方便優(yōu)雅的取消正在進行的由 signal 開啟的任務(wù)棺棵。
  • 立即銷毀任何信號的訂閱關(guān)系楼咳,由此來觸發(fā)他們的取消和清除代碼。
  • 釋放任何信號開辟的內(nèi)存和占用的資源烛恤。

不要在操作符中分塊

流操作符應(yīng)該立即返回一個新的流母怜。操作符需要執(zhí)行的任何操作都應(yīng)該是執(zhí)行新流的一部分,而不是運算符調(diào)用本身的一部分缚柏。

// WRONG!
- (RACSequence *)map:(id (^)(id))block {
    RACSequence *result = [RACSequence empty];
    for (id obj in self) {
        id mappedObj = block(obj);
        result = [result concat:[RACSequence return:mappedObj]];
    }

    return result;
}

// Right!
- (RACSequence *)map:(id (^)(id))block {
    return [self flattenMap:^(id obj) {
        id mappedObj = block(obj);
        return [RACSequence return:mappedObj];
    }];
}

避免深度遞歸導(dǎo)致的堆棧溢出

任何可能無限遞歸的操作符都應(yīng)該使用 -scheduleRecursiveBlock: RACScheduler 方法苹熏。這個方法將會把遞歸轉(zhuǎn)換為迭代來防止堆棧溢出。
例如:下面的例子將會錯誤地實現(xiàn) -repeat ,應(yīng)為這將潛在的引起堆棧溢出并且崩潰币喧。

- (RACSignal *)repeat {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        __block void (^resubscribe)(void) = ^{
            RACDisposable *disposable = [self subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                resubscribe();
            }];

            [compoundDisposable addDisposable:disposable];
        };

        return compoundDisposable;
    }];
}

相比之下轨域,下面的版本將會避免。

- (RACSignal *)repeat {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        RACScheduler *scheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler];
        RACDisposable *disposable = [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) {
            RACDisposable *disposable = [self subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                reschedule();
            }];

            [compoundDisposable addDisposable:disposable];
        }];

        [compoundDisposable addDisposable:disposable];
        return compoundDisposable;
    }];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杀餐,一起剝皮案震驚了整個濱河市干发,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌史翘,老刑警劉巖枉长,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異琼讽,居然都是意外死亡必峰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進店門钻蹬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吼蚁,“玉大人,你說我怎么就攤上這事问欠「未遥” “怎么了?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵溅潜,是天一觀的道長。 經(jīng)常有香客問我薪伏,道長滚澜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任嫁怀,我火速辦了婚禮设捐,結(jié)果婚禮上借浊,老公的妹妹穿的比我還像新娘。我一直安慰自己萝招,他們只是感情好蚂斤,可當(dāng)我...
    茶點故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著槐沼,像睡著了一般曙蒸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岗钩,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天纽窟,我揣著相機與錄音,去河邊找鬼兼吓。 笑死臂港,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的视搏。 我是一名探鬼主播审孽,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浑娜!你這毒婦竟也來了佑力?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤棚愤,失蹤者是張志新(化名)和其女友劉穎搓萧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宛畦,經(jīng)...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡瘸洛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了次和。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片反肋。...
    茶點故事閱讀 38,687評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖踏施,靈堂內(nèi)的尸體忽然破棺而出石蔗,到底是詐尸還是另有隱情,我是刑警寧澤畅形,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布养距,位于F島的核電站,受9級特大地震影響日熬,放射性物質(zhì)發(fā)生泄漏棍厌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望耘纱。 院中可真熱鬧敬肚,春花似錦、人聲如沸束析。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽员寇。三九已至弄慰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丁恭,已是汗流浹背曹动。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留牲览,地道東北人墓陈。 一個月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像第献,于是被迫代替她去往敵國和親贡必。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,576評論 2 349

推薦閱讀更多精彩內(nèi)容