细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号
前一篇文章我們介紹了冷信號(hào)與熱信號(hào)的概念,可能有同學(xué)會(huì)問了,為什么RAC要搞得如此復(fù)雜呢,只用一種信號(hào)不就行了么?要解釋這個(gè)問題,需要繞一些圈子。
前面可能比較難懂,如果不能很好理解,請(qǐng)仔細(xì)閱讀相關(guān)文檔。
最前面提到了RAC是一套基于Cocoa的FRP框架,那就來說說FRP吧。FRP的全稱是Functional Reactive Programming,中文譯作函數(shù)式響應(yīng)式編程,是RP(Reactive Programm,響應(yīng)式編程)的FP(Functional Programming,函數(shù)式編程)實(shí)現(xiàn)。說起來很拗口。太多的細(xì)節(jié)不多討論,我們著重關(guān)注下FRP的FP特征。
FP有個(gè)很重要的概念是和我們的主題相關(guān)的,那就是純函數(shù)。
純函數(shù)就是返回值只由輸入值決定、而且沒有可見副作用的函數(shù)或者表達(dá)式。這和數(shù)學(xué)中的函數(shù)是一樣的,比如:
f(x) = 5x + 1
這個(gè)函數(shù)在調(diào)用的過程中除了返回值以外的沒有任何對(duì)外界的影響,除了入?yún)以外也不受任何其他外界因素的影響。
那么副作用都有哪些呢?我來列舉以下幾個(gè)情況:
- 函數(shù)的處理過程中,修改了外部的變量,例如全局變量。一個(gè)特殊點(diǎn)的例子,就是如果把OC的一個(gè)方法看做一個(gè)函數(shù),所有的成員變量的賦值都是對(duì)外部變量的修改。是的,從FP的角度看OOP是充滿副作用的。
- 函數(shù)的處理過程中,觸發(fā)了一些額外的動(dòng)作,例如發(fā)送了一個(gè)全局的Notification,在console里面輸出了一行信息,保存了文件,觸發(fā)了網(wǎng)絡(luò),更新了屏幕等。
- 函數(shù)的處理過程中,受到外部變量的影響,例如全局變量,方法里面用到的成員變量。注意block中捕獲的外部變量也算副作用。
- 函數(shù)的處理過程中,受到線程鎖的影響算副作用。
由此我們可以看出,在目前的iOS編程中,我們是很難擺脫副作用的。甚至可以這么說,我們iOS編程的目的其實(shí)就是產(chǎn)生各種副作用。(基于用戶觸摸的外界因素,最終反饋到網(wǎng)絡(luò)變化和屏幕變化上。)
接下來我們來分析副作用與冷熱信號(hào)的關(guān)系。既然iOS編程中少不了副作用,那么RAC在實(shí)際的使用中也不可避免地要接觸副作用。下面通過一個(gè)業(yè)務(wù)場(chǎng)景,來看看冷信號(hào)中副作用的坑:
self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://api.xxxx.com"]];self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];@weakify(self)RACSignal *fetchData = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {@strongify(self)NSURLSessionDataTask *task = [self.sessionManager GET:@"fetchData" parameters:@{@"someParameter": @"someValue"} success:^(NSURLSessionDataTask *task, id responseObject) {[subscriber sendNext:responseObject];[subscriber sendCompleted];} failure:^(NSURLSessionDataTask *task, NSError *error) {[subscriber sendError:error];}];return [RACDisposable disposableWithBlock:^{if (task.state != NSURLSessionTaskStateCompleted) {[task cancel];}}];}];RACSignal *title = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {if ([value[@"title"] isKindOfClass:[NSString class]]) {return [RACSignal return:value[@"title"]];} else {return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];}}];RACSignal *desc = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {if ([value[@"desc"] isKindOfClass:[NSString class]]) {return [RACSignal return:value[@"desc"]];} else {return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];}}];RACSignal *renderedDesc = [desc flattenMap:^RACStream *(NSString *value) {NSError *error = nil;RenderManager *renderManager = [[RenderManager alloc] init];NSAttributedString *rendered = [renderManager renderText:value error:&error];if (error) {return [RACSignal error:error];} else {return [RACSignal return:rendered];}}];RAC(self.someLablel, text) = [[title catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];RAC(self.originTextView, text) = [[desc catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];RAC(self.renderedTextView, attributedText) = [[renderedDesc catchTo:[RACSignal return:[[NSAttributedString alloc] initWithString:@"Error"]]] startWith:[[NSAttributedString alloc] initWithString:@"Loading..."]];[[RACSignal merge:@[title, desc, renderedDesc]] subscribeError:^(NSError *error) {UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:error.domain delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];[alertView show];}];不知道大家有沒有被這么一大段的代碼嚇到,我想要表達(dá)的是,在真正的工程中,我們的業(yè)務(wù)邏輯是很復(fù)雜的,而一些坑就隱藏在如此看似復(fù)雜但是又很合理的代碼之下。所以我盡量模擬了一些需求,使得代碼看起來更豐富。下面我們還是來仔細(xì)看下這段代碼的邏輯吧:
這些代碼體現(xiàn)了RAC的一些優(yōu)勢(shì),例如良好的錯(cuò)誤處理和各種鏈?zhǔn)教幚?。很不錯(cuò),對(duì)不對(duì)?但是很遺憾的告訴大家,這段代碼其實(shí)有很嚴(yán)重的錯(cuò)誤。
如果你去嘗試運(yùn)行這段代碼,并且打開Charles查看,你會(huì)驚奇的發(fā)現(xiàn),這個(gè)網(wǎng)絡(luò)請(qǐng)求發(fā)送了6次。沒錯(cuò),是6次請(qǐng)求。我們也可以想象到類似的代碼存在其他副作用的問題,重新刷新了6次屏幕,寫入6次文件,發(fā)了6個(gè)全局通知。
下面來分析,為什么是6次網(wǎng)絡(luò)請(qǐng)求呢?首先根據(jù)上面的知識(shí),可以推斷出名為fetchData信號(hào)是一個(gè)冷信號(hào)。那么這個(gè)信號(hào)在訂閱的時(shí)候就會(huì)執(zhí)行里面的過程。那這個(gè)信號(hào)是在什么時(shí)候被訂閱了呢?仔細(xì)回看了代碼,我們發(fā)現(xiàn)并沒有訂閱這個(gè)信號(hào),只是調(diào)用這個(gè)信號(hào)的flattenMap產(chǎn)生了兩個(gè)新的信號(hào)。
這里有一個(gè)很重要的概念,就是任何的信號(hào)轉(zhuǎn)換即是對(duì)原有的信號(hào)進(jìn)行訂閱從而產(chǎn)生新的信號(hào)。由此我們可以寫出flattenMap的偽代碼如下:
- (instancetype)flattenMap_:(RACStream * (^)(id value))block { {return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {return [self subscribeNext:^(id x) {RACSignal *signal = (RACSignal *)block(x);[signal subscribeNext:^(id x) {[subscriber sendNext:x];} error:^(NSError *error) {[subscriber sendError:error];} completed:^{[subscriber sendCompleted];}];} error:^(NSError *error) {[subscriber sendError:error];} completed:^{[subscriber sendCompleted];}];}]; }除了沒有高度復(fù)用和缺少一些disposable的處理以外,上述代碼大致可以比較直觀地說明flattenMap的機(jī)制。觀察會(huì)發(fā)現(xiàn)其實(shí)是在調(diào)用這個(gè)方法的時(shí)候,生成了一個(gè)新的信號(hào),并在這個(gè)新信號(hào)的執(zhí)行過程中對(duì)self進(jìn)行的了訂閱。還需要注意一個(gè)細(xì)節(jié),就是這個(gè)返回信號(hào)在未來訂閱的時(shí)候,才會(huì)間接的訂閱self。后續(xù)的startWith、catchTo等都可以這樣理解。
回到我們的問題,那就是說,在fetchData被flattenMap之后,它就會(huì)因?yàn)槊麨閠itle和desc信號(hào)的訂閱而訂閱。而后續(xù)對(duì)desc也會(huì)進(jìn)行flattenMap,得到了renderedDesc,因此未來renderedDesc被訂閱的時(shí)候,fetchData也會(huì)被間接訂閱。這就解釋了,為什么后續(xù)我們用RAC宏進(jìn)行綁定的時(shí)候,fetchData會(huì)訂閱3次。由于fetchData是冷信號(hào),所以3次訂閱意味著它的過程被執(zhí)行了3次,也就是有3次網(wǎng)絡(luò)請(qǐng)求。
另外的3次訂閱來自RACSignal類的merge方法。根據(jù)上述的描述,我們也可以猜測(cè)merge方法也一定是創(chuàng)建了一個(gè)新的信號(hào),在這個(gè)信號(hào)被訂閱的時(shí)候,把它包含的所有信號(hào)訂閱。所以我們又得到了額外的3次網(wǎng)絡(luò)請(qǐng)求。
由此可以看到,不熟悉冷熱信號(hào)對(duì)業(yè)務(wù)造成的影響。我們可以想象對(duì)用戶流量的影響,對(duì)服務(wù)器負(fù)載的影響,對(duì)統(tǒng)計(jì)的影響,如果這是一個(gè)點(diǎn)贊的接口,會(huì)不會(huì)造成多次點(diǎn)贊?后果不堪設(shè)想啊。而這些都可以通過將fetchData轉(zhuǎn)換為熱信號(hào)來解決。
接下來也許你會(huì)問,如果我的整個(gè)計(jì)算過程中都沒有副作用,是否就不會(huì)有這個(gè)問題?答案是肯定的。試想下剛才那段代碼如果沒有網(wǎng)絡(luò)請(qǐng)求,換成一些標(biāo)準(zhǔn)化的計(jì)算會(huì)怎樣。雖然可以肯定它不會(huì)出現(xiàn)bug,但是不要忽視其中的運(yùn)算也會(huì)執(zhí)行多次。純函數(shù)還有一個(gè)概念就是引用透明。在純函數(shù)式語言(例如Haskell)中對(duì)此可以進(jìn)行一定的優(yōu)化,也就是說純函數(shù)的調(diào)用在相同參數(shù)下的返回值第二次不需要計(jì)算,所以在純函數(shù)式語言里面的FRP并沒有冷信號(hào)的擔(dān)憂。然而Objective-C語言中并沒有這種純函數(shù)優(yōu)化,因此有大規(guī)模運(yùn)算的冷信號(hào)對(duì)性能是有一定影響的。
從上文內(nèi)容可以看出,如果我們想更好地掌握RAC這個(gè)框架,區(qū)分冷信號(hào)與熱信號(hào)是十分重要的。接下來的系列第三篇文章,我會(huì)揭示冷信號(hào)與熱信號(hào)的本質(zhì),幫助大家正確的理解冷信號(hào)與熱信號(hào)。
總結(jié)
以上是生活随笔為你收集整理的细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Leaf:美团分布式ID生成服务开源
- 下一篇: 基于 KIF 的 iOS UI 自动化测