前言
原本這一篇應該在說完RACSignal之后再出的, 但是因為最近事情特別多, RACSignal就一直被耽擱下來了, 而這一篇又是對實際應用RAC框架的一直指導方針, 重要性還是不言而喻的, 因此, 就先留個印象, 看看官方推薦使用RAC框架的正確姿勢.
另外, 想要在團隊中推行RAC框架是很難的一件事情, 為了權(quán)衡, 但是有時候又想去使用一些函數(shù)式的代碼, 因此, 推薦一下RXCollection這個庫, github上這個庫有一個比較復雜的版本, 但是已經(jīng)說不用了, 我個人使用的是精簡版的, 也就是只有map, filter, fold這些高階函數(shù)(概念)的版本, 主要是事先轉(zhuǎn)換一下自己的思維, 把用for循環(huán)寫的代碼, 改用map, fold等等.
正文
本文檔包含了對于想要使用RAC框架的工程的一些設(shè)計指導, 文檔內(nèi)容主要受Rx設(shè)計指導的啟發(fā).
本文檔假定你已基本熟悉RAC的特性. 如果沒有, 那么框架總覽則是一個更好的選擇來快速起步學習RAC的特性.
RACSignal初探
RACSignal是一個推驅(qū)動的流, 通過訂閱來集中處理異步的事件傳遞. 在框架總覽中可以獲取到更多相關(guān)信息.
序列化的信號事件
一個信號可能選擇任意的線程來傳遞事件. 連續(xù)的事件甚至允許抵達不同的線程或者調(diào)度器, 除非顯式地指定傳遞到特定的調(diào)度器.
然而, RAC保證不會有2個信號事件同時到達. 當一個事件正在被處理的時候, 沒有其它的時間會被傳遞. 其余時間的發(fā)送方將會等待, 知道當前的時間已經(jīng)被處理完畢.
這很顯然地意味著, 傳遞給-subscribeNext:error:completed:的block不需要去互相同步, 因為它們絕對不同同步執(zhí)行.
錯誤會立即傳遞
在RAC中, 錯誤事件有異常的語義(譯注: 也就是意味著異常). 當一個錯誤事件被發(fā)送到信號中, 他將會理解被轉(zhuǎn)發(fā)到所有的受依賴信號, 導致整個鏈路終止.
主要目的為改變錯誤事件處理行為的操作符如-catch, -catchTo和-materialize, 不受此規(guī)則約束.
每次訂閱都發(fā)生副作用
對RACSignal的每一次新的訂閱都會觸發(fā)副作用. 這意味著, 任何副作用都會觸發(fā), 就像有多次訂閱到信號本身一樣. (譯注: 應該是為了說明訂閱幾次就會觸發(fā)幾次副作用 )
考慮下面的例子:
__block int aNumber = 0;
// 含有將`aNumber`自增副作用block的 RACSignal
RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
aNumber++;
[subscriber sendNext:@(aNumber)];
[subscriber sendCompleted];
return nil;
}];
// 這里將會打印 "subscriber one: 1"
[aSignal subscribeNext:^(id x) {
NSLog(@"subscriber one: %@", x);
}];
// 這里將會打印 "subscriber two: 2"
[aSignal subscribeNext:^(id x) {
NSLog(@"subscriber two: %@", x);
}];
副作用會在每次訂閱都重復. 這種行為應用于大多數(shù)操作符:
__block int missilesToLaunch = 0;
// 副作用為每次訂閱都會改變`missToLaunch`的信號
RACSignal *processedSignal = [[RACSignal
return:@"missiles"]
map:^(id x) {
missilesToLaunch++;
return [NSString stringWithFormat:@"will launch %d %@", missilesToLaunch, x];
}];
// 這里會打印 "First will launch 1 missiles"
[processedSignal subscribeNext:^(id x) {
NSLog(@"First %@", x);
}];
// 這里會打印 "Second will launch 2 missiles"
[processedSignal subscribeNext:^(id x) {
NSLog(@"Second %@", x);
}];
如果要抑制這種行為, 想要信號的副作用只執(zhí)行一次, 發(fā)送時間給一個subject.
副作用可能潛伏很深, 不容易被診斷出來. 為此, 建議盡可能使副作用顯性化
完成或者錯誤事件自動清除訂閱
當訂閱者發(fā)送了一個completed或'error'事件, 相關(guān)聯(lián)的訂閱將會被立即清除, 此行為通常是為了杜絕手動清除訂閱的需要.
查看文檔內(nèi)存管理來獲取更多信號聲明周期的信息.
清除會取消處理中的工作并清理資源
當一個訂閱被手動或者自動清除, 任何關(guān)聯(lián)這個訂閱的處理中或者待處理工作都會被盡快優(yōu)雅地取消掉, 任何管理這個訂閱的資源都會被清理掉.
對信號清除訂閱表現(xiàn)在文件上傳上的例子為, 飛行模式下取消網(wǎng)絡(luò)請求, 并從內(nèi)存中釋放文件數(shù)據(jù).
最佳實踐
接下來的建議意圖使基于RAC的代碼更加可預測, 可理解以及高效. 它們只是一些指導意見, 當要決定是否使用這里的建議到某些代碼場景時, 還是要自己做最佳判斷.
為返回信號的方法或者屬性使用描述性聲明
當一個方法或?qū)傩苑祷豏ACSignal時, 很難一眼就理解這個信號的具體語義.
下面有3個關(guān)鍵的問題可以細化聲明:
- 是熱信號(在返回給調(diào)用方時就已經(jīng)激活)還是冷信號(訂閱時激活)?
- 信號包含0個, 1個還是多個值?
- 信號是否有副作用?
無副作用的熱信號應該是一個屬性而不是方法. 屬性的使用暗示了在訂閱信號的事件前是不需要初始化的, 后續(xù)的訂閱者也不會改變這一點. 信號屬性應該被命名在事件之后(例如textChanged)
無副作用的冷信號應該由方法返回, 命名應該是類名詞式的(例如: -currentText). 方法的聲明暗示了信號可能被持有, 工作在訂閱時執(zhí)行. 如果信號發(fā)送多個值, 方法名應該變?yōu)閺蛿?shù)(如: -currentModels).
有副作用的信號應該由動詞式命名的方法返回(例如:-logIn). 動詞暗示著此方法并不是冪等的(譯注: 所謂冪等就是執(zhí)行多次和執(zhí)行一次是一樣的結(jié)果)并且調(diào)用方需要小心調(diào)用, 只有當副作用是期望的才調(diào)用. 如果信號將會發(fā)送一個或多個值, 方法要含有一個名詞(例如: -loadConfiguration, -fetchLatestEvents).
信號操作的不斷縮進
這一段不翻譯了, 主要是因為鏈式調(diào)用的原因, RAC代碼很容易寫的很密集, 因此為了方便理解, 官方給出的一鏈式調(diào)用的縮進例子
RACSignal *result = [[[RACSignal
zip:@[ firstSignal, secondSignal ]
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);
}];
以及:
[[signal
then:^{
@strongify(self);
return [[self
doSomethingElse]
catch:^(NSError *error) {
@strongify(self);
[self presentError:error];
return [RACSignal empty];
}];
}]
subscribeCompleted:^{
NSLog(@"All done.");
}];
同一信號所有值類型統(tǒng)一
RACSignal本身允許信號存在多種類型的對象, 就像Cocoa的集合類一樣. 然而, 在同一個信號中使用多種類型會使操作符的使用變得復雜, 并且增加使用者的負擔, 他們必須要小心地調(diào)用對象方法.
因此, 盡可能保持信號只包含同樣的類型.
只處理所需的信號
無謂地保持RACSignal訂閱只會增加內(nèi)存和CPU的消耗, 不需要的工作就不應該執(zhí)行.
如果只有特性的值需要通過信號傳遞, -take:操作符可以用來取回這些值, 并在之后立即自動清除訂閱.
類似-take:的操作符和-takeUntil:自動清理棧. 如果除了剩下的值并不需要其余的東西, 任何依賴也都將被終止, 潛移默化地節(jié)省了一大堆工作.
傳遞信號事件到已知調(diào)度器
當信號被方法返回, 或者與另一個信號結(jié)合, 會難以得知信號從哪個線程傳遞過來. 盡管保證事件是序列執(zhí)行的, 有時候也會需要有更強力的保證, 比如要執(zhí)行更新UI代碼的時候(必須在主線程執(zhí)行).
一旦這個保證很重要的時候, -deliverOn:操作符可以用來強制使信號的事件抵達到指定的調(diào)度器
盡量少切換調(diào)度器
盡管上面說了使事件在指定調(diào)度器傳遞是非常必要的, 但是切換調(diào)度器也會引入不必要的延遲以及導致CPU負載升高.
一般情況下, -deliverOn應該限制出現(xiàn)在信號鏈最后面, 理想位置應該在訂閱前, 或者在值綁定到一個屬性之前.
使信號的副作用顯性化
如果可以的話, 盡量不要讓RACSignal有副作用, 因為訂閱者可能會發(fā)現(xiàn)副作用的行為并不是想要的.
然而, 因為Cocoa主要是命令式編程, 有時候在信號事件發(fā)生時需要做點什么. 雖然大多數(shù)操作符接受任意block(這里可能包含副作用), 使用-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;
用主題(Subject)共享信號的副作用
默認每次訂閱都會發(fā)生副作用, 但是某些情況下, 副作用只需要發(fā)生一次即可, 例如一次典型的網(wǎng)絡(luò)請求不應該在新訂閱者加進來后再重復一次.
與其訂閱一個信號多次, 不如轉(zhuǎn)發(fā)信號的事件到RACSubject, 這一可以想訂閱幾次就訂閱幾次:
// 這個信號在每次訂閱都會開啟一個新請求
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];
}];
}];
// 這個主題會分發(fā)`networkRequest`信號的事件
RACSubject *results = [RACSubject subject];
// 設(shè)置一堆訂閱者給主題
[results subscribeNext:^(id response) {
NSLog(@"subscriber one: %@", response);
}];
[results subscribeNext:^(id response) {
NSLog(@"subscriber two: %@", response);
}];
// 然后, 真正地開始請求(通過訂閱一次請求的信號), 并且2個訂閱者都能接受到一樣的事件
[networkRequest subscribe:results];
給定信號名字來調(diào)試
每個信號都一個name屬性來輔助調(diào)試. 信號的-description包含了它的名字, 并且所有的RAC操作符都自動會添加名字. 一般情況下, 默認的名字就基本可以鑒別信號了.
例如:
RACSignal *signal = [[[RACObserve(self, username)
distinctUntilChanged]
take:3]
filter:^(NSString *newUsername) {
return [newUsername isEqualToString:@"joshaber"];
}];
NSLog(@"%@", signal);
將會打印出[[[RACObserve(self, username)] -distinctUntilChanged] -take: 3] -filter:.
名字也可以用-setNameWithFormat:來手動設(shè)定.
RACSignal也提供了-logNext, -logError, -logCompleted和-logAll這些方法, 可以在事件發(fā)生時自動打印信號日志. 這可以很方便地實時審查信號.
避免顯式地訂閱和解除
雖然-subscribeNext:error:completed:與其變形方法是最基本的處理信號的方式, 但是直接使用會使得代碼變得復雜, 因為缺少陳述聲明, 副作用的濫用, 以及潛在的內(nèi)置功能.
同樣地, 顯式使用RACDisposables類可能是迅速導致資源管理和清理的代碼的源頭.(譯注: rat's nest翻譯為了源頭)
這里有一些更加高階的模式來替代手動訂閱和解除:
RAC()或RACChannelTo宏可以用來綁定信號到屬性上, 而不是在改變發(fā)生時手動地更新.
-rac_liftSelector:withSignals:方法可以用來自動執(zhí)行某個方法當一個或者多個信號觸發(fā)后.
類似-takeUntil:可以來用在事件發(fā)生時自動解除訂閱(例如"取消"按鈕被按下了)
通常, 相比于在訂閱的回調(diào)中復制同樣的行為, 使用內(nèi)置的操作符可以使代碼更加簡潔和更加健壯.
避免直接操作主題
主題是橋接指令式代碼到信號的世界中和共享副作用的強大工具, 但是, 作為RAC中的"可變量", 主題很容易因為濫用而導致復雜度提升.
因為主題可以在任何地方任何時候操作, 經(jīng)常會打破線性流處理和使得邏輯很難追蹤. 同時主題還不支持有意義的解除, 這樣會導致不必要的工作.
在下列情況下, 主題可以被替換成其它的RAC模式:
考慮用+createSignal:block來生成一個值, 而不是給主題一個初始值,
嘗試用類似+combineLatest:或者+zip:來結(jié)合多個信號, 而不是立即把結(jié)果傳遞給主題
使用RACAction或者-rac_signalForSelector:而不是實現(xiàn)發(fā)送值給主題的控件行為.
然而, 主題在共享信號的副作用上還是很必要的. 在這種情況下, 使用-Subscribe:并且避免調(diào)用-sendNext:, -sendError和-sendCompleted來直接操作主題.
實現(xiàn)新的操作符
RAC提供了一大串的內(nèi)置操作符來覆蓋RACSignal的大多數(shù)應用場景, 然而, RAC并不是封閉的系統(tǒng). 針對特定使用場景實現(xiàn)額外的操作符來是完全有效的, 甚至可以針對RAC本身來實現(xiàn).
實現(xiàn)新的操作符需要小心細節(jié), 并且專注于簡潔, 避免引入bug到調(diào)用代碼中.
下面的指導覆蓋了一些常見的陷阱, 且有助于保護期望的API的規(guī)約.
(譯注: 大多數(shù)情況下我們不會去自定義新的操作符, 所以這節(jié)就簡單翻譯要點, 如果有需要查看完整內(nèi)容的, 還是查看源文檔更好)
- 盡量組合現(xiàn)有的操作符: 這些操作符已經(jīng)經(jīng)過了嚴格的測試和實際工程檢驗了.
- 避免引入并發(fā): 并發(fā)是很常見的bug之源, 代碼的并發(fā)應該交由調(diào)用方來決定.
- 在解除訂閱后取消工作和清理資源
- 不要在操作符中阻塞: 信號操作符應該理解返回一個新的信號, 操作符需要做的任何工作都應該成為訂閱一個新信號的一部分, 而不是本身的.
// WRONG!
- (RACSignal *)map:(id (^)(id))block {
RACSignal *result = [RACSignal empty];
for (id obj in self) {
id mappedObj = block(obj);
result = [result concat:[RACSignal return:mappedObj]];
}
return result;
}
// Right!
- (RACSignal *)map:(id (^)(id))block {
return [self flattenMap:^(id obj) {
id mappedObj = block(obj);
return [RACSignal return:mappedObj];
}];
}
- 避免深度遞歸造成棧溢出: 可能出現(xiàn)無限遞歸的地方都應該使用RACScheduler的
-scheduleRecursiveBlock:方法.
錯誤姿勢:
- (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;
}];
}
后記
限于水平, 文中難免出現(xiàn)錯漏, 如果你發(fā)現(xiàn)了, 麻煩評論指出.
同時, 通篇看完后, 還是要啰嗦一句, 如果真打算在實際工程中使用RAC, 這篇是必不可少的, 剩下的一篇比較關(guān)鍵的內(nèi)存管理其實結(jié)論還是比較簡單的, 找機會直接在一篇文章中插入即可.
最后, 希望對函數(shù)響應式編程感興趣的同學一起進步.