日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号

發布時間:2024/7/5 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前一篇文章我們介紹了冷信號與熱信號的概念,可能有同學會問了,為什么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在實際的使用中也不可避免地要接觸副作用。下面通過一個業務場景,來看看冷信號中副作用的坑:

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的偽代碼如下:

    - (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這個框架,區分冷信號與熱信號是十分重要的。接下來的系列第三篇文章,我會揭示冷信號與熱信號的本質,幫助大家正確的理解冷信號與熱信號。

    總結

    以上是生活随笔為你收集整理的细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。