前一篇文章我們介紹了冷訊號與熱訊號的概念,可能有同學會問了,為什麼RAC要搞得如此複雜呢,只用一種訊號不就行了麼?要解釋這個問題,需要繞一些圈子。
前面可能比較難懂,如果不能很好理解,請仔細閱讀相關文件。
最前面提到了RAC是一套基於Cocoa的FRP框架,那就來說說FRP吧。FRP的全稱是Functional Reactive Programming,中文譯作函式式響應式程式設計,是RP(Reactive Programm,響應式程式設計)的FP(Functional Programming,函數語言程式設計)實現。說起來很拗口。太多的細節不多討論,我們著重關注下FRP的FP特徵。
FP有個很重要的概念是和我們的主題相關的,那就是純函式。
純函式就是返回值只由輸入值決定、而且沒有可見副作用)的函式或者表示式。這和數學中的函式是一樣的,比如:
f(x) = 5x + 1
這個函式在呼叫的過程中除了返回值以外的沒有任何對外界的影響,除了入參x以外也不受任何其他外界因素的影響。
那麼副作用都有哪些呢?我來列舉以下幾個情況:
- 函式的處理過程中,修改了外部的變數,例如全域性變數。一個特殊點的例子,就是如果把OC的一個方法看做一個函式,所有的成員變數的賦值都是對外部變數的修改。是的,從FP的角度看OOP是充滿副作用的。
- 函式的處理過程中,觸發了一些額外的動作,例如傳送了一個全域性的Notification,在console裡面輸出了一行資訊,儲存了檔案,觸發了網路,更新了螢幕等。
- 函式的處理過程中,受到外部變數的影響,例如全域性變數,方法裡面用到的成員變數。注意block中捕獲的外部變數也算副作用。
- 函式的處理過程中,受到執行緒鎖的影響算副作用。
由此我們可以看出,在目前的iOS程式設計中,我們是很難擺脫副作用的。甚至可以這麼說,我們iOS程式設計的目的其實就是產生各種副作用。(基於使用者觸控的外界因素,最終反饋到網路變化和螢幕變化上。)
接下來我們來分析副作用與冷熱訊號的關係。既然iOS程式設計中少不了副作用,那麼RAC在實際的使用中也不可避免地要接觸副作用。下面通過一個業務場景,來看看冷訊號中副作用的坑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
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]; }]; |
不知道大家有沒有被這麼一大段的程式碼嚇到,我想要表達的是,在真正的工程中,我們的業務邏輯是很複雜的,而一些坑就隱藏在如此看似複雜但是又很合理的程式碼之下。所以我儘量模擬了一些需求,使得程式碼看起來更豐富。下面我們還是來仔細看下這段程式碼的邏輯吧:
- 建立了一個
AFHTTPSessionManager
用來做網路介面的資料獲取。 - 建立了一個名為
fetchData
的訊號來通過網路獲取資訊。 - 建立一個名為
title
的訊號從獲取的data
中取得title
欄位,如果沒有該欄位則反饋一個錯誤。 - 建立一個名為
desc
的訊號從獲取的data
中取得desc
欄位,如果沒有該欄位則反饋一個錯誤。 - 針對
desc
這個訊號做一個渲染,得到一個名為renderedDesc
的新訊號,該訊號會在渲染失敗的時候反饋一個錯誤。 - 把
title
訊號所有的錯誤轉換為字串@"Error"
並且在沒有獲取值之前以字串@"Loading..."
佔位,之後與self.someLablel
的text
屬性繫結。 - 把
desc
訊號所有的錯誤轉換為字串@"Error"
並且在沒有獲取值之前以字串@"Loading..."
佔位,之後與self.originTextView
的text
屬性繫結。 - 把
renderedDesc
訊號所有的錯誤轉換為屬性字串@"Error"
並且在沒有獲取值之前以屬性字串@"Loading..."
佔位,之後與self.renderedTextView
的text
屬性繫結。 - 訂閱
title
、desc
、renderedDesc
這三個訊號的任何錯誤,並且彈出UIAlertView
。
這些程式碼體現了RAC的一些優勢,例如良好的錯誤處理和各種鏈式處理。很不錯,對不對?但是很遺憾的告訴大家,這段程式碼其實有很嚴重的錯誤。
如果你去嘗試執行這段程式碼,並且開啟Charles檢視,你會驚奇的發現,這個網路請求傳送了6次。沒錯,是6次請求。我們也可以想象到類似的程式碼存在其他副作用的問題,重新重新整理了6次螢幕,寫入6次檔案,發了6個全域性通知。
下面來分析,為什麼是6次網路請求呢?首先根據上面的知識,可以推斷出名為fetchData
訊號是一個冷訊號。那麼這個訊號在訂閱的時候就會執行裡面的過程。那這個訊號是在什麼時候被訂閱了呢?仔細回看了程式碼,我們發現並沒有訂閱這個訊號,只是呼叫這個訊號的flattenMap
產生了兩個新的訊號。
這裡有一個很重要的概念,就是任何的訊號轉換即是對原有的訊號進行訂閱從而產生新的訊號。由此我們可以寫出flattenMap的虛擬碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (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]; }]; }]; } |
除了沒有高度複用和缺少一些disposable的處理以外,上述程式碼大致可以比較直觀地說明flattenMap的機制。觀察會發現其實是在呼叫這個方法的時候,生成了一個新的訊號,並在這個新訊號的執行過程中對self
進行的了訂閱。還需要注意一個細節,就是這個返回訊號在未來訂閱的時候,才會間接的訂閱self
。後續的startWith
、catchTo
等都可以這樣理解。
回到我們的問題,那就是說,在fetchData
被flattenMap
之後,它就會因為名為title
和desc
訊號的訂閱而訂閱。而後續對desc
也會進行flattenMap
,得到了renderedDesc
,因此未來renderedDesc
被訂閱的時候,fetchData
也會被間接訂閱。這就解釋了,為什麼後續我們用RAC
巨集進行繫結的時候,fetchData
會訂閱3次。由於fetchData
是冷訊號,所以3次訂閱意味著它的過程被執行了3次,也就是有3次網路請求。
另外的3次訂閱來自RACSignal
類的merge
方法。根據上述的描述,我們也可以猜測merge
方法也一定是建立了一個新的訊號,在這個訊號被訂閱的時候,把它包含的所有訊號訂閱。所以我們又得到了額外的3次網路請求。
由此可以看到,不熟悉冷熱訊號對業務造成的影響。我們可以想象對使用者流量的影響,對伺服器負載的影響,對統計的影響,如果這是一個點讚的介面,會不會造成多次點贊?後果不堪設想啊。而這些都可以通過將fetchData
轉換為熱訊號來解決。
接下來也許你會問,如果我的整個計算過程中都沒有副作用,是否就不會有這個問題?答案是肯定的。試想下剛才那段程式碼如果沒有網路請求,換成一些標準化的計算會怎樣。雖然可以肯定它不會出現bug,但是不要忽視其中的運算也會執行多次。純函式還有一個概念就是引用透明)。在純函式式語言(例如Haskell)中對此可以進行一定的優化,也就是說純函式的呼叫在相同引數下的返回值第二次不需要計算,所以在純函式式語言裡面的FRP並沒有冷訊號的擔憂。然而Objective-C語言中並沒有這種純函式優化,因此有大規模運算的冷訊號對效能是有一定影響的。
從上文內容可以看出,如果我們想更好地掌握RAC這個框架,區分冷訊號與熱訊號是十分重要的。接下來的系列第三篇文章,我會揭示冷訊號與熱訊號的本質,幫助大家正確的理解冷訊號與熱訊號。