背景和面臨的問(wèn)題(2016.6.29)
隨著移動(dòng)互聯(lián)網(wǎng)的蓬勃發(fā)展,iOS App的復(fù)雜度呈指數(shù)增長(zhǎng)。美團(tuán)·大眾點(diǎn)評(píng)兩個(gè)App隨著用戶(hù)量不斷增加、研發(fā)工程師數(shù)量不斷增多扰路,可用性的要求也隨之不斷提升。在這樣的一個(gè)背景之下倔叼,我們面臨了很多的問(wèn)題和挑戰(zhàn)汗唱。美團(tuán)和大眾點(diǎn)評(píng)的iOS工程師們面對(duì)挑戰(zhàn),想出了很多的策略和方針來(lái)應(yīng)對(duì)丈攒,引入函數(shù)響應(yīng)式編程就是美團(tuán)App中重要的一環(huán)哩罪。
函數(shù)響應(yīng)式編程簡(jiǎn)介
函數(shù)式編程想必您一定聽(tīng)過(guò),但響應(yīng)式編程的說(shuō)法就不大常見(jiàn)了巡验。與響應(yīng)式編程對(duì)應(yīng)的命令式編程就是大家所熟知的一種編程范式际插,我們先來(lái)看一段代碼:
int a = 3;
int b = 4;
int c = a + b;
NSLog(@"c is %d", c); // => 12
a = 5;
b = 7;
NSLog(@"c is %d", c); // 仍然是12
命令式編程就是通過(guò)表達(dá)式或語(yǔ)句來(lái)改變狀態(tài)量,例如c = a + b就是一個(gè)表達(dá)式显设,它創(chuàng)建了一個(gè)名稱(chēng)為c的狀態(tài)量框弛,其值為a與b的加和。下面的a = 5是另一個(gè)語(yǔ)句捕捂,它改變了a的值瑟枫,但這時(shí)c是沒(méi)有變化的。所以命令式編程中c = a + b只是一個(gè)瞬時(shí)的過(guò)程指攒,而不是一個(gè)關(guān)系描述慷妙。在傳統(tǒng)的開(kāi)發(fā)中,想讓c跟隨a和b的變化而變化是比較困難的允悦。而讓c的值實(shí)時(shí)等于a與b的加和的編程方式就是響應(yīng)式編程膝擂。
實(shí)際上,在日常的開(kāi)發(fā)中我們會(huì)經(jīng)常使用響應(yīng)式編程的思想來(lái)進(jìn)行開(kāi)發(fā)。最典型的例子就是Excel架馋,當(dāng)我們?cè)谝粋€(gè)B1單元格上書(shū)寫(xiě)一個(gè)公式“=A1+5”時(shí)狞山,便聲明了一種對(duì)應(yīng)關(guān)系,每當(dāng)A1單元格發(fā)生變化時(shí)叉寂,單元格B2都會(huì)隨之改變铣墨。
image
圖1 Excel中的響應(yīng)式
iOS開(kāi)發(fā)中也有響應(yīng)式編程的典型例子,例如Autolayout办绝。我們通過(guò)設(shè)置約束描述了各個(gè)視圖的位置關(guān)系,一旦其中一個(gè)變化姚淆,另一個(gè)就會(huì)響應(yīng)其變化孕蝉。類(lèi)似的例子還有很多。
函數(shù)響應(yīng)式編程(英文Functional Reactive Programming腌逢,以下簡(jiǎn)稱(chēng)FRP降淮,)正是在函數(shù)式編程的基礎(chǔ)之上,增加了響應(yīng)式的支持搏讶。
簡(jiǎn)單來(lái)講佳鳖,F(xiàn)RP是基于異步事件流進(jìn)行編程的一種編程范式。針對(duì)離散事件序列進(jìn)行有效的封裝媒惕,利用函數(shù)式編程的思想系吩,滿(mǎn)足響應(yīng)式編程的需要。
區(qū)別于面向過(guò)程編程范式以過(guò)程單元作為核心組成部分妒蔚,面向?qū)ο缶幊谭妒揭詫?duì)象單元作為核心組成部分穿挨,函數(shù)式編程范式以函數(shù)和高階函數(shù)作為核心組成部分。FRP則以離散有序序列作為核心組成部分肴盏,也可將其定義為信號(hào)科盛。其特點(diǎn)是具備可迭代特性并且允許離散事件節(jié)點(diǎn)有時(shí)間聯(lián)系,計(jì)算機(jī)科學(xué)中稱(chēng)為Monad菜皂。
嚴(yán)格意義上來(lái)講贞绵,下文提及的iOS開(kāi)發(fā)下的函數(shù)響應(yīng)式編程,并不能算完全的FRP恍飘,這一點(diǎn)榨崩,本文就不做學(xué)術(shù)上的討論了。
接來(lái)下會(huì)為您介紹iOS相關(guān)的FRP內(nèi)容常侣,我們先從選型開(kāi)始蜡饵。
iOS項(xiàng)目的函數(shù)響應(yīng)式編程選型
很長(zhǎng)一段時(shí)間以來(lái),iOS項(xiàng)目并沒(méi)有很好的FRP支持胳施,直到iOS 4.0 SDK中增加了Block語(yǔ)法才為函數(shù)式編程提供了前置條件溯祸,F(xiàn)RP開(kāi)源庫(kù)也逐步健全起來(lái)。
最先與大家見(jiàn)面的莫過(guò)于ReactiveCocoa這樣一個(gè)庫(kù)了,ReactiveCocoa是Github在制作Github客戶(hù)端時(shí)開(kāi)源的一個(gè)副產(chǎn)物焦辅,縮寫(xiě)為RAC博杖。它是Objective-C語(yǔ)言下FRP思想的一個(gè)優(yōu)秀實(shí)例,后續(xù)版本也支持了Swift語(yǔ)言筷登。
Swift語(yǔ)言的推出為iOS界的函數(shù)式編程愛(ài)好者迎來(lái)了曙光剃根。著名的FRP開(kāi)源庫(kù)Rx系列也新增了RxSwift,保持其接口與ReactiveX.net前方、RxJava狈醉、RxJS接口保持一致。
下面對(duì)不同廠商幾個(gè)版本的FRP庫(kù)進(jìn)行簡(jiǎn)單的對(duì)比:
語(yǔ)言 | Objective-C 支持 | Swift 支持 | Cocoa框架支持 | 其他 |
---|---|---|---|---|
RAC 2.5 | √ | × | 完善 | 迭代周期長(zhǎng)惠险,穩(wěn)定 |
RAC 3.0+ | √ | √ | 繼承2.5版本 | 開(kāi)始全面支持Swift |
RxSwift | × | √ | 不完善 | 符合Rx標(biāo)準(zhǔn) |
表1 iOS下幾種FRP庫(kù)的對(duì)比
美團(tuán)App由于歷史原因仍然沿用ReactiveCocoa 2.5版本苗傅。下文也主要會(huì)針對(duì)ReactiveCocoa 2.5版本做介紹,但各位可以根據(jù)自己項(xiàng)目的需要來(lái)選擇FRP庫(kù)班巩,其思想和主要的API大同小異渣慕。
為什么需要在iOS項(xiàng)目中引入FRP這樣厚重的庫(kù)呢?
iOS的項(xiàng)目主要以客戶(hù)端項(xiàng)目為主抱慌,主要的業(yè)務(wù)場(chǎng)景就是進(jìn)行頁(yè)面交互和與服務(wù)器拉取數(shù)據(jù)逊桦,這里面會(huì)包含多種事件和異步處理邏輯。FRP本身就是面向事件流的一種開(kāi)發(fā)方式抑进,又擅長(zhǎng)處理異步邏輯强经。所以從邏輯上是滿(mǎn)足iOS客戶(hù)端業(yè)務(wù)需要的。
然而能夠把一個(gè)理念融合到實(shí)際的項(xiàng)目中单匣,需要一個(gè)漫長(zhǎng)的過(guò)程夕凝。所以接下來(lái)就根據(jù)美團(tuán)App在FRP上的實(shí)踐,具體講述下融入FRP的過(guò)程户秤。希望能給大家一些參考码秉。
一步一步進(jìn)行函數(shù)響應(yīng)式編程
眾所周知,F(xiàn)RP的學(xué)習(xí)是存在一定門(mén)檻的鸡号,想必這也是大家對(duì)FRP转砖、ReactiveCocoa這些概念比較畏懼的主要原因。美團(tuán)App在推行FRP的過(guò)程中鲸伴,是采用分步驟的形式府蔗,逐步演化的。其演化的過(guò)程可以分為初探汞窗、入門(mén)姓赤、提高、進(jìn)階這樣四個(gè)階段仲吏。
初探
美團(tuán)App是在2014年5月第一次將ReactiveCocoa這個(gè)開(kāi)源庫(kù)納入到工程中的不铆,當(dāng)時(shí)iOS工程師數(shù)量還不是很多蝌焚,但是已經(jīng)遇到了寫(xiě)法不統(tǒng)一、代碼量膨脹等問(wèn)題了誓斥。
寫(xiě)法不統(tǒng)一聚焦在回調(diào)形式的不統(tǒng)一上只洒,iOS中的回調(diào)方式有非常多的種類(lèi):UIKit主要進(jìn)行的事件處理target-action、跨類(lèi)依賴(lài)推薦的delegate模式劳坑、iOS 4.0納入的block毕谴、利用通知中心(Notifcation Center)進(jìn)行松耦合的回調(diào)、利用鍵值觀察(Key-Value Observe距芬,簡(jiǎn)稱(chēng)KVO)進(jìn)行的監(jiān)聽(tīng)涝开。由于場(chǎng)景不同,選用的規(guī)則也不盡相同框仔,并且我們沒(méi)有辦法很好的界定什么場(chǎng)景該寫(xiě)什么樣的回調(diào)忠寻。
這時(shí)我們發(fā)現(xiàn)ReactiveCocoa這樣一個(gè)開(kāi)源庫(kù),恰好能以統(tǒng)一的形式來(lái)解決跨類(lèi)調(diào)用的問(wèn)題存和,也包裝了UIKit事件處理、Notifcation Center衷旅、KVO等常見(jiàn)的場(chǎng)景捐腿。使其代碼風(fēng)格上高度統(tǒng)一。
使用ReactiveCocoa進(jìn)行統(tǒng)一后柿顶,上述的幾種回調(diào)都可以寫(xiě)成如下形式:
// 代替target-action
[[self.confirmButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
// 回調(diào)內(nèi)容寫(xiě)在這里
}];
// 代替delegate
[[self.scrollView rac_signalForSelector:@selector(scrollViewDidScroll:) fromProtocol:@protocol(UIScrollViewDelegate)]
subscribeNext:^(id x) {
// 回調(diào)內(nèi)容寫(xiě)在這里
}];
// 代替block
[[self asyncProcess]
subscribeNext:^(id x) {
// 回調(diào)內(nèi)容寫(xiě)在這里
} error:^(NSError *error) {
// 錯(cuò)誤處理寫(xiě)到這里
}];
// 代替notification center
[[[NSNotificationCenter defaultCenter] rac_valuesForKeyPath:@"Some-key" observer:nil]
subscribeNext:^(id x) {
// 回調(diào)內(nèi)容寫(xiě)在這里
}];
// 代替KVO
[RACObserve(self, userReportString)
subscribeNext:^(id x) {
// 回調(diào)內(nèi)容寫(xiě)在這里
}];
代碼1 回調(diào)統(tǒng)一
通過(guò)觀察代碼不難發(fā)現(xiàn)茄袖,ReactiveCocoa使得不同場(chǎng)景下的代碼樣式上高度統(tǒng)一,使我們?cè)跁?shū)寫(xiě)代碼嘁锯、維護(hù)代碼宪祥、閱讀代碼方面的效率大大提高。
經(jīng)過(guò)一定的研究家乘,我們也發(fā)現(xiàn)使用RAC(target, key)宏可以更好組織代碼形式蝗羊,利用filter:和map:來(lái)代替原有的代碼,達(dá)到更好復(fù)用仁锯,例如下面兩段代碼:
// 舊寫(xiě)法
@weakify(self)
[[self.textField rac_newTextChannel]
subscribeNext:^(NSString *x) {
@strongify(self)
if (x.length > 15) {
self.confirmButton.enabled = NO;
[self showHud:@"Too long"];
} else {
self.confirmButton.enabled = YES;
}
self.someLabel.text = x;
}];
// 新寫(xiě)法
RACSignal *textValue = [self.textField rac_newTextChannel];
RACSignal *isTooLong = [textValue
map:^id(NSString *value) {
return @(value.length > 15);
}];
RACSignal *whenItsTooLongMessage = [[isTooLong
filter:^BOOL(NSNumber *value) {
return @(value.boolValue);
}]
mapReplace:@"Too long"];
[self rac_liftSelector:@selector(showHud:) withSignals:whenItsTooLongMessage, nil];
RAC(self.confirmButton, enabled) = [isTooLong not];
RAC(self.someLabel, text) = textValue;
代碼2 邏輯優(yōu)化
上述代碼修改雖然代碼行數(shù)有一定的增加耀找,但是結(jié)構(gòu)更加清晰,復(fù)用性也做得更好业崖。
綜上所述野芒,在這一階段,我們主要以回調(diào)形式的統(tǒng)一為主双炕,不斷嘗試合適的代碼形式來(lái)表達(dá)綁定這種關(guān)系狞悲,也尋找一些便捷的小技巧來(lái)優(yōu)化代碼。
入門(mén)
image
圖2 美團(tuán)App首頁(yè)
單純解決回調(diào)風(fēng)格的統(tǒng)一和樹(shù)立綁定的思維是遠(yuǎn)遠(yuǎn)不夠的妇斤,代碼中更大的問(wèn)題在于共享變量摇锋、異步協(xié)同以及異常傳遞的處理丹拯。 列舉幾個(gè)簡(jiǎn)單的場(chǎng)景,就拿美團(tuán)App的首頁(yè)來(lái)講乱投,我們可以看到上面包含很多的區(qū)塊咽笼,而各個(gè)區(qū)塊的訪問(wèn)接口不盡相同,但是渲染的邏輯卻又多種多樣:
有的需要幾個(gè)接口都返回后才能統(tǒng)一渲染戚炫。
有的需要一個(gè)接口返回后剑刑,根據(jù)返回的內(nèi)容決定后續(xù)的接口訪問(wèn)炼绘,最終才能渲染充尉。
有的則是幾個(gè)接口按照返回順序依次渲染寒亥。
這就導(dǎo)致我們?cè)谔幚磉@些邏輯的時(shí)候隔盛,需要很多的異步處理手段笆包,利用一些中間變量來(lái)儲(chǔ)存狀態(tài)熟嫩,每次返回的時(shí)候又判斷這些狀態(tài)決定渲染的邏輯片习。
更糟糕的是照卦,有的時(shí)候?qū)τ谕瑫r(shí)請(qǐng)求多個(gè)網(wǎng)絡(luò)接口蔑赘,某些出現(xiàn)了網(wǎng)絡(luò)錯(cuò)誤狸驳,異常處理也變得越來(lái)越復(fù)雜。
隨著對(duì)ReactiveCocoa理解的加深缩赛,我們意識(shí)到使用信號(hào)的組合等“高級(jí)”操作可以幫助我們解決很多的問(wèn)題耙箍。例如merge:操作可以解決依次渲染的問(wèn)題,zip:操作可以解決多個(gè)接口都返回后再渲染的問(wèn)題酥馍,flattenMap:可以解決接口串聯(lián)的問(wèn)題辩昆。大概的示例代碼如下:
// 依次渲染
RACSignal *mergedSignal = [RACSignal merge:@[[self fetchData1],
[self fetchData2]]];
// 接口都返回后一起渲染
RACSignal *zippedSignal = [RACSignal zip:@[[self fetchData1],
[self fetchData3]]];
// 接口串聯(lián)
@weakify(self)
RACSignal *finalSignal = [[self fetchData4]
flattenMap:^RACSignal *(NSString *data4Result) {
@strongify(self)
return [self fetchData5:data4Result];
}];
沒(méi)有用到一個(gè)中間狀態(tài)變量,我們通過(guò)這幾個(gè)“魔法接口”神奇地將邏輯描述了出來(lái)旨袒。這樣寫(xiě)的好處還有很多汁针。
FRP具備這樣一個(gè)特點(diǎn),信號(hào)因?yàn)檫M(jìn)行組合從而得到了一個(gè)數(shù)據(jù)鏈砚尽,而數(shù)據(jù)鏈的任一節(jié)點(diǎn)發(fā)出錯(cuò)誤信號(hào)施无,都可以順著這個(gè)鏈條最終交付給訂閱者。這就正好解決了異常處理的問(wèn)題必孤。
image
圖3 錯(cuò)誤傳遞鏈
由于此項(xiàng)特性帆精,我們可以不再關(guān)注錯(cuò)誤在哪個(gè)環(huán)節(jié),只需等待訂閱的時(shí)候統(tǒng)一處理即可隧魄。我們也找到了很多的方法用來(lái)更好地支持異常的處理卓练。例如try:、catch:购啄、catchTo:襟企、tryMap:等。
簡(jiǎn)單列舉下示例:
// 嘗試判斷并捕捉異常
RACSignal *signalForSubscriber =
[[[[[self fetchData1]
try:^BOOL(NSString *data1,
NSError *__autoreleasing *errorPtr) {
if (data1.length == 0) {
*errorPtr = [NSError new];
return YES;
}
return NO;
}]
flattenMap:^RACStream *(id value) {
@strongify(self)
return [self fetchData5:value];
}]
try:^BOOL(id value,
NSError *__autoreleasing *errorPtr) {
if (![value isKindOfClass:[NSString class]]) {
*errorPtr = [NSError new];
return YES;
}
return NO;
}]
catch:^RACSignal *(NSError *error) {
return [RACSignal return:error.domain];
}];
總結(jié)一下狮含,在這個(gè)階段顽悼,我們主要嘗試解決了異步協(xié)同的問(wèn)題曼振,包括了異常的處理。運(yùn)用了異常處理模型來(lái)解決了很多的實(shí)際問(wèn)題蔚龙,同時(shí)繼續(xù)尋找了更多的技巧來(lái)優(yōu)化代碼冰评。
在初探和入門(mén)這兩個(gè)階段,美團(tuán)App還只是謹(jǐn)慎地進(jìn)行小的嘗試木羹,主旨是以代碼簡(jiǎn)化為目的甲雅,使用ReactiveCocoa這個(gè)開(kāi)源框架的一些便利功能來(lái)優(yōu)化代碼。在代碼覆蓋程度上盡量只在模塊內(nèi)部使用坑填,避免跨層使用ReactiveCocoa抛人。
提高
隨著對(duì)ReactiveCocoa這個(gè)開(kāi)源框架的理解不斷加深。美團(tuán)App并不滿(mǎn)足于簡(jiǎn)單的嘗試脐瑰,而是開(kāi)始在更多的場(chǎng)景下使用ReactiveCocoa妖枚,并體現(xiàn)一定的FRP思想。這個(gè)階段最具代表性的實(shí)踐就是與MVVM架構(gòu)的融合了苍在,它就是體現(xiàn)了FRP響應(yīng)式的思想绝页。
Model-View-Controller(簡(jiǎn)稱(chēng)MVC)是蘋(píng)果Cocoa框架默認(rèn)的一個(gè)架構(gòu)。實(shí)際上業(yè)務(wù)場(chǎng)景的復(fù)雜度越來(lái)越高寂恬,而MVC架構(gòu)自身也存在分層不清晰等諸多問(wèn)題抒寂,最終使得MVC這一架構(gòu)在實(shí)際的使用中漸漸走了樣。
Model-View-ViewModel(簡(jiǎn)稱(chēng)MVVM)便是近幾年來(lái)十分推崇的一種架構(gòu)掠剑,它解決了MVC架構(gòu)的一些不足,在層次定義上更為清晰郊愧。在MVVM的架構(gòu)中朴译,最為關(guān)鍵的一環(huán)莫過(guò)于ViewModel層與View層的綁定了,我們的主角FRP恰好可以解決綁定問(wèn)題属铁,同時(shí)還能處理跨層錯(cuò)誤處理的問(wèn)題眠寿。
先來(lái)關(guān)注下綁定,自初探階段開(kāi)始焦蘑,我們就開(kāi)始使用RAC(target, key)這樣的一個(gè)宏來(lái)表述綁定的關(guān)系盯拱,并且使用一些簡(jiǎn)單的信號(hào)轉(zhuǎn)換使得原始信號(hào)滿(mǎn)足視圖渲染的需求。在引入MVVM架構(gòu)后例嘱,我們將之前的經(jīng)驗(yàn)利用起來(lái)狡逢,并且使用了RACChannel、RACCommand等組件來(lái)支持MVVM拼卵。
// 單向綁定
RAC(self.someLabel, text) = RACObserve(self.viewModel, someProperty);
RAC(self.scrollView, hidden) = self.viewModel.someSignal;
RAC(self.confirmButton, frame) = [self.viewModel.someChannel
map:^id(NSString *str) {
CGRect rect = CGRectMake(0, 0, 0, str.length * 3);
return [NSValue valueWithCGRect:rect];
}];
// 雙向綁定
RACChannelTo(self.someLabel, text) = RACChannelTo(self.viewModel, someProperty);
[self.textField.rac_newTextChannel subscribe:self.viewModel.someChannel];
[self.viewModel.someChannel subscribe:self.textField.rac_newTextChannel];
RACChannelTo(self, reviewID) = self.viewModel.someChannel;
// 命令綁定
self.confirmButton.rac_command = self.viewModel.someCommand;
RAC(self.textField, hidden) = self.viewModel.someCommand.executing;
[self.viewModel.someCommand.errors
subscribeNext:^(NSError *error) {
// 錯(cuò)誤處理在這里
}];
綁定只是提供了上層的業(yè)務(wù)邏輯奢浑,更為重要的是,F(xiàn)RP的響應(yīng)式范式恰如其分地體現(xiàn)在MVVM中腋腮。一個(gè)MVVM中View就會(huì)響應(yīng)ViewModel的變化雀彼。我們來(lái)根據(jù)一副簡(jiǎn)單的圖來(lái)分析一下:
image
圖4 MVVM示意圖
上述簡(jiǎn)圖列出了View-ViewModel-Model的大致關(guān)系壤蚜,View和ViewModel間通過(guò)RACSignal來(lái)進(jìn)行單向綁定,通過(guò)RACChannel來(lái)進(jìn)行雙向綁定徊哑,通過(guò)RACCommand進(jìn)行執(zhí)行過(guò)程的綁定袜刷。
ViewModel和Model間通過(guò)RACObserve進(jìn)行監(jiān)聽(tīng),通過(guò)RACSignal進(jìn)行回調(diào)處理莺丑,也可以直接調(diào)用方法著蟹。
Model有自身的數(shù)據(jù)業(yè)務(wù)邏輯,包含請(qǐng)求Web Service和進(jìn)行本地持久化窒盐。
響應(yīng)式的體現(xiàn)就在于View從一開(kāi)始就是“聲明”了與ViewModel間的關(guān)系草则,就如同A3單元格聲明其“=A2+A1”一樣。一旦后續(xù)數(shù)據(jù)發(fā)生變化蟹漓,就按照之前的約定響應(yīng)炕横,彼此之間存在一層明確的定義。View在業(yè)務(wù)層面也得到了極大簡(jiǎn)化葡粒。
具體的數(shù)據(jù)流動(dòng)就如同下圖兩種形式:
image
image
圖5&圖6 MVVM的數(shù)據(jù)流向示意
從兩張圖中可以看出份殿,無(wú)論View收到用戶(hù)修改TextField的文本框內(nèi)容的事件,還是受到用戶(hù)點(diǎn)擊Button的事件嗽交。View層都不需要對(duì)此做特殊的邏輯處理卿嘲,只是將之傳遞給ViewModel。而ViewModel自身維護(hù)邏輯夫壁,并體現(xiàn)在某些綁定關(guān)系上拾枣。這是與MVC中ViewController和Model的關(guān)系是截然不同的。FRP的響應(yīng)式范式很好的幫助我們實(shí)現(xiàn)了此類(lèi)需求盒让。
之前雖然也提到過(guò)錯(cuò)誤處理梅肤,但是也提到美團(tuán)App在初探和入門(mén)階段,只是小規(guī)模的在模塊內(nèi)使用邑茄,對(duì)外并不會(huì)以RACSignal的形式暴露姨蝴。而這個(gè)階段,我們也嘗試了層級(jí)間通過(guò)RACSignal來(lái)進(jìn)行信息的傳遞肺缕。這也自然可以應(yīng)用上FRP異常處理的優(yōu)勢(shì)左医。
image
圖7 MVVM的數(shù)據(jù)流向示意
上圖體現(xiàn)了一個(gè)按鈕綁定了RACCommand收到錯(cuò)誤后的一個(gè)數(shù)據(jù)流向。
除了MVVM框架的應(yīng)用同木,這個(gè)階段美團(tuán)App也利用FRP解決另外的一個(gè)大問(wèn)題浮梢。那就是多線程。
如果你做過(guò)異步拉取數(shù)據(jù)主線程渲染彤路,那么你一定很熟悉子線程的回調(diào)結(jié)果轉(zhuǎn)移到主線程的過(guò)程黔寇。這種操作不但繁瑣,重復(fù)斩萌,關(guān)鍵還容易出錯(cuò)缝裤。
RAC提供了很多便于處理多線程的組件屏轰,核心類(lèi)為RACScheduler,使得可以方便的通過(guò)subscirbeOn:方法憋飞、deliverOn:方法進(jìn)行線程控制霎苗。
// 多線程控制
RACScheduler *backgroundScheduler = [RACScheduler scheduler];
RACSignal *testSignal = [[RACSignal
createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 本例中,這段代碼會(huì)確保運(yùn)行在子線程
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}]
subscribeOn:backgroundScheduler];
[[RACScheduler mainThreadScheduler]
schedule:^{
// 這段代碼會(huì)在下次Runloop時(shí)在主線程中運(yùn)行
[[testSignal
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id x) {
// 雖然信號(hào)的發(fā)出者是在子線程中發(fā)出消息的
// 但是接收者確可以確保在主線程接收消息
// 主線程可以做你想做的渲染工作了榛做!
}
];
}];
這一個(gè)階段也算是大躍進(jìn)的一個(gè)階段唁盏,隨著MVVM的框架融入,跨層的使用RAC使得代碼整體使用FRP的比重大幅提高检眯,全員對(duì)FRP的熟悉程度和思想的理解也變得深刻了許多厘擂。同時(shí)也真正使用了響應(yīng)式的一些思想和特性來(lái)解決實(shí)際的問(wèn)題。使其不再是紙上空談锰瘸。
我們美團(tuán)App也在這個(gè)階段挖掘了更多的RAC高級(jí)組件刽严,繼續(xù)代碼優(yōu)化的持續(xù)之路。
進(jìn)階
美團(tuán)App的iOS工程師們?cè)诖笠?guī)模使用FRP后避凝,也積蓄了很多的問(wèn)題舞萄。很多小伙伴也問(wèn)起了,既然是叫FRP管削,為什么一直體現(xiàn)的都是響應(yīng)式的思想倒脓,對(duì)于函數(shù)式的思想應(yīng)用體現(xiàn)似乎不是很明顯。雖然FRP是F開(kāi)頭含思,稱(chēng)為函數(shù)響應(yīng)式編程崎弃。但是考慮到函數(shù)式編程的復(fù)雜性,我們也將函數(shù)式編程的優(yōu)化拿到了進(jìn)階這一階段來(lái)嘗試含潘。
這一階段面臨的問(wèn)題是RAC的大規(guī)模應(yīng)用饲做,使得代碼中包含了大量的框架性質(zhì)的代碼。例如下面的代碼:
// 冗余的代碼
[[self fetchData1]
try:^BOOL(id value,
NSError *__autoreleasing *errorPtr) {
if ([value isKindOfClass:[NSString class]]) {
return YES;
}
*errorPtr = [NSError new];
return NO;
}];
[[self fetchData2]
tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
if ([value isKindOfClass:[NSDictionary class]]) {
if (value[@"someKey"]) {
return value[@"someKey"];
}
// 并沒(méi)有一個(gè)名為“someKey”的key
*errorPtr = [NSError new];
return nil;
}
// 這不是一個(gè)Dictionary
*errorPtr = [NSError new];
return nil;
}];
[[self fetchData3]
tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
if ([value isKindOfClass:[NSDictionary class]]) {
if (value[@"someOtherKey"]) {
return value[@"someOtherKey"];
}
// 并沒(méi)有一個(gè)名為“someOtherKey”的key
*errorPtr = [NSError new];
return nil;
}
// 這不是一個(gè)Dictionary
*errorPtr = [NSError new];
return nil;
}];
上述的幾個(gè)代碼段调鬓,我們可以看到功能非常近似,內(nèi)容稍有不同的部分重復(fù)出現(xiàn)酌伊,很多的同學(xué)在實(shí)際的開(kāi)發(fā)中也并沒(méi)有太好地優(yōu)化它們腾窝,甚至很多的同學(xué)表示束手無(wú)策。這時(shí)候函數(shù)式編程就可以派上用場(chǎng)了居砖。
函數(shù)式編程是一種良好的編程范式虹脯,我們?cè)谶@里主要利用它的幾個(gè)特點(diǎn):高階函數(shù)、不變量和迭代奏候。
先來(lái)看高階函數(shù)循集,高階函數(shù)是入?yún)⑹呛瘮?shù)或者返回值是函數(shù)的函數(shù)。說(shuō)起來(lái)雖然有些拗口蔗草,但實(shí)際上在iOS開(kāi)發(fā)中司空見(jiàn)慣咒彤,例如典型的訂閱其實(shí)就是一個(gè)高階函數(shù)的體現(xiàn)疆柔。
// 高階函數(shù)
[[self fetchData1]
subscribeNext:^(id x) {
// 這是一個(gè)block,作為一個(gè)參數(shù)
}];
我們更關(guān)心的是返回值是函數(shù)的函數(shù)镶柱,這是上面冗長(zhǎng)的代碼解決之道旷档。代碼7的代碼中會(huì)發(fā)現(xiàn)一些相同的邏輯,例如類(lèi)型判斷歇拆。我們就可以先做一個(gè)這樣的小函數(shù):
typedef BOOL (^VerifyFunction)(id value);
VerifyFunction isKindOf(Class aClass)
{
return ^BOOL(id value) {
return [value isKindOfClass:aClass];
};
}
瞧鞋屈,很簡(jiǎn)單對(duì)不對(duì)!只要把一個(gè)類(lèi)型傳進(jìn)去故觅,就會(huì)得到一個(gè)用來(lái)判斷某個(gè)對(duì)象是否是這個(gè)類(lèi)型的函數(shù)厂庇。細(xì)心的讀者會(huì)發(fā)現(xiàn)我們實(shí)際要的是一個(gè)入?yún)閷?duì)象和一個(gè)NSError對(duì)象指針的指針類(lèi)型,返回值是布爾類(lèi)型的block输吏,但是這個(gè)只能返回入?yún)⑹菍?duì)象的权旷,顯然不滿(mǎn)足條件。很多人第一個(gè)想到的就是把這個(gè)函數(shù)改成返回參數(shù)為兩個(gè)參數(shù)返回值為布爾類(lèi)型的block评也,但是函數(shù)式的解決方法是新增一個(gè)這樣的函數(shù):
typedef BOOL (^VerifyAndOutputErrorFunction)(id value, NSError **error);
VerifyAndOutputErrorFunction verifyAndOutputError(VerifyFunction verify,
NSError *outputError)
{
return ^BOOL(id value, NSError **error) {
if (verify(value)) {
return YES;
}
*error = outputError;
return NO;
};
}
一個(gè)典型的高階函數(shù)炼杖,入?yún)в幸粋€(gè)block,返回值也是一個(gè)block盗迟,組合起來(lái)就可以把剛才的幾個(gè)try:代碼段優(yōu)化坤邪。可能你會(huì)問(wèn)罚缕,為什么要搞成兩個(gè)呢艇纺,一個(gè)不是更好?搞成兩個(gè)的好處就在于邮弹,我們可以將任意的VerifyFunction類(lèi)型的block與一個(gè)outputError相結(jié)合黔衡,來(lái)返回一個(gè)我們想要的VerifyAndOutputErrorFunction類(lèi)型block,例如增加一個(gè)判斷NSDictionary是否包含某個(gè)Key的VerifyFunction腌乡。下面給出一個(gè)優(yōu)化后的代碼盟劫,大家可以仔細(xì)思考下:
// 可以高度復(fù)用的函數(shù)
typedef BOOL (^VerifyFunction)(id value);
VerifyFunction isKindOf(Class aClass)
{
return ^BOOL(id value) {
return [value isKindOfClass:aClass];
};
}
VerifyFunction hasKey(NSString *key)
{
return ^BOOL(NSDictionary *value) {
return value[key] != nil;
};
}
typedef BOOL (^VerifyAndOutputErrorFunction)(id value, NSError **error);
VerifyAndOutputErrorFunction verifyAndOutputError(VerifyFunction verify,
NSError *outputError)
{
return ^BOOL(id value, NSError **error) {
if (verify(value)) {
return YES;
}
*error = outputError;
return NO;
};
}
typedef id (^MapFunction)(id value);
MapFunction dictionaryValueByKey(NSString *key)
{
return ^id(NSDictionary *value) {
return value[key];
};
}
// 與本例關(guān)聯(lián)比較大的函數(shù)
typedef id (^MapAndOutputErrorFunction)(id value, NSError **error);
MapAndOutputErrorFunction transferToKeyChild(NSString *key)
{
return ^id(id value, NSError **error) {
if (hasKey(key)(value)) {
return dictionaryValueByKey(key)(value);
} else {
*error = [NSError new];
return nil;
}
};
};
- (void)oldStyle
{
// 冗余的代碼
[[self fetchData1]
try:^BOOL(id value,
NSError *__autoreleasing *errorPtr) {
if ([value isKindOfClass:[NSString class]]) {
return YES;
}
*errorPtr = [NSError new];
return NO;
}];
[[self fetchData2]
tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
if ([value isKindOfClass:[NSDictionary class]]) {
if (value[@"someKey"]) {
return value[@"someKey"];
}
// 并沒(méi)有一個(gè)名為“someKey”的key
*errorPtr = [NSError new];
return nil;
}
// 這不是一個(gè)Dictionary
*errorPtr = [NSError new];
return nil;
}];
[[self fetchData3]
tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
if ([value isKindOfClass:[NSDictionary class]]) {
if (value[@"someOtherKey"]) {
return value[@"someOtherKey"];
}
// 并沒(méi)有一個(gè)名為“someOtherKey”的key
*errorPtr = [NSError new];
return nil;
}
// 這不是一個(gè)Dictionary
*errorPtr = [NSError new];
return nil;
}];
}
- (void)newStyle
{
VerifyAndOutputErrorFunction isDictionary =
verifyAndOutputError(isKindOf([NSDictionary class]),
NSError.new);
VerifyAndOutputErrorFunction isString =
verifyAndOutputError(isKindOf([NSString class]),
NSError.new);
[[self fetchData1]
try:isString];
[[[self fetchData2]
try:isDictionary]
tryMap:transferToKeyChild(@"someKey")];
[[[self fetchData3]
try:isDictionary]
tryMap:transferToKeyChild(@"someOtherKey")];
}
雖然代碼有些多,但是從newStyle函數(shù)的結(jié)果來(lái)看与纽,我們?cè)趯?shí)際的業(yè)務(wù)代碼上非常的簡(jiǎn)潔侣签,而且還抽離出很多可復(fù)用的小函數(shù)。在實(shí)際的業(yè)務(wù)中急迂,我們甚至通過(guò)這種范式在某些業(yè)務(wù)場(chǎng)景簡(jiǎn)化了超過(guò)50%的代碼量影所。
除此之外,我們還嘗試用迭代來(lái)進(jìn)一步減少臨時(shí)變量僚碎。為什么要減少臨時(shí)變量呢猴娩?因?yàn)槲覀兿胍裱蛔兞吭瓌t,這是函數(shù)式編程的一個(gè)特點(diǎn)。試想下如果我們都是使用一些不變量卷中,就不再會(huì)有那么多異步鎖和痛苦的多線程問(wèn)題了矛双。基于以上考慮仓坞,我們要求工程師盡量在開(kāi)發(fā)的過(guò)程中減少使用變量背零,從而鍛煉用更加函數(shù)式的方式來(lái)解決問(wèn)題。
例如下面的簡(jiǎn)單問(wèn)題无埃,實(shí)現(xiàn)一個(gè)每秒發(fā)送值為0 1 2 3 … 100的遞增整數(shù)信號(hào)徙瓶,實(shí)現(xiàn)的方法可以是這樣:
- (void)countSignalOldStyle
{
RACSignal *signal =
[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACDisposable *disposable = [RACDisposable new];
__block int i = 0;
__block void (^recursion)();
recursion = ^{
if (disposable.disposed || i > 100) {
return;
}
[subscriber sendNext:@(i)];
++i;
[[RACScheduler mainThreadScheduler]
afterDelay:1 schedule:recursion];
};
recursion();
return disposable;
}];
}
這樣的代碼不但用了block自遞歸,還用了一個(gè)閉包的i變量嫉称。i變量也在數(shù)次遞歸中進(jìn)行了修改侦镇。代碼不易理解且block自遞歸會(huì)存在循環(huán)引用。使用迭代和不變量的形式是這樣的:
- (void)countSignalNewStyle
{
RACSignal *signal =
[[[[[[RACSignal return:@1]
repeat] take: 100]
aggregateWithStart:@0 reduce:^id(NSNumber *running,
NSNumber *next) {
return @(running.integerValue + next.integerValue);
}]
map:^id(NSNumber *value) {
return [[RACSignal return:value]
delay:1];
}]
concat];
}
解法是這樣的织阅,先用固定返回1的信號(hào)生成一個(gè)無(wú)限重復(fù)信號(hào)壳繁,取前100個(gè)值,然后用迭代方法荔棉,產(chǎn)生一個(gè)遞增的迭代闹炉,再將發(fā)送的密集的遞增信號(hào)轉(zhuǎn)成一個(gè)延時(shí)1秒的子信號(hào),最后將子信號(hào)進(jìn)行連接润樱。感興趣的同學(xué)可以自己動(dòng)手嘗試下渣触,也希望大家都去思考不適用變量來(lái)解決問(wèn)題的思路。
這些函數(shù)式的寫(xiě)法不僅解決了業(yè)務(wù)上的問(wèn)題壹若,也給我們美團(tuán)App的iOS工程師們開(kāi)拓了代碼優(yōu)化的新思路嗅钻。
可以看到,到這一階段店展,需要對(duì)FRP的理解要求更高养篓。為了追求更好的代碼體驗(yàn),我們朝著FRP的道路又邁進(jìn)了許多赂蕴,走到這一步是每一個(gè)美團(tuán)App的iOS工程師共同努力的結(jié)果柳弄。這是一個(gè)尚未完結(jié)的階段,我們的工程師仍然在不選找尋更好的FRP范式概说。對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)碧注,優(yōu)化之路永遠(yuǎn)不會(huì)停步。
總結(jié)
單純靠這樣一篇文章來(lái)介紹全部的FRP思想是不可能的席怪,這也僅是起到了拋磚引玉的作用应闯。FRP不僅可以解決項(xiàng)目中實(shí)際遇到的很多問(wèn)題纤控,也能鍛煉更好的工程師素養(yǎng)挂捻。希望大家能夠掌握起來(lái),用FRP的思想來(lái)解決更多實(shí)際的問(wèn)題船万。社區(qū)和開(kāi)源庫(kù)也需要大家的不斷投入刻撒。謝謝大家骨田!