RACSequence 的一些約定
- 默認(rèn)情況延遲發(fā)生
- 默認(rèn)會阻塞調(diào)用者
- 副作用只發(fā)生一次
RACSignal 的約定
- 信號事件有序串行執(zhí)行刻两,保證不會同時到達兩個或多個信號绰沥,但是可以運行在不同的scheduler之上矫限。
- 訂閱事件總會在一個 scheduler 上
- 錯誤會立即傳播(優(yōu)先級最高)
- 訂閱事件會產(chǎn)生副作用
- 訂閱關(guān)系總會在complete或error時自動銷毀
- 銷毀會取消正在執(zhí)行的任務(wù)并且清理相關(guān)的資源
常用練習(xí)
-
為返回信號的方法或?qū)傩允褂妹枋鲂月暶?br> 聲明語義有三個維度
- 冷信號(是否在此時被激活)還是熱信號(需要訂閱后激活)
- 信號返回值的個數(shù)是0個一個或更多法绵?
- 信號是否有副作用
-
舉個栗子
-
沒有副作用的熱信號:通常聲明為屬性來代替方法。暗示訂閱之前不需要任何初始化涧狮,并且之后附加的任何訂閱不會影響當(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)前的訂閱語義(沒有副作用且每次訂閱都是獨立的語義)炕矮。信號屬性通常命名在事件之后(eg:
縮進流操作一致
如果不適當(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)置的
stream
和signal
操作符將會讓代碼變得更簡潔并且能產(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;
}];
}