iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案

轉(zhuǎn)載自Casa Taloyum架構(gòu)系列文章:
iOS應(yīng)用架構(gòu)談 開篇
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案
iOS應(yīng)用架構(gòu)談 本地持久化方案及動(dòng)態(tài)部署
iOS應(yīng)用架構(gòu)談 組件化方案

前言

網(wǎng)絡(luò)層在一個(gè)App中也是一個(gè)不可缺少的部分签餐,工程師們?cè)诰W(wǎng)絡(luò)層能夠發(fā)揮的空間也比較大假抄。另外,蘋果對(duì)網(wǎng)絡(luò)請(qǐng)求部分已經(jīng)做了很好的封裝撮珠,業(yè)界的AFNetworking也被廣泛使用歌粥。其它的ASIHttpRequest塌忽,MKNetworkKit啥的其實(shí)也都還不錯(cuò),但前者已經(jīng)棄坑失驶,后者也在棄坑的邊緣土居。在實(shí)際的App開發(fā)中,Afnetworking已經(jīng)成為了事實(shí)上各大App的標(biāo)準(zhǔn)配置嬉探。

網(wǎng)絡(luò)層在一個(gè)App中承載了API調(diào)用擦耀,用戶操作日志記錄,甚至是即時(shí)通訊等任務(wù)甲馋。我接觸過一些App(開源的和不開源的)的代碼埂奈,在看到網(wǎng)絡(luò)層這一塊時(shí),尤其是在看到各位架構(gòu)師各顯神通展示了各種技巧定躏,我非常為之感到興奮账磺。但有的時(shí)候芹敌,往往也對(duì)于其中的一些缺陷感到失望。

關(guān)于網(wǎng)絡(luò)層的設(shè)計(jì)方案會(huì)有很多垮抗,需要權(quán)衡的地方也會(huì)有很多氏捞,甚至于爭(zhēng)議的地方都會(huì)有很多。但無(wú)論如何冒版,我都不會(huì)對(duì)這些問題做出任何逃避液茎,我會(huì)在這篇文章中給出我對(duì)它們的看法和解決方案,觀點(diǎn)絕不中立辞嗡,不會(huì)跟大家打太極捆等。

這篇文章就主要會(huì)講這些方面:

  1. 網(wǎng)絡(luò)層跟業(yè)務(wù)對(duì)接部分的設(shè)計(jì)
  2. 網(wǎng)絡(luò)層的安全機(jī)制實(shí)現(xiàn)
  3. 網(wǎng)絡(luò)層的優(yōu)化方案

網(wǎng)絡(luò)層跟業(yè)務(wù)對(duì)接部分的設(shè)計(jì)

在安居客App的架構(gòu)更新?lián)Q代的時(shí)候,我深深地感覺到網(wǎng)絡(luò)層跟業(yè)務(wù)對(duì)接部分的設(shè)計(jì)有多么重要续室,因此我對(duì)它做的最大改變就是針對(duì)網(wǎng)絡(luò)層跟業(yè)務(wù)對(duì)接部分的改變栋烤。網(wǎng)絡(luò)層跟業(yè)務(wù)層對(duì)接部分設(shè)計(jì)的好壞,會(huì)直接影響到業(yè)務(wù)工程師實(shí)現(xiàn)功能時(shí)的心情挺狰。

在正式開始講設(shè)計(jì)之前明郭,我們要先討論幾個(gè)問題:

  1. 使用哪種交互模式來跟業(yè)務(wù)層做對(duì)接?
  2. 是否有必要將API返回的數(shù)據(jù)封裝成對(duì)象然后再交付給業(yè)務(wù)層丰泊?
  3. 使用集約化調(diào)用方式還是離散型調(diào)用方式去調(diào)用API薯定?

這些問題討論完畢之后,我會(huì)給出一個(gè)完整的設(shè)計(jì)方案來給大家做參考瞳购,設(shè)計(jì)方案是魚话侄,討論的這些問題是漁,我什么都授了学赛,大家各取所需满葛。

使用哪種交互模式來跟業(yè)務(wù)層做對(duì)接?

這里其實(shí)有兩個(gè)問題:

  1. 以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層罢屈?
  2. 交付什么樣的數(shù)據(jù)給業(yè)務(wù)層?
以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層篇亭?

iOS開發(fā)領(lǐng)域有很多對(duì)象間數(shù)據(jù)的傳遞方式缠捌,我看到的大多數(shù)App在網(wǎng)絡(luò)層所采用的方案主要集中于這三種:Delegate,Notification译蒂,Block曼月。KVO和Target-Action我目前還沒有看到有使用的。

目前我知道邊鋒主要是采用的block柔昼,大智慧主要采用的是Notification哑芹,安居客早期以Block為主,后面改成了以Delegate為主捕透,阿里沒發(fā)現(xiàn)有通過Notification來做數(shù)據(jù)傳遞的地方(可能有)聪姿,Delegate碴萧、Block以及target-action都有,阿里iOS App網(wǎng)絡(luò)層的作者說這是為了方便業(yè)務(wù)層選擇自己合適的方法去使用末购。這里大家都是各顯神通破喻,每次我看到這部分的時(shí)候,我都喜歡問作者為什么采用這種交互方案盟榴,但很少有作者能夠說出個(gè)條條框框來曹质。

然而在我這邊,我的意見是以Delegate為主擎场,Notification為輔羽德。原因如下:

  • 盡可能減少跨層數(shù)據(jù)交流的可能,限制耦合
  • 統(tǒng)一回調(diào)方法迅办,便于調(diào)試和維護(hù)
  • 在跟業(yè)務(wù)層對(duì)接的部分只采用一種對(duì)接手段(在我這兒就是只采用delegate這一個(gè)手段)限制靈活性宅静,以此來交換應(yīng)用的可維護(hù)性

盡可能減少跨層數(shù)據(jù)交流的可能,限制耦合

什么叫跨層數(shù)據(jù)交流礼饱?就是某一層(或模塊)跟另外的與之沒有直接對(duì)接關(guān)系的層(或模塊)產(chǎn)生了數(shù)據(jù)交換坏为。為什么這種情況不好?嚴(yán)格來說應(yīng)該是大部分情況都不好镊绪,有的時(shí)候跨層數(shù)據(jù)交流確實(shí)也是一種需求匀伏。之所以說不好的地方在于,它會(huì)導(dǎo)致代碼混亂蝴韭,破壞模塊的封裝性够颠。我們?cè)谧龇謱蛹軜?gòu)的目的其中之一就在于下層對(duì)上層有一次抽象,讓上層可以不必關(guān)心下層細(xì)節(jié)而執(zhí)行自己的業(yè)務(wù)榄鉴。

所以履磨,如果下層細(xì)節(jié)被跨層暴露,一方面你很容易因此失去鄰層對(duì)這個(gè)暴露細(xì)節(jié)的保護(hù)庆尘;另一方面剃诅,你又不可能不去處理這個(gè)細(xì)節(jié),所以處理細(xì)節(jié)的相關(guān)代碼就會(huì)散落各地驶忌,最終難以維護(hù)矛辕。

說得具象一點(diǎn)就是,我們考慮這樣一種情況:A<-B<-C付魔。當(dāng)C有什么事件聊品,通過某種方式告知B,然后B執(zhí)行相應(yīng)的邏輯几苍。一旦告知方式不合理翻屈,讓A有了跨層知道C的事件的可能,你 就很難保證A層業(yè)務(wù)工程師在將來不會(huì)對(duì)這個(gè)細(xì)節(jié)作處理妻坝。一旦業(yè)務(wù)工程師在A層產(chǎn)生處理操作伸眶,有可能是補(bǔ)充邏輯惊窖,也有可能是執(zhí)行業(yè)務(wù),那么這個(gè)細(xì)節(jié)的相關(guān)處理代碼就會(huì)有一部分散落在A層赚抡。然而前者是不應(yīng)該散落在A層的爬坑,后者有可能是需求页徐。另外疲吸,因?yàn)锽層是對(duì)A層抽象的曙求,執(zhí)行補(bǔ)充邏輯的時(shí)候簸州,有可能和B層針對(duì)這個(gè)事件的處理邏輯產(chǎn)生沖突醇坝,這是我們很不希望看到的延蟹。

那么什么情況跨層數(shù)據(jù)交流會(huì)成為需求窍仰?在網(wǎng)絡(luò)層這邊将硝,信號(hào)從2G變成3G變成4G變成Wi-Fi岩四,這個(gè)是跨層數(shù)據(jù)交流的其中一個(gè)需求哭尝。不過其他的跨層數(shù)據(jù)交流需求我暫時(shí)也想不到了,哈哈剖煌,應(yīng)該也就這一個(gè)吧材鹦。


嚴(yán)格來說,使用Notification來進(jìn)行網(wǎng)絡(luò)層和業(yè)務(wù)層之間數(shù)據(jù)的交換耕姊,并不代表這一定就是跨層數(shù)據(jù)交流桶唐,但是使用Notification給跨層數(shù)據(jù)交流開了一道口子,因?yàn)镹otification的影響面不可控制茉兰,只要存在實(shí)例就存在被影響的可能尤泽。另外,這也會(huì)導(dǎo)致誰(shuí)都不能保證相關(guān)處理代碼就在唯一的那個(gè)地方规脸,進(jìn)而帶來維護(hù)災(zāi)難坯约。作為架構(gòu)師,在這里給業(yè)務(wù)工程師限制其操作的靈活性是必要的莫鸭。另外闹丐,Notification也支持一對(duì)多的情況,這也給代碼散落提供了條件被因。同時(shí)妇智,Notification所對(duì)應(yīng)的響應(yīng)方法很難在編譯層面作限制,不同的業(yè)務(wù)工程師會(huì)給他取不同的名字氏身,這也會(huì)給代碼的可維護(hù)性帶來災(zāi)難。

手機(jī)淘寶架構(gòu)組的俠武同學(xué)曾經(jīng)給我分享過一個(gè)問題惑畴,在這里我也分享給大家:曾經(jīng)有一個(gè)工程師在監(jiān)聽Notification之后蛋欣,沒有寫釋放監(jiān)聽的代碼,當(dāng)然如贷,找到這個(gè)原因又是很漫長(zhǎng)的一段故事陷虎,現(xiàn)在找到原因了到踏,然而監(jiān)聽這個(gè)Notification的對(duì)象有那么多,不知道具體是哪個(gè)Notificaiton尚猿,也不知道那個(gè)沒釋放監(jiān)聽的對(duì)象是誰(shuí)窝稿。后來折騰了很久大家都沒辦法的時(shí)候,有一個(gè)經(jīng)驗(yàn)豐富的工程師提出用hook(Method Swizzling)的方式凿掂,最終找到了那個(gè)沒釋放監(jiān)聽的對(duì)象伴榔,bug修復(fù)了。

我分享這個(gè)問題的目的并不是想強(qiáng)調(diào)Notification多么多么不好庄萎,Notification本身就是一種設(shè)計(jì)模式踪少,在屬于他的問題領(lǐng)域內(nèi),Notification是非常好的一種解決方案糠涛。但我想強(qiáng)調(diào)的是援奢,對(duì)于網(wǎng)絡(luò)層這個(gè)問題領(lǐng)域內(nèi)來看,架構(gòu)師首先一定要限制代碼的影響范圍忍捡,在能用影響范圍小的方案的時(shí)候就盡量采用這種小的方案集漾,否則將來要是有什么奇怪需求或者出了什么小問題,維護(hù)起來就非常麻煩砸脊。因此Notification這個(gè)方案不能作為首選方案具篇,只能作為備選。

那么Notification也不是完全不能使用脓规,當(dāng)需求要求跨層時(shí)栽连,我們就可以使用Notification,比如前面提到的網(wǎng)絡(luò)條件切換侨舆,而且這個(gè)需求也是需要滿足一對(duì)多的秒紧。

所以,為了符合前面所說的這些要求挨下,使用Delegate能夠很好地避免跨層訪問熔恢,同時(shí)限制了響應(yīng)代碼的形式,相比Notification而言有更好的可維護(hù)性臭笆。


然后我們順便來說說為什么盡量不要用block叙淌。

  • block很難追蹤,難以維護(hù)

我們?cè)谡{(diào)試的時(shí)候經(jīng)常會(huì)單步追蹤到某一個(gè)地方之后愁铺,發(fā)現(xiàn)尼瑪這里有個(gè)block鹰霍,如果想知道這個(gè)block里面都做了些什么事情,這時(shí)候就比較蛋疼了茵乱。

- (void)someFunctionWithBlock:(SomeBlock *)block
{
    ... ...

 -> block();  //當(dāng)你單步走到這兒的時(shí)候茂洒,要想知道block里面都做了哪些事情的話,就很麻煩瓶竭。

    ... ...
}
  • block會(huì)延長(zhǎng)相關(guān)對(duì)象的生命周期

block會(huì)給內(nèi)部所有的對(duì)象引用計(jì)數(shù)加一督勺,這一方面會(huì)帶來潛在的retain cycle渠羞,不過我們可以通過Weak Self的手段解決。另一方面比較重要就是智哀,它會(huì)延長(zhǎng)對(duì)象的生命周期次询。

在網(wǎng)絡(luò)回調(diào)中使用block,是block導(dǎo)致對(duì)象生命周期被延長(zhǎng)的其中一個(gè)場(chǎng)合瓷叫,當(dāng)ViewController從window中卸下時(shí)屯吊,如果尚有請(qǐng)求帶著block在外面飛,然后block里面引用了ViewController(這種場(chǎng)合非常常見)赞辩,那么ViewController是不能被及時(shí)回收的雌芽,即便你已經(jīng)取消了請(qǐng)求,那也還是必須得等到請(qǐng)求著陸之后才能被回收辨嗽。

然而使用delegate就不會(huì)有這樣的問題世落,delegate是弱引用,哪怕請(qǐng)求仍然在外面飛糟需,屉佳,ViewController還是能夠及時(shí)被回收的,回收之后指針自動(dòng)被置為了nil洲押,無(wú)傷大雅武花。

  • block在離散型場(chǎng)景下不符合使用的規(guī)范

block和delegate乍看上去在作用上是很相似,但是關(guān)于它們的選型有一條嚴(yán)格的規(guī)范:當(dāng)回調(diào)之后要做的任務(wù)在每次回調(diào)時(shí)都是一致的情況下杈帐,選擇delegate体箕,在回調(diào)之后要做的任務(wù)在每次回調(diào)時(shí)無(wú)法保證一致,選擇block挑童。在離散型調(diào)用的場(chǎng)景下累铅,每一次回調(diào)都是能夠保證任務(wù)一致的,因此適用delegate站叼。這也是蘋果原生的網(wǎng)絡(luò)調(diào)用也采用delegate的原因娃兽,因?yàn)樘O果也是基于離散模型去設(shè)計(jì)網(wǎng)絡(luò)調(diào)用的,而且本文即將要介紹的網(wǎng)絡(luò)層架構(gòu)也是基于離散型調(diào)用的思路去設(shè)計(jì)的尽楔。

在集約型調(diào)用的場(chǎng)景下投储,使用block是合理的,因?yàn)槊看握?qǐng)求的類型都不一樣阔馋,那么自然回調(diào)要做的任務(wù)也都會(huì)不一樣玛荞,因此只能采用block。AFNetworking就是屬于集約型調(diào)用呕寝,因此它采用了block來做回調(diào)冲泥。

就我所知,目前大部分公司的App網(wǎng)絡(luò)層都是集約型調(diào)用,因此廣泛采取了block回調(diào)凡恍。但是在App的網(wǎng)絡(luò)層架構(gòu)設(shè)計(jì)中直接采用集約型調(diào)用來為業(yè)務(wù)服務(wù)的思路是有問題的,因此在遷移到離散型調(diào)用時(shí)怔球,一定要注意這一點(diǎn)嚼酝,記得遷回delegate回調(diào)。關(guān)于離散型和集約型調(diào)用的介紹和如何選型竟坛,我在后面的集約型API調(diào)用方式和離散型API調(diào)用方式的選擇闽巩?小節(jié)中有詳細(xì)的介紹。

所以平時(shí)盡量不要濫用block担汤,尤其是在網(wǎng)絡(luò)層這里涎跨。


統(tǒng)一回調(diào)方法,便于調(diào)試和維護(hù)

前面講的是跨層問題崭歧,區(qū)分了Delegate和Notification隅很,順帶談了一下Block。然后現(xiàn)在談到的這個(gè)情況率碾,就是另一個(gè)采用Block方案不是很合適的情況叔营。首先,Block本身無(wú)好壞對(duì)錯(cuò)之分所宰,只有合適不合適绒尊。在這一節(jié)要講的情況里,Block無(wú)法做到回調(diào)方法的統(tǒng)一仔粥,調(diào)試和維護(hù)的時(shí)候也很難在調(diào)用棧上顯示出來婴谱,找的時(shí)候會(huì)很蛋疼。

在網(wǎng)絡(luò)請(qǐng)求和網(wǎng)絡(luò)層接受請(qǐng)求的地方時(shí)躯泰,使用Block沒問題谭羔。但是在獲得數(shù)據(jù)交給業(yè)務(wù)方時(shí),最好還是通過Delegate去通知到業(yè)務(wù)方斟冕。因?yàn)锽lock所包含的回調(diào)代碼跟調(diào)用邏輯放在同一個(gè)地方口糕,會(huì)導(dǎo)致那部分代碼變得很長(zhǎng),因?yàn)檫@里面包括了調(diào)用前和調(diào)用后的邏輯磕蛇。從另一個(gè)角度說景描,這在一定程度上違背了single function,single task的原則秀撇,在需要調(diào)用API的地方超棺,就只要寫API調(diào)用相關(guān)的代碼,在回調(diào)的地方呵燕,寫回調(diào)的代碼棠绘。

然后我看到大部分App里,當(dāng)業(yè)務(wù)工程師寫代碼寫到這邊的時(shí)候,也意識(shí)到了這個(gè)問題氧苍。因此他們會(huì)在block里面寫個(gè)一句話的方法接收參數(shù)夜矗,然后做轉(zhuǎn)發(fā),然后就可以把這個(gè)方法放在其他地方了让虐,繞過了Block的回調(diào)著陸點(diǎn)不統(tǒng)一的情況紊撕。比如這樣:

[API callApiWithParam:param successed:^(Response *response){
  [self successedWithResponse:response];
} failed:^(Request *request, NSError *error){
  [self failedWithRequest:request error:error];
}];

這實(shí)質(zhì)上跟使用Delegate的手段沒有什么區(qū)別,只是繞了一下赡突,不過還是沒有解決統(tǒng)一回調(diào)方法的問題对扶,因?yàn)閎lock里面寫的方法名字可能在不同的ViewController對(duì)象中都會(huì)不一樣,畢竟業(yè)務(wù)工程師也是很多人惭缰,各人有各人的想法浪南。所以架構(gòu)師在這邊不要貪圖方便,還是使用delegate的手段吧漱受,業(yè)務(wù)工程師那邊就能不用那么繞了络凿。Block是目前大部分第三方網(wǎng)絡(luò)庫(kù)都采用的方式,因?yàn)樵诎l(fā)送請(qǐng)求的那一部分拜效,使用Block能夠比較簡(jiǎn)潔喷众,因此在請(qǐng)求那一層是沒有問題的,只是在交換數(shù)據(jù)之后紧憾,還是轉(zhuǎn)變成delegate比較好到千,比如AFNetworking里面:

    [AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
        if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
            [self.delegate successedWithResponse:response];
        }
    } failed:^(Request *request, NSError *error){
        if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
            [self failedWithRequest:request error:error];
        }
    }];

這樣在業(yè)務(wù)方這邊回調(diào)函數(shù)就能夠比較統(tǒng)一,便于維護(hù)赴穗。


綜上憔四,對(duì)于以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層?這個(gè)問題的回答是這樣:

盡可能通過Delegate的回調(diào)方式交付數(shù)據(jù)般眉,這樣可以避免不必要的跨層訪問了赵。當(dāng)出現(xiàn)跨層訪問的需求時(shí)(比如信號(hào)類型切換),通過Notification的方式交付數(shù)據(jù)甸赃。正常情況下應(yīng)該是避免使用Block的柿汛。

交付什么樣的數(shù)據(jù)給業(yè)務(wù)層?

我見過非常多的App的網(wǎng)絡(luò)層在拿到JSON數(shù)據(jù)之后埠对,會(huì)將數(shù)據(jù)轉(zhuǎn)變成對(duì)應(yīng)的對(duì)象原型络断。注意,我這里指的不是NSDictionary项玛,而是類似Item這樣的對(duì)象貌笨。這種做法是能夠提高后續(xù)操作代碼的可讀性的。在比較直覺的思路里面襟沮,是需要這部分轉(zhuǎn)化過程的锥惋,但這部分轉(zhuǎn)化過程的成本是很大的昌腰,主要成本在于:

  1. 數(shù)組內(nèi)容的轉(zhuǎn)化成本較高:數(shù)組里面每項(xiàng)都要轉(zhuǎn)化成Item對(duì)象,如果Item對(duì)象中還有類似數(shù)組膀跌,就很頭疼遭商。
  2. 轉(zhuǎn)化之后的數(shù)據(jù)在大部分情況是不能直接被展示的,為了能夠被展示捅伤,還需要第二次轉(zhuǎn)化株婴。
  3. 只有在API返回的數(shù)據(jù)高度標(biāo)準(zhǔn)化時(shí),這些對(duì)象原型(Item)的可復(fù)用程度才高暑认,否則容易出現(xiàn)類型爆炸,提高維護(hù)成本大审。
  4. 調(diào)試時(shí)通過對(duì)象原型查看數(shù)據(jù)內(nèi)容不如直接通過NSDictionary/NSArray直觀蘸际。
  5. 同一API的數(shù)據(jù)被不同View展示時(shí),難以控制數(shù)據(jù)轉(zhuǎn)化的代碼徒扶,它們有可能會(huì)散落在任何需要的地方粮彤。

其實(shí)我們的理想情況是希望API的數(shù)據(jù)下發(fā)之后就能夠直接被View所展示。首先要說的是姜骡,這種情況非常少导坟。另外,這種做法使得View和API聯(lián)系緊密圈澈,也是我們不希望發(fā)生的惫周。

在設(shè)計(jì)安居客的網(wǎng)絡(luò)層數(shù)據(jù)交付這部分時(shí),我添加了reformer(名字而已康栈,叫什么都好)這個(gè)對(duì)象用于封裝數(shù)據(jù)轉(zhuǎn)化的邏輯递递,這個(gè)對(duì)象是一個(gè)獨(dú)立對(duì)象,事實(shí)上啥么,它是作為Adaptor模式存在的登舞。我們可以這么理解:想象一下我們洗澡時(shí)候使用的蓮蓬頭,水管里出來的水是API下發(fā)的原始數(shù)據(jù)悬荣。reformer就是蓮蓬頭上的不同水流擋板菠秒,需要什么模式,就撥到什么模式氯迂。

在實(shí)際使用時(shí)践叠,代碼觀感是這樣的:

先定義一個(gè)protocol:

@protocol ReformerProtocol <NSObject>
- (NSDictionary)reformDataWithManager:(APIManager *)manager;
@end

在Controller里是這樣:

@property (nonatomic, strong) id<ReformerProtocol> XXXReformer;
@property (nonatomic, strong) id<ReformerProtocol> YYYReformer;

#pragma mark - APIManagerDelegate
- (void)apiManagerDidSuccess:(APIManager *)manager
{
    NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
    [self.XXXView configWithData:reformedXXXData];

    NSDictionary *reformedYYYData = [manager fetchDataWithReformer:self.YYYReformer];
    [self.YYYView configWithData:reformedYYYData];
}

在APIManager里面,fetchDataWithReformer是這樣:
- (NSDictionary)fetchDataWithReformer:(id<ReformerProtocol>)reformer
{
    if (reformer == nil) {
        return self.rawData;
    } else {
        return [reformer reformDataWithManager:self];
    }
}
  • 要點(diǎn)1:reformer是一個(gè)符合ReformerProtocol的對(duì)象囚戚,它提供了通用的方法供Manager使用酵熙。

  • 要點(diǎn)2:API的原始數(shù)據(jù)(JSON對(duì)象)由Manager實(shí)例保管,reformer方法里面取Manager的原始數(shù)據(jù)(manager.rawData)做轉(zhuǎn)換驰坊,然后交付出去匾二。蓮蓬頭的水管部分是Manager,負(fù)責(zé)提供原始水流(數(shù)據(jù)流),reformer就是不同的模式察藐,換什么reformer就能出來什么水流皮璧。

  • 要點(diǎn)3:例子中舉的場(chǎng)景是一個(gè)API數(shù)據(jù)被多個(gè)View使用的情況,體現(xiàn)了reformer的一個(gè)特點(diǎn):可以根據(jù)需要改變同一數(shù)據(jù)來源的展示方式分飞。比如API數(shù)據(jù)展示的是“附近的小區(qū)”悴务,那么這個(gè)數(shù)據(jù)可以被列表(XXXView)和地圖(YYYView)共用,不同的view使用的數(shù)據(jù)的轉(zhuǎn)化方式不一樣譬猫,這就通過不同的reformer解決了讯檐。

  • 要點(diǎn)4:在一個(gè)view用來同一展示不同API數(shù)據(jù)的情況,reformer是絕佳利器染服。比如安居客的列表view的數(shù)據(jù)來源可能有三個(gè):二手房列表API别洪,租房列表API,新房列表API柳刮。這些API返回來的數(shù)據(jù)的value可能一致挖垛,但是key都是不一致的。這時(shí)候就可以通過同一個(gè)reformer來做數(shù)據(jù)的標(biāo)準(zhǔn)化輸出秉颗,這樣就使得view代碼復(fù)用成為可能痢毒。這體現(xiàn)了reformer另外一個(gè)特點(diǎn):同一個(gè)reformer出來的數(shù)據(jù)是高度標(biāo)準(zhǔn)化的。形象點(diǎn)說就是:只要蓮蓬頭不換蚕甥,哪怕水管的水變成海水或者污水了哪替,也依舊能夠輸出符合洗澡要求的淡水水流。舉個(gè)例子:

- (void)apiManagerDidSuccess:(APIManager *)manager
{
    // 這個(gè)回調(diào)方法有可能是來自二手房列表APIManager的回調(diào)梢灭,也有可能是租房夷家,也有可能是新房。但是在Controller層面我們不需要對(duì)它做額外區(qū)分敏释,只要是同一個(gè)reformer出來的數(shù)據(jù)库快,我們就能保證是一定能被self.XXXView使用的。這樣的保證由reformer的實(shí)現(xiàn)者來提供钥顽。
    NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
    [self.XXXView configWithData:reformedXXXData];
}
  • 要點(diǎn)5:有沒有發(fā)現(xiàn)义屏,使用reformer之后,Controller的代碼簡(jiǎn)潔了很多蜂大?而且闽铐,數(shù)據(jù)原型在這種情況下就沒有必要存在了,隨之而來的成本也就被我們繞過了奶浦。

reformer本質(zhì)上就是一個(gè)符合某個(gè)protocol的對(duì)象兄墅,在controller需要從api manager中獲得數(shù)據(jù)的時(shí)候,順便把reformer傳進(jìn)去澳叉,于是就能獲得經(jīng)過reformer重新洗過的數(shù)據(jù)隙咸,然后就可以直接使用了沐悦。

更抽象地說,reformer其實(shí)是對(duì)數(shù)據(jù)轉(zhuǎn)化邏輯的一個(gè)封裝五督。在controller從manager中取數(shù)據(jù)之后藏否,并且把數(shù)據(jù)交給view之前,這期間或多或少都是要做一次數(shù)據(jù)轉(zhuǎn)化的充包,有的時(shí)候不同的view副签,對(duì)應(yīng)的轉(zhuǎn)化邏輯還不一樣,但是展示的數(shù)據(jù)是一樣的基矮。而且往往這一部分代碼都非常復(fù)雜淆储,且跟業(yè)務(wù)強(qiáng)相關(guān),直接上代碼家浇,將來就會(huì)很難維護(hù)遏考。所以我們可以考慮采用不同的reformer封裝不同的轉(zhuǎn)化邏輯,然后讓controller根據(jù)需要選擇一個(gè)合適的reformer裝上蓝谨,就像洗澡的蓮蓬頭,需要什么樣的水流(數(shù)據(jù)的表現(xiàn)形式)就換什么樣的頭青团,然而水(數(shù)據(jù))都是一樣的譬巫。這種做法能夠大大提高代碼的可維護(hù)性,以及減少ViewController的體積督笆。

總結(jié)一下芦昔,reformer事實(shí)上是把轉(zhuǎn)化的代碼封裝之后再?gòu)闹黧w業(yè)務(wù)中拆分了出來,拆分出來之后不光降低了原有業(yè)務(wù)的復(fù)雜度娃肿,更重要的是咕缎,它提高了數(shù)據(jù)交付的靈活性。另外料扰,由于Controller負(fù)責(zé)調(diào)度Manager和View凭豪,因此它是知道Manager和View之間的關(guān)系的,Controller知道了這個(gè)關(guān)系之后晒杈,就有了充要條件來為不同的View選擇不同的Reformer嫂伞,并用這個(gè)Reformer去改造Mananger的數(shù)據(jù),然后ViewController獲得了經(jīng)過reformer處理過的數(shù)據(jù)之后拯钻,就可以直接交付給view去使用帖努。Controller因此得到瘦身,負(fù)責(zé)業(yè)務(wù)數(shù)據(jù)轉(zhuǎn)化的這部分代碼也不用寫在Controller里面粪般,提高了可維護(hù)性拼余。


所以reformer機(jī)制能夠帶來以下好處:

  • 好處1:繞開了API數(shù)據(jù)原型的轉(zhuǎn)換,避免了相關(guān)成本亩歹。

  • 好處2:在處理單View對(duì)多API匙监,以及在單API對(duì)多View的情況時(shí)凡橱,reformer提供了非常優(yōu)雅的手段來響應(yīng)這種需求,隔離了轉(zhuǎn)化邏輯和主體業(yè)務(wù)邏輯舅柜,避免了維護(hù)災(zāi)難梭纹。

  • 好處3:轉(zhuǎn)化邏輯集中,且將轉(zhuǎn)化次數(shù)轉(zhuǎn)為只有一次致份。使用數(shù)據(jù)原型的轉(zhuǎn)化邏輯至少有兩次变抽,第一次是把JSON映射成對(duì)應(yīng)的原型,第二次是把原型轉(zhuǎn)變成能被View處理的數(shù)據(jù)氮块。reformer一步到位绍载。另外,轉(zhuǎn)化邏輯在reformer里面滔蝉,將來如果API數(shù)據(jù)有變击儡,就只要去找到對(duì)應(yīng)reformer然后改掉就好了。

  • 好處4:Controller因此可以省去非常多的代碼蝠引,降低了代碼復(fù)雜度阳谍,同時(shí)提高了靈活性,任何時(shí)候切換reformer而不必切換業(yè)務(wù)邏輯就可以應(yīng)對(duì)不同View對(duì)數(shù)據(jù)的需要螃概。

  • 好處5:業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)有了適當(dāng)?shù)母綦x矫夯。這么做的話,將來如果業(yè)務(wù)邏輯有修改吊洼,換一個(gè)reformer就好了训貌。如果其他業(yè)務(wù)也有相同的數(shù)據(jù)轉(zhuǎn)化邏輯,其他業(yè)務(wù)直接拿這個(gè)reformer就可以用了冒窍,不用重寫递沪。另外,如果controller有修改(比如UI交互方式改變)综液,可以放心換controller款慨,完全不用擔(dān)心業(yè)務(wù)數(shù)據(jù)的處理。


在不使用特定對(duì)象表征數(shù)據(jù)的情況下谬莹,如何保持?jǐn)?shù)據(jù)可讀性樱调?

不使用對(duì)象來表征數(shù)據(jù)的時(shí)候,事實(shí)上就是使用NSDictionary的時(shí)候届良。事實(shí)上笆凌,這個(gè)問題就是,如何在NSDictionary表征數(shù)據(jù)的情況下保持良好的可讀性士葫?

蘋果已經(jīng)給出了非常好的做法乞而,用固定字符串做key,比如你在接收到KeyBoardWillShow的Notification時(shí)慢显,帶了一個(gè)userInfo爪模,他的key就都是類似UIKeyboardAnimationCurveUserInfoKey這樣的欠啤,所以我們采用這樣的方案來維持可讀性。下面我舉一個(gè)例子:

PropertyListReformerKeys.h

extern NSString * const kPropertyListDataKeyID;
extern NSString * const kPropertyListDataKeyName;
extern NSString * const kPropertyListDataKeyTitle;
extern NSString * const kPropertyListDataKeyImage;
PropertyListReformer.h

#import "PropertyListReformerKeys.h"

... ...
PropertyListReformer.m

NSString * const kPropertyListDataKeyID = @"kPropertyListDataKeyID";
NSString * const kPropertyListDataKeyName = @"kPropertyListDataKeyName";
NSString * const kPropertyListDataKeyTitle = @"kPropertyListDataKeyTitle";
NSString * const kPropertyListDataKeyImage = @"kPropertyListDataKeyImage";

- (NSDictionary *)reformData:(NSDictionary *)originData fromManager:(APIManager *)manager
{
    ... ...
    ... ...

    NSDictionary *resultData = nil;

    if ([manager isKindOfClass:[ZuFangListAPIManager class]]) {
        resultData = @{
            kPropertyListDataKeyID:originData[@"id"],
            kPropertyListDataKeyName:originData[@"name"],
            kPropertyListDataKeyTitle:originData[@"title"],
            kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"imageUrl"]]
        };
    }

    if ([manager isKindOfClass:[XinFangListAPIManager class]]) {
        resultData = @{
            kPropertyListDataKeyID:originData[@"xinfang_id"],
            kPropertyListDataKeyName:originData[@"xinfang_name"],
            kPropertyListDataKeyTitle:originData[@"xinfang_title"],
            kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"xinfang_imageUrl"]]
        };
    }

    if ([manager isKindOfClass:[ErShouFangListAPIManager class]]) {
        resultData = @{
            kPropertyListDataKeyID:originData[@"esf_id"],
            kPropertyListDataKeyName:originData[@"esf_name"],
            kPropertyListDataKeyTitle:originData[@"esf_title"],
            kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"esf_imageUrl"]]
        };
    }

    return resultData;
}
PropertListCell.m

#import "PropertyListReformerKeys.h"

- (void)configWithData:(NSDictionary *)data
{
    self.imageView.image = data[kPropertyListDataKeyImage];
    self.idLabel.text = data[kPropertyListDataKeyID];
    self.nameLabel.text = data[kPropertyListDataKeyName];
    self.titleLabel.text = data[kPropertyListDataKeyTitle];
}

這一大段代碼看下來屋灌,我如果不說一下要點(diǎn)洁段,那基本上就白寫了哈:

我們先看一下結(jié)構(gòu):


使用Const字符串來表征Key,字符串的定義跟著reformer的實(shí)現(xiàn)文件走共郭,字符串的extern聲明放在獨(dú)立的頭文件內(nèi)祠丝。

這樣reformer生成的數(shù)據(jù)的key都使用Const字符串來表示,然后每次別的地方需要使用相關(guān)數(shù)據(jù)的時(shí)候除嘹,把PropertyListReformerKeys.h這個(gè)頭文件import進(jìn)去就好了写半。

另外要注意的一點(diǎn)是,如果一個(gè)OriginData可能會(huì)被多個(gè)Reformer去處理的話尉咕,Key的命名規(guī)范需要能夠表征出其對(duì)應(yīng)的reformer名字叠蝇。如果reformer是PropertyListReformer,那么Key的名字就是PropertyListKeyXXXX年缎。

這么做的好處就是悔捶,將來遷移的時(shí)候相當(dāng)方便,只要扔頭文件就可以了单芜,只扔頭文件是不會(huì)導(dǎo)致拔出蘿卜帶出泥的情況的炎功。而且也避免了自定義對(duì)象帶來的額外代碼體積。


另外缓溅,關(guān)于交付的NSDictionary,其實(shí)具體還是看view的需求赁温,reformer的設(shè)計(jì)初衷是:通過reformer轉(zhuǎn)化出來的可以直接是View坛怪,或者是view直接可以使用的對(duì)象(包括NSDictionary)。比如地圖標(biāo)點(diǎn)列表API的數(shù)據(jù)股囊,通過reformer轉(zhuǎn)化之后就可以直接變成MKAnnotation袜匿,然后MKMapView就可以直接使用了。這里說的只是當(dāng)你的需求是交付NSDictionary時(shí)稚疹,如何保證可讀性的情況居灯,再?gòu)?qiáng)調(diào)一下哈,reformer交付的是view直接可以使用的對(duì)象内狗,交付出去的可以是NSDictionary怪嫌,也可以是UIView,跟DataSource結(jié)合之后交付的甚至可以是UITableViewCell/UICollectionViewCell柳沙。不要被NSDictionary或所謂的轉(zhuǎn)化成model再交付的思想局限岩灭。


綜上,我對(duì)交付什么樣的數(shù)據(jù)給業(yè)務(wù)層赂鲤?這個(gè)問題的回答就是這樣:

對(duì)于業(yè)務(wù)層而言噪径,由Controller根據(jù)View和APIManager之間的關(guān)系柱恤,選擇合適的reformer將View可以直接使用的數(shù)據(jù)(甚至reformer可以用來直接生成view)轉(zhuǎn)化好之后交付給View。對(duì)于網(wǎng)絡(luò)層而言找爱,只需要保持住原始數(shù)據(jù)即可梗顺,不需要主動(dòng)轉(zhuǎn)化成數(shù)據(jù)原型。然后數(shù)據(jù)采用NSDictionary加Const字符串key來表征车摄,避免了使用對(duì)象來表征帶來的遷移困難寺谤,同時(shí)不失去可讀性。


集約型API調(diào)用方式和離散型API調(diào)用方式的選擇练般?

集約型API調(diào)用其實(shí)就是所有API的調(diào)用只有一個(gè)類矗漾,然后這個(gè)類接收API名字,API參數(shù)薄料,以及回調(diào)著陸點(diǎn)(可以是target-action敞贡,或者block,或者delegate等各種模式的著陸點(diǎn))作為參數(shù)摄职。然后執(zhí)行類似startRequest這樣的方法誊役,它就會(huì)去根據(jù)這些參數(shù)起飛去調(diào)用API了,然后獲得API數(shù)據(jù)之后再根據(jù)指定的著陸點(diǎn)去著陸谷市。比如這樣:

集約型API調(diào)用方式:

[APIRequest startRequestWithApiName:@"itemList.v1" params:params success:@selector(success:) fail:@selector(fail:) target:self];

離散型API調(diào)用是這樣的院水,一個(gè)API對(duì)應(yīng)于一個(gè)APIManager,然后這個(gè)APIManager只需要提供參數(shù)就能起飛配深,API名字于购、著陸方式都已經(jīng)集成入APIManager中。比如這樣:

離散型API調(diào)用方式:

@property (nonatomic, strong) ItemListAPIManager *itemListAPIManager;

// getter
- (ItemListAPIManager *)itemListAPIManager
{
    if (_itemListAPIManager == nil) {
        _itemListAPIManager = [[ItemListAPIManager alloc] init];
        _itemListAPIManager.delegate = self;
    }

    return _itemListAPIManager;
}

// 使用的時(shí)候就這么寫:
[self.itemListAPIManager loadDataWithParams:params];

集約型API調(diào)用和離散型API調(diào)用這兩者實(shí)現(xiàn)方案不是互斥的创泄,單看下層艺玲,大家都是集約型。因?yàn)榘l(fā)起一個(gè)API請(qǐng)求之后鞠抑,除去業(yè)務(wù)相關(guān)的部分(比如參數(shù)和API名字等)饭聚,剩下的都是要統(tǒng)一處理的:加密,URL拼接搁拙,API請(qǐng)求的起飛和著陸秒梳,這些處理如果不用集約化的方式來實(shí)現(xiàn),作者非癲即癡箕速。然而對(duì)于整個(gè)網(wǎng)絡(luò)層來說酪碘,尤其是業(yè)務(wù)方使用的那部分,我傾向于提供離散型的API調(diào)用方式盐茎,并不建議在業(yè)務(wù)層的代碼直接使用集約型的API調(diào)用方式婆跑。原因如下:

  • 原因1:當(dāng)前請(qǐng)求正在外面飛著的時(shí)候,根據(jù)不同的業(yè)務(wù)需求存在兩種不同的請(qǐng)求起飛策略:一個(gè)是取消新發(fā)起的請(qǐng)求庭呜,等待外面飛著的請(qǐng)求著陸滑进。另一個(gè)是取消外面飛著的請(qǐng)求犀忱,讓新發(fā)起的請(qǐng)求起飛。集約化的API調(diào)用方式如果要滿足這樣的需求扶关,那么每次要調(diào)用的時(shí)候都要多寫一部分判斷和取消的代碼阴汇,手段就做不到很干凈。

前者的業(yè)務(wù)場(chǎng)景舉個(gè)例子就是刷新頁(yè)面的請(qǐng)求节槐,刷新詳情搀庶,刷新列表等。后者的業(yè)務(wù)場(chǎng)景舉個(gè)例子是列表多維度篩選铜异,比如你先篩選了商品類型哥倔,然后篩選了價(jià)格區(qū)間。當(dāng)然揍庄,后者的情況不一定每次篩選都要調(diào)用API咆蒿,我們先假設(shè)這種篩選每次都必須要通過調(diào)用API才能獲得數(shù)據(jù)。

如果是離散型的API調(diào)用蚂子,在編寫不同的APIManager時(shí)候就可以針對(duì)不同的API設(shè)置不同的起飛策略沃测,在實(shí)際使用的時(shí)候,就可以不必關(guān)心起飛策略了食茎,因?yàn)锳PIMananger里面已經(jīng)寫好了蒂破。

  • 原因2:便于針對(duì)某個(gè)API請(qǐng)求來進(jìn)行AOP。在集約型的API調(diào)用方式下别渔,如果要針對(duì)某個(gè)API請(qǐng)求的起飛和著陸過程進(jìn)行AOP附迷,這代碼得寫成什么樣。哎媚。喇伯。噢,尼瑪這畫面太美別說看了抄伍,我都不敢想。

  • 原因3:當(dāng)API請(qǐng)求的著陸點(diǎn)消失時(shí)管宵,離散型的API調(diào)用方式能夠更加透明地處理這種情況截珍。

當(dāng)一個(gè)頁(yè)面的請(qǐng)求正在天上飛的時(shí)候,用戶等了好久不耐煩了箩朴,小手點(diǎn)了個(gè)back岗喉,然后ViewController被pop被回收。此時(shí)請(qǐng)求的著陸點(diǎn)就沒了炸庞。這是很危險(xiǎn)的情況钱床,著陸點(diǎn)要是沒了,就很容易crash的埠居。一般來說處理這個(gè)情況都是在dealloc的時(shí)候取消當(dāng)前頁(yè)面所有的請(qǐng)求查牌。如果是集約型的API調(diào)用事期,這個(gè)代碼就要寫到ViewController的dealloc里面,但如果是離散型的API調(diào)用纸颜,這個(gè)代碼寫到APIManager里面就可以了兽泣,然后隨著ViewController的回收進(jìn)程,APIManager也會(huì)被跟著回收胁孙,這部分代碼就得到了調(diào)用的機(jī)會(huì)唠倦。這樣業(yè)務(wù)方在使用的時(shí)候就可以不必關(guān)心著陸點(diǎn)消失的情況了,從而更加關(guān)注業(yè)務(wù)涮较。

  • 原因4:離散型的API調(diào)用方式能夠最大程度地給業(yè)務(wù)方提供靈活性稠鼻,比如reformer機(jī)制就是基于離散型的API調(diào)用方式的。另外狂票,如果是針對(duì)提供翻頁(yè)機(jī)制的API候齿,APIManager就能簡(jiǎn)單地提供loadNextPage方法去加載下一頁(yè),頁(yè)碼的管理就不用業(yè)務(wù)方去管理了苫亦。還有就是毛肋,如果要針對(duì)業(yè)務(wù)請(qǐng)求參數(shù)進(jìn)行驗(yàn)證,比如用戶填寫注冊(cè)信息屋剑,在離散型的APIManager里面實(shí)現(xiàn)就會(huì)非常輕松润匙。

綜上,關(guān)于集約型的API調(diào)用和離散型的API調(diào)用唉匾,我傾向于這樣:對(duì)外提供一個(gè)BaseAPIManager來給業(yè)務(wù)方做派生孕讳,在BaseManager里面采用集約化的手段組裝請(qǐng)求,放飛請(qǐng)求巍膘,然而業(yè)務(wù)方調(diào)用API的時(shí)候厂财,則是以離散的API調(diào)用方式來調(diào)用。如果你的App只提供了集約化的方式峡懈,而沒有離散方式的通道璃饱,那么我建議你再封裝一層,便于業(yè)務(wù)方使用離散的API調(diào)用方式來放飛請(qǐng)求肪康。


怎么做APIManager的繼承荚恶?

如果要做成離散型的API調(diào)用,那么使用繼承是逃不掉的磷支。BaseAPIManager里面負(fù)責(zé)集約化的部分谒撼,外部派生的XXXAPIManager負(fù)責(zé)離散的部分,對(duì)于BaseAPIManager來說雾狈,離散的部分有一些是必要的廓潜,比如API名字等,而我們派生的目的,也是為了提供這些數(shù)據(jù)辩蛋。

我在這篇文章里面列舉了種種繼承的壞處呻畸,呼吁大家盡量不要使用繼承。但是現(xiàn)在到了不得不用繼承的時(shí)候堪澎,所以我得提醒一下大家別把繼承用壞了擂错。

在APIManager的情況下,我們最直覺的思路是BaseAPIManager提供一些空方法來給子類做重載樱蛤,比如apiMethodName這樣的函數(shù)钮呀,然而我的建議是,不要這么做昨凡。我們可以用IOP的方式來限制派生類的重載爽醋。

大概就是長(zhǎng)這樣:

BaseAPIManager的init方法里這么寫:

// 注意是weak。
@property (nonatomic, weak) id<APIManager> child;

- (instancetype)init
{
    self = [super init];
    if ([self conformsToProtocol:@protocol(APIManager)]) {
        self.child = (id<APIManager>)self;
    } else {
        // 不遵守這個(gè)protocol的就讓他crash便脊,防止派生類亂來蚂四。
        NSAssert(NO, "子類必須要實(shí)現(xiàn)APIManager這個(gè)protocol。");
    }
    return self;
}

protocol這么寫哪痰,把原本要重載的函數(shù)都定義在這個(gè)protocol里面遂赠,就不用在父類里面寫空方法了:
@protocol APIManager <NSObject>

@required
- (NSString *)apiMethodName;
...

@end

然后在父類里面如果要使用的話,就這么寫:

[self requestWithAPIName:[self.child apiMethodName] ......];

簡(jiǎn)單說就是在init的時(shí)候檢查自己是否符合預(yù)先設(shè)計(jì)的子類的protocol晌杰,這就要求所有子類必須遵守這個(gè)protocol跷睦,所有針對(duì)父類的重載、覆蓋也都以這個(gè)protocol為準(zhǔn)肋演,protocol以外的方法不允許重載抑诸、覆蓋。而在父類的代碼里爹殊,可以不必遵守這個(gè)protocol蜕乡,保持了未來維護(hù)的靈活性。

這么做的好處就是避免了父類寫空方法梗夸,同時(shí)也給子類帶上了緊箍咒:要想當(dāng)我的孩子层玲,就要遵守這些規(guī)矩,不能亂來反症。業(yè)務(wù)方在實(shí)現(xiàn)子類的時(shí)候辛块,就可以根據(jù)protocol中的方法去一一實(shí)現(xiàn),然后約定就比較好做了:不允許重載父類方法惰帽,只允許選擇實(shí)現(xiàn)或不實(shí)現(xiàn)protocol中的方法憨降。

關(guān)于這個(gè)的具體的論述在這篇文章里面有父虑,感興趣的話可以看看该酗。

網(wǎng)絡(luò)層與業(yè)務(wù)層對(duì)接部分的小總結(jié)

這一節(jié)主要是講了以下這些點(diǎn):

  1. 使用delegate來做數(shù)據(jù)對(duì)接,僅在必要時(shí)采用Notification來做跨層訪問
  2. 交付NSDictionary給業(yè)務(wù)層,使用Const字符串作為Key來保持可讀性
  3. 提供reformer機(jī)制來處理網(wǎng)絡(luò)層反饋的數(shù)據(jù)呜魄,這個(gè)機(jī)制很重要悔叽,好處極多
  4. 網(wǎng)絡(luò)層上部分使用離散型設(shè)計(jì),下部分使用集約型設(shè)計(jì)
  5. 設(shè)計(jì)合理的繼承機(jī)制爵嗅,讓派生出來的APIManager受到限制娇澎,避免混亂
  6. 應(yīng)該不止這5點(diǎn)...

網(wǎng)絡(luò)層的安全機(jī)制

判斷API的調(diào)用請(qǐng)求是來自于經(jīng)過授權(quán)的APP

使用這個(gè)機(jī)制的目的主要有兩點(diǎn):

  1. 確保API的調(diào)用者是來自你自己的APP,防止競(jìng)爭(zhēng)對(duì)手爬你的API
  2. 如果你對(duì)外提供了需要注冊(cè)才能使用的API平臺(tái)睹晒,那么你需要有這個(gè)機(jī)制來識(shí)別是否是注冊(cè)用戶調(diào)用了你的API

解決方案:設(shè)計(jì)簽名

要達(dá)到第一個(gè)目的其實(shí)很簡(jiǎn)單趟庄,服務(wù)端需要給你一個(gè)密鑰,每次調(diào)用API時(shí)伪很,你使用這個(gè)密鑰再加上API名字和API請(qǐng)求參數(shù)算一個(gè)hash出來戚啥,然后請(qǐng)求的時(shí)候帶上這個(gè)hash。服務(wù)端收到請(qǐng)求之后锉试,按照同樣的密鑰同樣的算法也算一個(gè)hash出來猫十,然后跟請(qǐng)求帶來的hash做一個(gè)比較,如果一致呆盖,那么就表示這個(gè)API的調(diào)用者確實(shí)是你的APP拖云。為了不讓別人也獲取到這個(gè)密鑰,你最好不要把這個(gè)密鑰存儲(chǔ)在本地应又,直接寫死在代碼里面就好了宙项。另外適當(dāng)增加一下求Hash的算法的復(fù)雜度,那就是各種Hash算法(比如MD5)加點(diǎn)鹽丁频,再回爐跑一次Hash啥的杉允。這樣就能解決第一個(gè)目的了:確保你的API是來自于你自己的App。

一般情況下大部分公司不會(huì)出現(xiàn)需要滿足第二種情況的需求席里,除非公司開發(fā)了自己的API平臺(tái)給第三方使用叔磷。這個(gè)需求跟上面的需求有一點(diǎn)不同:符合授權(quán)的API請(qǐng)求者不只是一個(gè)。所以在這種情況下奖磁,需要的安全機(jī)制會(huì)更加復(fù)雜一點(diǎn)改基。

這里有一個(gè)較容易實(shí)現(xiàn)的方案:客戶端調(diào)用API的時(shí)候,把自己的密鑰通過一個(gè)可逆的加密算法加密后連著請(qǐng)求和加密之后的Hash一起送上去咖为。當(dāng)然秕狰,這個(gè)可逆的加密算法肯定是放在在調(diào)用API的SDK里面,編譯好的躁染。然后服務(wù)端拿到加密后的密鑰和加密的Hash之后鸣哀,解碼得到原始密鑰,然后再用它去算Hash吞彤,最后再進(jìn)行比對(duì)我衬。

保證傳輸數(shù)據(jù)的安全

使用這個(gè)機(jī)制的主要目的有兩點(diǎn):

  1. 防止中間人攻擊叹放,比如說運(yùn)營(yíng)商很喜歡往用戶的Http請(qǐng)求里面塞廣告...
  2. SPDY依賴于HTTPS,而且是未來HTTP/2的基礎(chǔ)挠羔,他們能夠提高你APP在網(wǎng)絡(luò)層整體的性能井仰。

解決方案:HTTPS

目前使用HTTPS的主要目的在于防止運(yùn)營(yíng)商往你的Response Data里面加廣告啥的(中間人攻擊),面對(duì)的威脅范圍更廣破加。從2011年開始俱恶,國(guó)外業(yè)界就已經(jīng)提倡所有的請(qǐng)求(不光是API,還有網(wǎng)站)都走HTTPS范舀,國(guó)內(nèi)差不多晚了兩年(2013年左右)才開始提倡這事合是,天貓是這兩個(gè)月才開始做HTTPS的全APP遷移。

關(guān)于速度锭环,HTTPS肯定是比HTTP慢的端仰,畢竟多了一次握手,但掛上SPDY之后田藐,有了鏈接復(fù)用荔烧,這方面的性能就有了較大提升。這里的性能提升并不是說一個(gè)請(qǐng)求原來要500ms能完成汽久,然后現(xiàn)在只要300ms鹤竭,這是不對(duì)的。所謂整體性能是基于大量請(qǐng)求去討論的:同樣的請(qǐng)求量(假設(shè)100個(gè))在短期發(fā)生時(shí)景醇,掛上SPDY之后完成這些任務(wù)所要花的時(shí)間比不用SPDY要少臀稚。SPDY還有Header壓縮的功能,不過因?yàn)橐粋€(gè)API請(qǐng)求本身已經(jīng)比較小了三痰,壓縮數(shù)據(jù)量所帶來的性能提升不會(huì)特別明顯吧寺,所以就單個(gè)請(qǐng)求來看,性能的提升是比較小的散劫。不過這是下一節(jié)要討論的事兒了稚机,這兒只是順帶說一下。

安全機(jī)制小總結(jié)

這一節(jié)說了兩種安全機(jī)制获搏,一般來說第一種是標(biāo)配赖条,第二種屬于可選配置。不過隨著我國(guó)互聯(lián)網(wǎng)基礎(chǔ)設(shè)施的完善常熙,移動(dòng)設(shè)備性能的提高纬乍,以及優(yōu)化技術(shù)的提高,第二種配置的缺點(diǎn)(速度慢)正在越來越微不足道裸卫,因此HTTPS也會(huì)成為不久之后的未來App的網(wǎng)絡(luò)層安全機(jī)制標(biāo)配仿贬。各位架構(gòu)師們,如果你的App還沒有掛HTTPS墓贿,現(xiàn)在就已經(jīng)可以開始著手這件事情了茧泪。

網(wǎng)絡(luò)層的優(yōu)化方案

網(wǎng)絡(luò)層的優(yōu)化手段主要從以下三方面考慮:

  1. 針對(duì)鏈接建立環(huán)節(jié)的優(yōu)化
  2. 針對(duì)鏈接傳輸數(shù)據(jù)量的優(yōu)化
  3. 針對(duì)鏈接復(fù)用的優(yōu)化

這三方面是所有優(yōu)化手段的內(nèi)容退个,各種五花八門的優(yōu)化手段基本上都不會(huì)逃脫這三方面,下面我就會(huì)分別針對(duì)這三方面講一下各自對(duì)應(yīng)的優(yōu)化手段调炬。

1. 針對(duì)鏈接建立環(huán)節(jié)的優(yōu)化

在API發(fā)起請(qǐng)求建立鏈接的環(huán)節(jié),大致會(huì)分這些步驟:

  1. 發(fā)起請(qǐng)求
  2. DNS域名解析得到IP
  3. 根據(jù)IP進(jìn)行三次握手(HTTPS四次握手)舱馅,鏈接建立成功

其實(shí)第三步的優(yōu)化手段跟第二步的優(yōu)化手段是一致的缰泡,我會(huì)在講第二步的時(shí)候一起講掉。

1.1 針對(duì)發(fā)起請(qǐng)求的優(yōu)化手段

其實(shí)要解決的問題就是網(wǎng)絡(luò)層該不該為此API調(diào)用發(fā)起請(qǐng)求代嗤。

  • 1.1.1 使用緩存手段減少請(qǐng)求的發(fā)起次數(shù)

對(duì)于大部分API調(diào)用請(qǐng)求來說棘钞,有些API請(qǐng)求所帶來的數(shù)據(jù)的時(shí)效性是比較長(zhǎng)的,比如商品詳情干毅,比如App皮膚等宜猜。那么我們就可以針對(duì)這些數(shù)據(jù)做本地緩存,這樣下次請(qǐng)求這些數(shù)據(jù)的時(shí)候就可以不必再發(fā)起新的請(qǐng)求硝逢。

一般是把API名字和參數(shù)拼成一個(gè)字符串然后取MD5作為key姨拥,存儲(chǔ)對(duì)應(yīng)返回的數(shù)據(jù)。這樣下次有同樣請(qǐng)求的時(shí)候就可以直接讀取這里面的數(shù)據(jù)渠鸽。關(guān)于這里有一個(gè)緩存策略的問題需要討論:什么時(shí)候清理緩存叫乌?要么就是根據(jù)超時(shí)時(shí)間限制進(jìn)行清理,要么就是根據(jù)緩存數(shù)據(jù)大小進(jìn)行清理徽缚。這個(gè)策略的選擇要根據(jù)具體App的操作日志來決定憨奸。

比如安居客App,日志數(shù)據(jù)記錄顯示用戶平均使用時(shí)長(zhǎng)不到3分鐘凿试,但是用戶查看房源詳情的次數(shù)比較多排宰,而房源詳情數(shù)據(jù)量較大。那么這個(gè)時(shí)候那婉,就適合根據(jù)使用時(shí)長(zhǎng)來做緩存板甘,我當(dāng)時(shí)給安居客設(shè)置的緩存超時(shí)時(shí)間就是3分鐘,這樣能夠保證這個(gè)緩存能夠在大部分用戶使用時(shí)間產(chǎn)生作用详炬。嗯虾啦,極端情況下做什么緩存手段不考慮,只要能夠服務(wù)好80%的用戶就可以了痕寓,而且針對(duì)極端情況采用的優(yōu)化手段對(duì)大部分普通用戶而言是不必要的傲醉,做了反而會(huì)對(duì)他們有影響。

再比如網(wǎng)絡(luò)圖片緩存呻率,數(shù)據(jù)量基本上都特別大硬毕,這種就比較適合針對(duì)緩存大小來清理緩存的策略。

另外礼仗,之前的緩存的前提都是基于內(nèi)存的吐咳。我們也可以把需要清理的緩存存儲(chǔ)在硬盤上(APP的本地存儲(chǔ)逻悠,我就先用硬盤來表示了,雖然很少有手機(jī)硬盤的說法韭脊,哈哈)童谒,比如前面提到的圖片緩存,因?yàn)閳D片很有可能在很長(zhǎng)時(shí)間之后沪羔,再被顯示的饥伊,那么原本需要被清理的圖片緩存,我們就可以考慮存到硬盤上去蔫饰。當(dāng)下次再有顯示網(wǎng)絡(luò)圖片的需求的時(shí)候琅豆,我們可以先從內(nèi)存中找,內(nèi)存找不到那就從硬盤上找篓吁,這都找不到茫因,那就發(fā)起請(qǐng)求吧。

當(dāng)然杖剪,有些時(shí)效性非常短的API數(shù)據(jù)冻押,就不能使用這個(gè)方法了,比如用戶的資金數(shù)據(jù)盛嘿,那就需要每次都調(diào)用了翼雀。

  • 1.1.2 使用策略來減少請(qǐng)求的發(fā)起次數(shù)

這個(gè)我在前面提到過,就是針對(duì)重復(fù)請(qǐng)求的發(fā)起和取消孩擂,是有對(duì)應(yīng)的請(qǐng)求策略的狼渊。我們先說取消策略。

如果是界面刷新請(qǐng)求這種类垦,而且存在重復(fù)請(qǐng)求的情況(下拉刷新時(shí)狈邑,在請(qǐng)求著陸之前用戶不斷執(zhí)行下拉操作),那么這個(gè)時(shí)候蚤认,后面重復(fù)操作導(dǎo)致的API請(qǐng)求就可以不必發(fā)送了米苹。

如果是條件篩選這種,那就取消前面已經(jīng)發(fā)送的請(qǐng)求砰琢。雖然很有可能這個(gè)請(qǐng)求已經(jīng)被執(zhí)行了蘸嘶,那么取消所帶來的性能提升就基本沒有了。但如果這個(gè)請(qǐng)求還在隊(duì)列中待執(zhí)行的話陪汽,那么對(duì)應(yīng)的這次鏈接就可以省掉了训唱。

以上是一種,另外一種情況就是請(qǐng)求策略:類似用戶操作日志的請(qǐng)求策略挚冤。

用戶操作會(huì)觸發(fā)操作日志上報(bào)Server况增,這種請(qǐng)求特別頻繁,但是是暗地里進(jìn)行的训挡,不需要用戶對(duì)此有所感知澳骤。所以也沒必要操作一次就發(fā)起一次的請(qǐng)求歧强。在這里就可以采用這樣的策略:在本地記錄用戶的操作記錄,當(dāng)記錄滿30條的時(shí)候發(fā)起一次請(qǐng)求將操作記錄上傳到服務(wù)器为肮。然后每次App啟動(dòng)的時(shí)候摊册,上傳一次上次遺留下來沒上傳的操作記錄。這樣能夠有效降低用戶設(shè)備的耗電量颊艳,同時(shí)提升網(wǎng)絡(luò)層的性能茅特。

小總結(jié)

針對(duì)建立連接這部分的優(yōu)化就是這樣的原則:能不發(fā)請(qǐng)求的就盡量不發(fā)請(qǐng)求,必須要發(fā)請(qǐng)求時(shí)籽暇,能合并請(qǐng)求的就盡量合并請(qǐng)求。然而饭庞,任何優(yōu)化手段都是有前提的戒悠,而且也不能保證對(duì)所有需求都能起作用,有些API請(qǐng)求就是不符合這些優(yōu)化手段前提的舟山,那就老老實(shí)實(shí)發(fā)請(qǐng)求吧绸狐。不過這類API請(qǐng)求所占比例一般不大,大部分的請(qǐng)求都或多或少符合優(yōu)化條件累盗,所以針對(duì)發(fā)送請(qǐng)求的優(yōu)化手段還是值得做的寒矿。

1.2 & 1.3 針對(duì)DNS域名解析做的優(yōu)化,以及建立鏈接的優(yōu)化

其實(shí)在整個(gè)DNS鏈路上也是有DNS緩存的若债,理論上也是能夠提高速度的符相。這個(gè)鏈路上的DNS緩存在PC用戶上效果明顯,因?yàn)镻C用戶的DNS鏈路相對(duì)穩(wěn)定蠢琳,信號(hào)源不會(huì)變來變?nèi)グ≈铡5窃谝苿?dòng)設(shè)備的用戶這邊,鏈路上的DNS緩存所帶來的性能提升就不太明顯了傲须。因?yàn)橐苿?dòng)設(shè)備的實(shí)際使用場(chǎng)景比較復(fù)雜蓝牲,網(wǎng)絡(luò)信號(hào)源會(huì)經(jīng)常變換,信號(hào)源每變換一次泰讽,對(duì)應(yīng)的DNS解析鏈路就會(huì)變換一次例衍,那么原鏈路上的DNS緩存就不起作用了。而且信號(hào)源變換的情況特別特別頻繁已卸,所以對(duì)于移動(dòng)設(shè)備用戶來說佛玄,鏈路的DNS緩存我們基本上可以默認(rèn)為沒有。所以大部分時(shí)間是手機(jī)系統(tǒng)自帶的本地DNS緩存在起作用累澡,但是一般來說翎嫡,移動(dòng)設(shè)備上網(wǎng)的需求也特別頻繁,專門為我們這個(gè)App所做的DNS緩存很有可能會(huì)被別的DNS緩存給擠出去被清理掉永乌,這種情況是特別多的惑申,用戶看一會(huì)兒知乎刷一下微博查一下地圖逛一逛點(diǎn)評(píng)再聊個(gè)Q具伍,回來之后很有可能屬于你自己的App的本地DNS緩存就沒了。這還沒完圈驼,這里還有一個(gè)只有在中國(guó)特色社會(huì)主義的互聯(lián)網(wǎng)環(huán)境中才會(huì)有的問題:國(guó)內(nèi)的互聯(lián)網(wǎng)環(huán)境由于GFW的存在人芽,就使得DNS服務(wù)速度會(huì)比正常情況慢不少。

基于以上三個(gè)原因所導(dǎo)致的最終結(jié)果就是绩脆,API請(qǐng)求在DNS解析階段的耗時(shí)會(huì)很多萤厅。

那么針對(duì)這個(gè)的優(yōu)化方案就是,索性直接走IP請(qǐng)求靴迫,那不就繞過DNS服務(wù)的耗時(shí)了嘛惕味。


另外一個(gè),就是上面提到的建立鏈接時(shí)候的第三步玉锌,國(guó)內(nèi)的網(wǎng)絡(luò)環(huán)境分北網(wǎng)通南電信(當(dāng)然實(shí)際情況更復(fù)雜名挥,這里隨便說說),不同服務(wù)商之間的連接主守,延時(shí)是很大的禀倔,我們需要想辦法讓用戶在最適合他的IP上給他提供服務(wù),那么就針對(duì)我們繞過DNS服務(wù)的手段有一個(gè)額外要求:盡可能不要讓用戶使用對(duì)他來說很慢的IP参淫。

所以綜上所述救湖,方案就應(yīng)該是這樣:本地有一份IP列表,這些IP是所有提供API的服務(wù)器的IP涎才,每次應(yīng)用啟動(dòng)的時(shí)候鞋既,針對(duì)這個(gè)列表里的所有IP取ping延時(shí)時(shí)間,然后取延時(shí)時(shí)間最小的那個(gè)IP作為今后發(fā)起請(qǐng)求的IP地址耍铜。


針對(duì)建立連接的優(yōu)化手段其實(shí)是跟DNS域名解析的優(yōu)化手段是一樣的涛救。不過這需要你的服務(wù)器提供服務(wù)的網(wǎng)絡(luò)情況要多,一般現(xiàn)在的服務(wù)器都是雙網(wǎng)卡业扒,電信和網(wǎng)通检吆。由于中國(guó)特色的互聯(lián)網(wǎng)ISP分布,南北網(wǎng)絡(luò)之間存在瓶頸程储,而我們App針對(duì)鏈接的優(yōu)化手段主要就是著手于如何減輕這個(gè)瓶頸對(duì)App產(chǎn)生的影響蹭沛,所以需要維護(hù)一個(gè)IP列表,這樣就能就近連接了章鲤,就起到了優(yōu)化的效果摊灭。

我們一般都是在應(yīng)用啟動(dòng)的時(shí)候獲得本地列表中所有IP的ping值,然后通過NSURLProtocol的手段將URL中的HOST修改為我們找到的最快的IP败徊。另外帚呼,這個(gè)本地IP列表也會(huì)需要通過一個(gè)API來維護(hù),一般是每天第一次啟動(dòng)的時(shí)候讀一次API,然后更新到本地煤杀。

如果你還不熟悉NSURLProtocol應(yīng)該怎么玩眷蜈,看完官方文檔這篇文章以及這個(gè)Demo之后,你肯定就會(huì)了沈自,其實(shí)很簡(jiǎn)單的酌儒。另外,剛才提到那篇文章的作者(mattt)還寫了這個(gè)基于NSURLProtocol的工具枯途,相當(dāng)好用忌怎,是可以直接拿來集成到項(xiàng)目中的。

不用NSURLProtocol的話酪夷,用其他手段也可以做到這一點(diǎn)榴啸,但那些手段未免又比較愚蠢。

2. 針對(duì)鏈接傳輸數(shù)據(jù)量的優(yōu)化

這個(gè)很好理解晚岭,傳輸?shù)臄?shù)據(jù)少了鸥印,那么自然速度就上去了。這里沒什么花樣可以講的腥例,就是壓縮唄辅甥。各種壓縮酝润。

3. 針對(duì)鏈接復(fù)用的優(yōu)化

建立鏈接本身是屬于比較消耗資源的操作燎竖,耗電耗時(shí)。SPDY自帶鏈接復(fù)用以及數(shù)據(jù)壓縮的功能要销,所以服務(wù)端支持SPDY的時(shí)候构回,App直接掛SPDY就可以了。如果服務(wù)端不支持SPDY疏咐,也可以使用PipeLine纤掸,蘋果原生自帶這個(gè)功能。

一般來說業(yè)界內(nèi)普遍的認(rèn)識(shí)是SPDY優(yōu)于PipeLine浑塞,然后即便如此借跪,SPDY能夠帶來的網(wǎng)絡(luò)層效率提升其實(shí)也沒有文獻(xiàn)上的圖表那么明顯,但還是有性能提升的酌壕。還有另外一種比較笨的鏈接復(fù)用的方法掏愁,就是維護(hù)一個(gè)隊(duì)列,然后將隊(duì)列里的請(qǐng)求壓縮成一個(gè)請(qǐng)求發(fā)出去卵牍,之所以會(huì)存在滯留在隊(duì)列中的請(qǐng)求果港,是因?yàn)樵谏弦粋€(gè)請(qǐng)求還在外面飄的時(shí)候。這種做法最終的效果表面上看跟鏈接復(fù)用差別不大糊昙,但并不是真正的鏈接復(fù)用辛掠,只能說是請(qǐng)求合并。

還是說回來,我建議最好是用SPDY萝衩,SPDY和pipeline雖然都屬于鏈接復(fù)用的范疇回挽,但是pipeline并不是真正意義上的鏈接復(fù)用,SPDY的鏈接復(fù)用相對(duì)pipeline而言更為徹底欠气。SPDY目前也有現(xiàn)成的客戶端SDK可以使用厅各,一個(gè)是twitter的CocoaSPDY,另一個(gè)是Voxer/iSPDY预柒,這兩個(gè)庫(kù)都很活躍队塘,大家可以挑合適的采用。

不過目前業(yè)界趨勢(shì)是傾向于使用HTTP/2.0來代替SPDY宜鸯,不過目前HTTP/2.0還沒有正式出臺(tái)憔古,相關(guān)實(shí)現(xiàn)大部分都處在demo階段,所以我們還是先SPDY搞起就好了淋袖。未來很有可能會(huì)放棄SPDY鸿市,轉(zhuǎn)而采用HTTP/2.0來實(shí)現(xiàn)網(wǎng)絡(luò)的優(yōu)化。這是要提醒各位架構(gòu)師注意的事情即碗。嗯焰情,我也不知道HTTP/2.0什么時(shí)候能出來。


漁說完了剥懒,魚來了

這里是我當(dāng)年設(shè)計(jì)并實(shí)現(xiàn)的安居客的網(wǎng)絡(luò)層架構(gòu)代碼内舟。當(dāng)然,該脫敏的地方我都已經(jīng)脫敏了初橘,所以編不過是正常的验游,哈哈哈。但是代碼比較齊全保檐,重要地方注釋我也寫了很多耕蝉。另外,為了讓大家能夠把這些代碼看明白夜只,我還附帶了當(dāng)年介紹這個(gè)框架演講時(shí)的PPT垒在。(補(bǔ)充說明一下,評(píng)論區(qū)好多人問PPT找不著在哪兒扔亥,PPT也在上面提到的repo里面场躯,是個(gè)key后綴名的文件,用keynote打開)

然后就是砸王,當(dāng)年也有很多問題其實(shí)考慮得并沒有現(xiàn)在清楚推盛,所以有些地方還是做得不夠好,比如攔截器和繼承谦铃。而且當(dāng)時(shí)的優(yōu)化手段只有本地cache耘成,安居客沒有那么多IP可以給我ping,當(dāng)年也沒流行SPDY,而且API也還不支持HTTPS瘪菌,所以當(dāng)時(shí)的代碼里面沒有在這些地方做優(yōu)化撒会,比較原始。然而整個(gè)架構(gòu)的基本思路一直沒有變化:優(yōu)先服務(wù)于業(yè)務(wù)方师妙。另外诵肛,安居客的網(wǎng)絡(luò)層多了一個(gè)service的概念,這是我這篇文章中沒有講的默穴。主要是因?yàn)榘簿涌偷腁PI提供方很多怔檩,二手房,租房蓄诽,新房薛训,X項(xiàng)目等等API都是不同的API team提供的,以service作區(qū)分仑氛,如果你的app也是類似的情況乙埃,我也建議你設(shè)計(jì)一套service機(jī)制。現(xiàn)在這些service被我刪得只剩下一個(gè)google的service锯岖,因?yàn)槠渌鹲ervice都屬于敏感內(nèi)容介袜。

另外,這里面提供的PPT我很希望大家能夠花時(shí)間去看看出吹,在PPT里面有些更加細(xì)的東西我在博客里沒有寫遇伞,主要是我比較懶,然后這篇文章拖的時(shí)間比較長(zhǎng)了趋箩,花時(shí)間搬運(yùn)這個(gè)沒什么意思赃额,不過內(nèi)容還是值得各位讀者去看的加派。關(guān)于PPT里面大家有什么問題的叫确,也可以在評(píng)論區(qū)問,我都會(huì)回答芍锦。

總結(jié)

第一部分主要講了網(wǎng)絡(luò)層應(yīng)當(dāng)如何跟業(yè)務(wù)層進(jìn)行數(shù)據(jù)交互竹勉,進(jìn)行數(shù)據(jù)交互時(shí)采用怎樣的數(shù)據(jù)格式,以及設(shè)計(jì)時(shí)代碼結(jié)構(gòu)上的一些問題娄琉,諸如繼承的處理次乓,回調(diào)的處理寥茫,交互方式的選擇望艺,reformer的設(shè)計(jì),保持?jǐn)?shù)據(jù)可讀性等等等等封救,主要偏重于設(shè)計(jì)(這可是藝術(shù)活女气,哈哈哈)杏慰。

第二部分講了網(wǎng)絡(luò)安全上,客戶端要做的兩點(diǎn)。當(dāng)然缘滥,從網(wǎng)絡(luò)安全的角度上講轰胁,服務(wù)端也要做很多很多事情,客戶端要做的一些邊角細(xì)節(jié)的事情也還會(huì)有很多朝扼,比如做一些代碼混淆赃阀,盡可能避免代碼中明文展示key。不過大頭主要就是這兩個(gè)擎颖,而且也都是需要服務(wù)端同學(xué)去配合的榛斯。主要偏重于介紹。(主要是也沒啥好實(shí)踐的搂捧,google一下教程照著來就好了)肖抱。

第三部分講了優(yōu)化,優(yōu)化的所有方面都已經(jīng)列出來了异旧,如果業(yè)界再有七七八八的別的手段意述,也基本逃離不出本文的范圍。這里有些優(yōu)化手段是需要服務(wù)端同學(xué)配合的吮蛹,有些不需要荤崇,大家看各自情況來決定。主要偏重于實(shí)踐潮针。

最后給出了我之前在安居客做的網(wǎng)絡(luò)層架構(gòu)的主要代碼术荤,以及當(dāng)時(shí)演講時(shí)的PPT。關(guān)于代碼或PPT中有任何問題每篷,都可以在評(píng)論區(qū)問我瓣戚。

這一篇文章出得比較晚,因?yàn)楣镜氖虑榻苟粒虚g間隔了一個(gè)禮拜子库,希望大家諒解。另外矗晃,隔了一個(gè)禮拜之后我再寫仑嗅,發(fā)現(xiàn)有些地方我已經(jīng)想不起來當(dāng)初是應(yīng)該怎么行文下去的了,然后發(fā)之前我把文章又看了幾遍张症,盡可能把斷片的地方抹平了仓技,如果大家讀起來有什么地方感覺奇怪的,或者講到一半就沒了的俗他,那應(yīng)該就是斷片了脖捻。在評(píng)論區(qū)跟我說一下,我補(bǔ)上去兆衅。

然后如果有需要勘誤的地方地沮,也請(qǐng)?jiān)谠u(píng)論區(qū)指出颜价,幫助我把錯(cuò)的地方訂正回來,如果有沒講到的地方诉濒,但你又特別想要了解的周伦,也可以在評(píng)論區(qū)提出來,我會(huì)補(bǔ)上去未荒。說不定看完之后你腦袋里還會(huì)有很多個(gè)問號(hào)专挪,也請(qǐng)?jiān)谠u(píng)論區(qū)問出來哈,說不定別人也有跟你一樣的問題片排,他就能在評(píng)論區(qū)找到答案了寨腔。


在第二篇文章的評(píng)論區(qū)里面出現(xiàn)了噴子,遇到這種情況我怎么可能刪帖呢率寡?那根本就不是我的風(fēng)格哇迫卢,哈哈哈。我肯定是會(huì)噴回去的冶共,并且還會(huì)把鏈接傳播給周圍人乾蛤,發(fā)動(dòng)周圍朋友來看:"快看,這兒有2B捅僵,哈哈哈"家卖。

嗯,所以評(píng)論的時(shí)候你一定要想清楚哈庙楚,我寫代碼的實(shí)力不差上荡,打嘴仗的實(shí)力那可比寫代碼強(qiáng)多了。評(píng)論區(qū)同樣歡迎切磋馒闷。


有任何問題建議直接在評(píng)論區(qū)提問酪捡,這樣后來的人如果有相同的問題,就能直接找到答案了纳账。提問之前也可以先看看評(píng)論區(qū)有沒有人問過類似問題了逛薇。

所有評(píng)論和問題我都會(huì)在第一時(shí)間回復(fù),QQ上我是不回答問題的哈塞祈。

評(píng)論系統(tǒng)我用的是Disqus金刁,不定期被墻帅涂。所以如果你看到文章下面沒有加載出評(píng)論列表议薪,翻個(gè)墻就有了。

本文遵守CC-BY媳友。 請(qǐng)保持轉(zhuǎn)載后文章內(nèi)容的完整斯议,以及文章出處。本人保留所有版權(quán)相關(guān)權(quán)利醇锚。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哼御,一起剝皮案震驚了整個(gè)濱河市坯临,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恋昼,老刑警劉巖看靠,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異液肌,居然都是意外死亡挟炬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門嗦哆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谤祖,“玉大人,你說我怎么就攤上這事老速≈嘞玻” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵橘券,是天一觀的道長(zhǎng)额湘。 經(jīng)常有香客問我,道長(zhǎng)旁舰,這世上最難降的妖魔是什么缩挑? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鬓梅,結(jié)果婚禮上供置,老公的妹妹穿的比我還像新娘。我一直安慰自己绽快,他們只是感情好芥丧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坊罢,像睡著了一般续担。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上活孩,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天物遇,我揣著相機(jī)與錄音,去河邊找鬼憾儒。 笑死询兴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的起趾。 我是一名探鬼主播诗舰,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼训裆!你這毒婦竟也來了眶根?” 一聲冷哼從身側(cè)響起蜀铲,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎属百,沒想到半個(gè)月后记劝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡族扰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年隆夯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片别伏。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蹄衷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厘肮,到底是詐尸還是另有隱情愧口,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布类茂,位于F島的核電站耍属,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏巩检。R本人自食惡果不足惜厚骗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望兢哭。 院中可真熱鬧领舰,春花似錦、人聲如沸迟螺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)矩父。三九已至锉桑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窍株,已是汗流浹背民轴。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留球订,地道東北人后裸。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像辙售,于是被迫代替她去往敵國(guó)和親轻抱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容