iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案
前言
網(wǎng)絡(luò)層在一個App中也是一個不可缺少的部分鸦做,工程師們在網(wǎng)絡(luò)層能夠發(fā)揮的空間也比較大。另外袱结,蘋果對網(wǎng)絡(luò)請求部分已經(jīng)做了很好的封裝作瞄,業(yè)界的AFNetworking也被廣泛使用。其它的ASIHttpRequest桐愉,MKNetworkKit啥的其實(shí)也都還不錯财破,但前者已經(jīng)棄坑,后者也在棄坑的邊緣仅财。在實(shí)際的App開發(fā)中狈究,Afnetworking已經(jīng)成為了事實(shí)上各大App的標(biāo)準(zhǔn)配置。
網(wǎng)絡(luò)層在一個App中承載了API調(diào)用,用戶操作日志記錄抖锥,甚至是即時通訊等任務(wù)亿眠。我接觸過一些App(開源的和不開源的)的代碼,在看到網(wǎng)絡(luò)層這一塊時磅废,尤其是在看到各位架構(gòu)師各顯神通展示了各種技巧纳像,我非常為之感到興奮。但有的時候拯勉,往往也對于其中的一些缺陷感到失望竟趾。
關(guān)于網(wǎng)絡(luò)層的設(shè)計(jì)方案會有很多,需要權(quán)衡的地方也會有很多宫峦,甚至于爭議的地方都會有很多岔帽。但無論如何,我都不會對這些問題做出任何逃避导绷,我會在這篇文章中給出我對它們的看法和解決方案犀勒,觀點(diǎn)絕不中立,不會跟大家打太極妥曲。
這篇文章就主要會講這些方面:
網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的設(shè)計(jì)
網(wǎng)絡(luò)層的安全機(jī)制實(shí)現(xiàn)
網(wǎng)絡(luò)層的優(yōu)化方案
網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的設(shè)計(jì)
在安居客App的架構(gòu)更新?lián)Q代的時候贾费,我深深地感覺到網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的設(shè)計(jì)有多么重要,因此我對它做的最大改變就是針對網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的改變檐盟。網(wǎng)絡(luò)層跟業(yè)務(wù)層對接部分設(shè)計(jì)的好壞褂萧,會直接影響到業(yè)務(wù)工程師實(shí)現(xiàn)功能時的心情。
在正式開始講設(shè)計(jì)之前葵萎,我們要先討論幾個問題:
使用哪種交互模式來跟業(yè)務(wù)層做對接导犹?
是否有必要將API返回的數(shù)據(jù)封裝成對象然后再交付給業(yè)務(wù)層?
使用集約化調(diào)用方式還是離散型調(diào)用方式去調(diào)用API陌宿?
這些問題討論完畢之后锡足,我會給出一個完整的設(shè)計(jì)方案來給大家做參考,設(shè)計(jì)方案是魚壳坪,討論的這些問題是漁舶得,我什么都授了,大家各取所需爽蝴。
使用哪種交互模式來跟業(yè)務(wù)層做對接沐批?
這里其實(shí)有兩個問題:
以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層?
交付什么樣的數(shù)據(jù)給業(yè)務(wù)層蝎亚?
以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層九孩?
iOS開發(fā)領(lǐng)域有很多對象間數(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ù)層選擇自己合適的方法去使用球涛。這里大家都是各顯神通,每次我看到這部分的時候校镐,我都喜歡問作者為什么采用這種交互方案亿扁,但很少有作者能夠說出個條條框框來。
然而在我這邊灭翔,我的意見是以Delegate為主魏烫,Notification為輔辣苏。原因如下:
盡可能減少跨層數(shù)據(jù)交流的可能肝箱,限制耦合
統(tǒng)一回調(diào)方法,便于調(diào)試和維護(hù)
在跟業(yè)務(wù)層對接的部分只采用一種對接手段(在我這兒就是只采用delegate這一個手段)限制靈活性稀蟋,以此來交換應(yīng)用的可維護(hù)性
盡可能減少跨層數(shù)據(jù)交流的可能煌张,限制耦合
什么叫跨層數(shù)據(jù)交流?就是某一層(或模塊)跟另外的與之沒有直接對接關(guān)系的層(或模塊)產(chǎn)生了數(shù)據(jù)交換退客。為什么這種情況不好骏融?嚴(yán)格來說應(yīng)該是大部分情況都不好,有的時候跨層數(shù)據(jù)交流確實(shí)也是一種需求萌狂。之所以說不好的地方在于档玻,它會導(dǎo)致代碼混亂,破壞模塊的封裝性茫藏。我們在做分層架構(gòu)的目的其中之一就在于下層對上層有一次抽象误趴,讓上層可以不必關(guān)心下層細(xì)節(jié)而執(zhí)行自己的業(yè)務(wù)。
所以务傲,如果下層細(xì)節(jié)被跨層暴露凉当,一方面你很容易因此失去鄰層對這個暴露細(xì)節(jié)的保護(hù);另一方面售葡,你又不可能不去處理這個細(xì)節(jié)看杭,所以處理細(xì)節(jié)的相關(guān)代碼就會散落各地,最終難以維護(hù)挟伙。
說得具象一點(diǎn)就是楼雹,我們考慮這樣一種情況:A<-B<-C。當(dāng)C有什么事件,通過某種方式告知B贮缅,然后B執(zhí)行相應(yīng)的邏輯瓜贾。一旦告知方式不合理,讓A有了跨層知道C的事件的可能携悯,你 就很難保證A層業(yè)務(wù)工程師在將來不會對這個細(xì)節(jié)作處理祭芦。一旦業(yè)務(wù)工程師在A層產(chǎn)生處理操作,有可能是補(bǔ)充邏輯憔鬼,也有可能是執(zhí)行業(yè)務(wù)龟劲,那么這個細(xì)節(jié)的相關(guān)處理代碼就會有一部分散落在A層。然而前者是不應(yīng)該散落在A層的轴或,后者有可能是需求昌跌。另外,因?yàn)锽層是對A層抽象的照雁,執(zhí)行補(bǔ)充邏輯的時候蚕愤,有可能和B層針對這個事件的處理邏輯產(chǎn)生沖突,這是我們很不希望看到的饺蚊。
那么什么情況跨層數(shù)據(jù)交流會成為需求萍诱?在網(wǎng)絡(luò)層這邊,信號從2G變成3G變成4G變成Wi-Fi污呼,這個是跨層數(shù)據(jù)交流的其中一個需求裕坊。不過其他的跨層數(shù)據(jù)交流需求我暫時也想不到了,哈哈燕酷,應(yīng)該也就這一個吧籍凝。
嚴(yán)格來說,使用Notification來進(jìn)行網(wǎng)絡(luò)層和業(yè)務(wù)層之間數(shù)據(jù)的交換苗缩,并不代表這一定就是跨層數(shù)據(jù)交流饵蒂,但是使用Notification給跨層數(shù)據(jù)交流開了一道口子谴餐,因?yàn)镹otification的影響面不可控制缺脉,只要存在實(shí)例就存在被影響的可能矢洲。另外砸西,這也會導(dǎo)致誰都不能保證相關(guān)處理代碼就在唯一的那個地方器紧,進(jìn)而帶來維護(hù)災(zāi)難螃概。作為架構(gòu)師砍聊,在這里給業(yè)務(wù)工程師限制其操作的靈活性是必要的丈氓。另外软免,Notification也支持一對多的情況宫纬,這也給代碼散落提供了條件。同時膏萧,Notification所對應(yīng)的響應(yīng)方法很難在編譯層面作限制漓骚,不同的業(yè)務(wù)工程師會給他取不同的名字蝌衔,這也會給代碼的可維護(hù)性帶來災(zāi)難。
手機(jī)淘寶架構(gòu)組的俠武同學(xué)曾經(jīng)給我分享過一個問題蝌蹂,在這里我也分享給大家:曾經(jīng)有一個工程師在監(jiān)聽Notification之后噩斟,沒有寫釋放監(jiān)聽的代碼,當(dāng)然孤个,找到這個原因又是很漫長的一段故事剃允,現(xiàn)在找到原因了,然而監(jiān)聽這個Notification的對象有那么多齐鲤,不知道具體是哪個Notificaiton斥废,也不知道那個沒釋放監(jiān)聽的對象是誰。后來折騰了很久大家都沒辦法的時候给郊,有一個經(jīng)驗(yàn)豐富的工程師提出用hook(Method Swizzling)的方式牡肉,最終找到了那個沒釋放監(jiān)聽的對象,bug修復(fù)了淆九。
我分享這個問題的目的并不是想強(qiáng)調(diào)Notification多么多么不好统锤,Notification本身就是一種設(shè)計(jì)模式,在屬于他的問題領(lǐng)域內(nèi)炭庙,Notification是非常好的一種解決方案饲窿。但我想強(qiáng)調(diào)的是,對于網(wǎng)絡(luò)層這個問題領(lǐng)域內(nèi)來看煤搜,架構(gòu)師首先一定要限制代碼的影響范圍免绿,在能用影響范圍小的方案的時候就盡量采用這種小的方案,否則將來要是有什么奇怪需求或者出了什么小問題擦盾,維護(hù)起來就非常麻煩。因此Notification這個方案不能作為首選方案淌哟,只能作為備選迹卢。
那么Notification也不是完全不能使用,當(dāng)需求要求跨層時徒仓,我們就可以使用Notification腐碱,比如前面提到的網(wǎng)絡(luò)條件切換,而且這個需求也是需要滿足一對多的掉弛。
所以症见,為了符合前面所說的這些要求,使用Delegate能夠很好地避免跨層訪問殃饿,同時限制了響應(yīng)代碼的形式谋作,相比Notification而言有更好的可維護(hù)性。
然后我們順便來說說為什么盡量不要用block乎芳。
block很難追蹤遵蚜,難以維護(hù)
我們在調(diào)試的時候經(jīng)常會單步追蹤到某一個地方之后帖池,發(fā)現(xiàn)尼瑪這里有個block,如果想知道這個block里面都做了些什么事情吭净,這時候就比較蛋疼了睡汹。
- (void)someFunctionWithBlock:(SomeBlock *)block
{
... ...
-> block();? //當(dāng)你單步走到這兒的時候,要想知道block里面都做了哪些事情的話寂殉,就很麻煩囚巴。
... ...
}
block會延長相關(guān)對象的生命周期
block會給內(nèi)部所有的對象引用計(jì)數(shù)加一,這一方面會帶來潛在的retain cycle友扰,不過我們可以通過Weak Self的手段解決文兢。另一方面比較重要就是,它會延長對象的生命周期焕檬。
在網(wǎng)絡(luò)回調(diào)中使用block姆坚,是block導(dǎo)致對象生命周期被延長的其中一個場合,當(dāng)ViewController從window中卸下時实愚,如果尚有請求帶著block在外面飛兼呵,然后block里面引用了ViewController(這種場合非常常見),那么ViewController是不能被及時回收的腊敲,即便你已經(jīng)取消了請求击喂,那也還是必須得等到請求著陸之后才能被回收。
然而使用delegate就不會有這樣的問題碰辅,delegate是弱引用懂昂,哪怕請求仍然在外面飛,没宾,ViewController還是能夠及時被回收的凌彬,回收之后指針自動被置為了nil,無傷大雅循衰。
block在離散型場景下不符合使用的規(guī)范
block和delegate乍看上去在作用上是很相似铲敛,但是關(guān)于它們的選型有一條嚴(yán)格的規(guī)范:當(dāng)回調(diào)之后要做的任務(wù)在每次回調(diào)時都是一致的情況下,選擇delegate会钝,在回調(diào)之后要做的任務(wù)在每次回調(diào)時無法保證一致伐蒋,選擇block。在離散型調(diào)用的場景下迁酸,每一次回調(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)用的場景下全蝶,使用block是合理的闹蒜,因?yàn)槊看握埱蟮念愋投疾灰粯铀峦鳎敲醋匀换卣{(diào)要做的任務(wù)也都會不一樣,因此只能采用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)用時避归,一定要注意這一點(diǎn),記得遷回delegate回調(diào)管呵。關(guān)于離散型和集約型調(diào)用的介紹和如何選型梳毙,我在后面的集約型API調(diào)用方式和離散型API調(diào)用方式的選擇?小節(jié)中有詳細(xì)的介紹捐下。
所以平時盡量不要濫用block账锹,尤其是在網(wǎng)絡(luò)層這里。
統(tǒng)一回調(diào)方法坷襟,便于調(diào)試和維護(hù)
前面講的是跨層問題奸柬,區(qū)分了Delegate和Notification,順帶談了一下Block婴程。然后現(xiàn)在談到的這個情況廓奕,就是另一個采用Block方案不是很合適的情況。首先档叔,Block本身無好壞對錯之分桌粉,只有合適不合適。在這一節(jié)要講的情況里蹲蒲,Block無法做到回調(diào)方法的統(tǒng)一番甩,調(diào)試和維護(hù)的時候也很難在調(diào)用棧上顯示出來,找的時候會很蛋疼届搁。
在網(wǎng)絡(luò)請求和網(wǎng)絡(luò)層接受請求的地方時,使用Block沒問題窍育。但是在獲得數(shù)據(jù)交給業(yè)務(wù)方時卡睦,最好還是通過Delegate去通知到業(yè)務(wù)方。因?yàn)锽lock所包含的回調(diào)代碼跟調(diào)用邏輯放在同一個地方漱抓,會導(dǎo)致那部分代碼變得很長表锻,因?yàn)檫@里面包括了調(diào)用前和調(diào)用后的邏輯。從另一個角度說乞娄,這在一定程度上違背了single function瞬逊,single task的原則显歧,在需要調(diào)用API的地方,就只要寫API調(diào)用相關(guān)的代碼确镊,在回調(diào)的地方士骤,寫回調(diào)的代碼。
然后我看到大部分App里蕾域,當(dāng)業(yè)務(wù)工程師寫代碼寫到這邊的時候拷肌,也意識到了這個問題。因此他們會在block里面寫個一句話的方法接收參數(shù)旨巷,然后做轉(zhuǎn)發(fā)巨缘,然后就可以把這個方法放在其他地方了,繞過了Block的回調(diào)著陸點(diǎn)不統(tǒng)一的情況采呐。比如這樣:
[APIcallApiWithParam:paramsuccessed:^(Response*response){[selfsuccessedWithResponse:response];}failed:^(Request*request,NSError*error){[selffailedWithRequest:requesterror:error];}];
這實(shí)質(zhì)上跟使用Delegate的手段沒有什么區(qū)別若锁,只是繞了一下,不過還是沒有解決統(tǒng)一回調(diào)方法的問題斧吐,因?yàn)閎lock里面寫的方法名字可能在不同的ViewController對象中都會不一樣又固,畢竟業(yè)務(wù)工程師也是很多人,各人有各人的想法会通。所以架構(gòu)師在這邊不要貪圖方便口予,還是使用delegate的手段吧,業(yè)務(wù)工程師那邊就能不用那么繞了涕侈。Block是目前大部分第三方網(wǎng)絡(luò)庫都采用的方式沪停,因?yàn)樵诎l(fā)送請求的那一部分,使用Block能夠比較簡潔裳涛,因此在請求那一層是沒有問題的木张,只是在交換數(shù)據(jù)之后,還是轉(zhuǎn)變成delegate比較好端三,比如AFNetworking里面:
[AFNetworkingAPIcallApiWithParam:self.paramsuccessed:^(Response*response){if([self.delegaterespondsToSelector:@selector(successWithResponse:)]){[self.delegatesuccessedWithResponse:response];}}failed:^(Request*request,NSError*error){if([self.delegaterespondsToSelector:@selector(failedWithResponse:)]){[selffailedWithRequest:requesterror:error];}}];
這樣在業(yè)務(wù)方這邊回調(diào)函數(shù)就能夠比較統(tǒng)一舷礼,便于維護(hù)。
綜上郊闯,對于以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層妻献?這個問題的回答是這樣:
盡可能通過Delegate的回調(diào)方式交付數(shù)據(jù),這樣可以避免不必要的跨層訪問团赁。當(dāng)出現(xiàn)跨層訪問的需求時(比如信號類型切換)育拨,通過Notification的方式交付數(shù)據(jù)。正常情況下應(yīng)該是避免使用Block的欢摄。
交付什么樣的數(shù)據(jù)給業(yè)務(wù)層熬丧?
我見過非常多的App的網(wǎng)絡(luò)層在拿到JSON數(shù)據(jù)之后,會將數(shù)據(jù)轉(zhuǎn)變成對應(yīng)的對象原型怀挠。注意析蝴,我這里指的不是NSDictionary害捕,而是類似Item這樣的對象。這種做法是能夠提高后續(xù)操作代碼的可讀性的闷畸。在比較直覺的思路里面尝盼,是需要這部分轉(zhuǎn)化過程的,但這部分轉(zhuǎn)化過程的成本是很大的腾啥,主要成本在于:
數(shù)組內(nèi)容的轉(zhuǎn)化成本較高:數(shù)組里面每項(xiàng)都要轉(zhuǎn)化成Item對象东涡,如果Item對象中還有類似數(shù)組,就很頭疼倘待。
轉(zhuǎn)化之后的數(shù)據(jù)在大部分情況是不能直接被展示的疮跑,為了能夠被展示,還需要第二次轉(zhuǎn)化凸舵。
只有在API返回的數(shù)據(jù)高度標(biāo)準(zhǔn)化時祖娘,這些對象原型(Item)的可復(fù)用程度才高,否則容易出現(xiàn)類型爆炸啊奄,提高維護(hù)成本渐苏。
調(diào)試時通過對象原型查看數(shù)據(jù)內(nèi)容不如直接通過NSDictionary/NSArray直觀。
同一API的數(shù)據(jù)被不同View展示時菇夸,難以控制數(shù)據(jù)轉(zhuǎn)化的代碼琼富,它們有可能會散落在任何需要的地方。
其實(shí)我們的理想情況是希望API的數(shù)據(jù)下發(fā)之后就能夠直接被View所展示庄新。首先要說的是鞠眉,這種情況非常少。另外择诈,這種做法使得View和API聯(lián)系緊密械蹋,也是我們不希望發(fā)生的。
在設(shè)計(jì)安居客的網(wǎng)絡(luò)層數(shù)據(jù)交付這部分時羞芍,我添加了reformer(名字而已哗戈,叫什么都好)這個對象用于封裝數(shù)據(jù)轉(zhuǎn)化的邏輯,這個對象是一個獨(dú)立對象荷科,事實(shí)上唯咬,它是作為Adaptor模式存在的。我們可以這么理解:想象一下我們洗澡時候使用的蓮蓬頭畏浆,水管里出來的水是API下發(fā)的原始數(shù)據(jù)副渴。reformer就是蓮蓬頭上的不同水流擋板,需要什么模式全度,就撥到什么模式。
在實(shí)際使用時斥滤,代碼觀感是這樣的:
先定義一個protocol:@protocolReformerProtocol-(NSDictionary)reformDataWithManager:(APIManager*)manager;@end在Controller里是這樣:@property(nonatomic,strong)idXXXReformer;@property(nonatomic,strong)idYYYReformer;#pragma mark - APIManagerDelegate-(void)apiManagerDidSuccess:(APIManager*)manager{NSDictionary*reformedXXXData=[managerfetchDataWithReformer:self.XXXReformer];[self.XXXViewconfigWithData:reformedXXXData];NSDictionary*reformedYYYData=[managerfetchDataWithReformer:self.YYYReformer];[self.YYYViewconfigWithData:reformedYYYData];}在APIManager里面将鸵,fetchDataWithReformer是這樣:-(NSDictionary)fetchDataWithReformer:(id)reformer{if(reformer==nil){returnself.rawData;}else{return[reformerreformDataWithManager:self];}}
要點(diǎn)1:reformer是一個符合ReformerProtocol的對象勉盅,它提供了通用的方法供Manager使用。
要點(diǎn)2:API的原始數(shù)據(jù)(JSON對象)由Manager實(shí)例保管顶掉,reformer方法里面取Manager的原始數(shù)據(jù)(manager.rawData)做轉(zhuǎn)換草娜,然后交付出去。蓮蓬頭的水管部分是Manager痒筒,負(fù)責(zé)提供原始水流(數(shù)據(jù)流)宰闰,reformer就是不同的模式,換什么reformer就能出來什么水流簿透。
要點(diǎn)3:例子中舉的場景是一個API數(shù)據(jù)被多個View使用的情況移袍,體現(xiàn)了reformer的一個特點(diǎn):可以根據(jù)需要改變同一數(shù)據(jù)來源的展示方式。比如API數(shù)據(jù)展示的是“附近的小區(qū)”老充,那么這個數(shù)據(jù)可以被列表(XXXView)和地圖(YYYView)共用葡盗,不同的view使用的數(shù)據(jù)的轉(zhuǎn)化方式不一樣,這就通過不同的reformer解決了啡浊。
要點(diǎn)4:在一個view用來同一展示不同API數(shù)據(jù)的情況觅够,reformer是絕佳利器。比如安居客的列表view的數(shù)據(jù)來源可能有三個:二手房列表API巷嚣,租房列表API喘先,新房列表API。這些API返回來的數(shù)據(jù)的value可能一致廷粒,但是key都是不一致的窘拯。這時候就可以通過同一個reformer來做數(shù)據(jù)的標(biāo)準(zhǔn)化輸出,這樣就使得view代碼復(fù)用成為可能评雌。這體現(xiàn)了reformer另外一個特點(diǎn):同一個reformer出來的數(shù)據(jù)是高度標(biāo)準(zhǔn)化的树枫。形象點(diǎn)說就是:只要蓮蓬頭不換,哪怕水管的水變成海水或者污水了景东,也依舊能夠輸出符合洗澡要求的淡水水流砂轻。舉個例子:
-(void)apiManagerDidSuccess:(APIManager*)manager{// 這個回調(diào)方法有可能是來自二手房列表APIManager的回調(diào),也有可能是租房斤吐,也有可能是新房搔涝。但是在Controller層面我們不需要對它做額外區(qū)分,只要是同一個reformer出來的數(shù)據(jù)和措,我們就能保證是一定能被self.XXXView使用的庄呈。這樣的保證由reformer的實(shí)現(xiàn)者來提供。NSDictionary*reformedXXXData=[managerfetchDataWithReformer:self.XXXReformer];[self.XXXViewconfigWithData:reformedXXXData];}
要點(diǎn)5:有沒有發(fā)現(xiàn)派阱,使用reformer之后诬留,Controller的代碼簡潔了很多?而且,數(shù)據(jù)原型在這種情況下就沒有必要存在了文兑,隨之而來的成本也就被我們繞過了盒刚。
reformer本質(zhì)上就是一個符合某個protocol的對象,在controller需要從api manager中獲得數(shù)據(jù)的時候绿贞,順便把reformer傳進(jìn)去因块,于是就能獲得經(jīng)過reformer重新洗過的數(shù)據(jù),然后就可以直接使用了籍铁。
更抽象地說涡上,reformer其實(shí)是對數(shù)據(jù)轉(zhuǎn)化邏輯的一個封裝。在controller從manager中取數(shù)據(jù)之后拒名,并且把數(shù)據(jù)交給view之前吩愧,這期間或多或少都是要做一次數(shù)據(jù)轉(zhuǎn)化的,有的時候不同的view靡狞,對應(yīng)的轉(zhuǎn)化邏輯還不一樣耻警,但是展示的數(shù)據(jù)是一樣的。而且往往這一部分代碼都非常復(fù)雜甸怕,且跟業(yè)務(wù)強(qiáng)相關(guān)甘穿,直接上代碼,將來就會很難維護(hù)梢杭。所以我們可以考慮采用不同的reformer封裝不同的轉(zhuǎn)化邏輯温兼,然后讓controller根據(jù)需要選擇一個合適的reformer裝上,就像洗澡的蓮蓬頭武契,需要什么樣的水流(數(shù)據(jù)的表現(xiàn)形式)就換什么樣的頭募判,然而水(數(shù)據(jù))都是一樣的。這種做法能夠大大提高代碼的可維護(hù)性咒唆,以及減少ViewController的體積届垫。
總結(jié)一下,reformer事實(shí)上是把轉(zhuǎn)化的代碼封裝之后再從主體業(yè)務(wù)中拆分了出來全释,拆分出來之后不光降低了原有業(yè)務(wù)的復(fù)雜度装处,更重要的是,它提高了數(shù)據(jù)交付的靈活性浸船。另外妄迁,由于Controller負(fù)責(zé)調(diào)度Manager和View,因此它是知道Manager和View之間的關(guān)系的李命,Controller知道了這個關(guān)系之后登淘,就有了充要條件來為不同的View選擇不同的Reformer,并用這個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對多API,以及在單API對多View的情況時项贺,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映射成對應(yīng)的原型俺泣,第二次是把原型轉(zhuǎn)變成能被View處理的數(shù)據(jù)。reformer一步到位完残。另外伏钠,轉(zhuǎn)化邏輯在reformer里面,將來如果API數(shù)據(jù)有變谨设,就只要去找到對應(yīng)reformer然后改掉就好了熟掂。
好處4:Controller因此可以省去非常多的代碼,降低了代碼復(fù)雜度扎拣,同時提高了靈活性赴肚,任何時候切換reformer而不必切換業(yè)務(wù)邏輯就可以應(yīng)對不同View對數(shù)據(jù)的需要。
好處5:業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)有了適當(dāng)?shù)母綦x二蓝。這么做的話誉券,將來如果業(yè)務(wù)邏輯有修改,換一個reformer就好了刊愚。如果其他業(yè)務(wù)也有相同的數(shù)據(jù)轉(zhuǎn)化邏輯踊跟,其他業(yè)務(wù)直接拿這個reformer就可以用了,不用重寫百拓。另外琴锭,如果controller有修改(比如UI交互方式改變),可以放心換controller衙传,完全不用擔(dān)心業(yè)務(wù)數(shù)據(jù)的處理决帖。
在不使用特定對象表征數(shù)據(jù)的情況下,如何保持?jǐn)?shù)據(jù)可讀性蓖捶?
不使用對象來表征數(shù)據(jù)的時候地回,事實(shí)上就是使用NSDictionary的時候。事實(shí)上,這個問題就是刻像,如何在NSDictionary表征數(shù)據(jù)的情況下保持良好的可讀性畅买?
蘋果已經(jīng)給出了非常好的做法,用固定字符串做key细睡,比如你在接收到KeyBoardWillShow的Notification時谷羞,帶了一個userInfo,他的key就都是類似UIKeyboardAnimationCurveUserInfoKey這樣的溜徙,所以我們采用這樣的方案來維持可讀性湃缎。下面我舉一個例子:
PropertyListReformerKeys.h
extern NSString * const kPropertyListDataKeyID;
extern NSString * const kPropertyListDataKeyName;
extern NSString * const kPropertyListDataKeyTitle;
extern NSString * const kPropertyListDataKeyImage;
PropertyListReformer.h#import "PropertyListReformerKeys.h"......
PropertyListReformer.mNSString*constkPropertyListDataKeyID=@"kPropertyListDataKeyID";NSString*constkPropertyListDataKeyName=@"kPropertyListDataKeyName";NSString*constkPropertyListDataKeyTitle=@"kPropertyListDataKeyTitle";NSString*constkPropertyListDataKeyImage=@"kPropertyListDataKeyImage";-(NSDictionary*)reformData:(NSDictionary*)originDatafromManager:(APIManager*)manager{............NSDictionary*resultData=nil;if([managerisKindOfClass:[ZuFangListAPIManagerclass]]){resultData=@{kPropertyListDataKeyID:originData[@"id"],kPropertyListDataKeyName:originData[@"name"],kPropertyListDataKeyTitle:originData[@"title"],kPropertyListDataKeyImage:[UIImageimageWithUrlString:originData[@"imageUrl"]]};}if([managerisKindOfClass:[XinFangListAPIManagerclass]]){resultData=@{kPropertyListDataKeyID:originData[@"xinfang_id"],kPropertyListDataKeyName:originData[@"xinfang_name"],kPropertyListDataKeyTitle:originData[@"xinfang_title"],kPropertyListDataKeyImage:[UIImageimageWithUrlString:originData[@"xinfang_imageUrl"]]};}if([managerisKindOfClass:[ErShouFangListAPIManagerclass]]){resultData=@{kPropertyListDataKeyID:originData[@"esf_id"],kPropertyListDataKeyName:originData[@"esf_name"],kPropertyListDataKeyTitle:originData[@"esf_title"],kPropertyListDataKeyImage:[UIImageimageWithUrlString:originData[@"esf_imageUrl"]]};}returnresultData;}
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):
---------------------------------------------------------------------------|||||PropertyListReformer.m||PropertyListReformer.h||||||#import PropertyListReformer.h | <------- |? #import "PropertyListReformerKeys.h" ||NSString*constkey=@"key"|||||||---------------------------------------------------------------------------./|\||||---------------------------------|||PropertyListReformerKeys.h||||externNSString*constkey;|||---------------------------------
使用Const字符串來表征Key嗓违,字符串的定義跟著reformer的實(shí)現(xiàn)文件走,字符串的extern聲明放在獨(dú)立的頭文件內(nèi)图贸。
這樣reformer生成的數(shù)據(jù)的key都使用Const字符串來表示蹂季,然后每次別的地方需要使用相關(guān)數(shù)據(jù)的時候,把PropertyListReformerKeys.h這個頭文件import進(jìn)去就好了疏日。
另外要注意的一點(diǎn)是偿洁,如果一個OriginData可能會被多個Reformer去處理的話,Key的命名規(guī)范需要能夠表征出其對應(yīng)的reformer名字制恍。如果reformer是PropertyListReformer父能,那么Key的名字就是PropertyListKeyXXXX。
這么做的好處就是净神,將來遷移的時候相當(dāng)方便何吝,只要扔頭文件就可以了,只扔頭文件是不會導(dǎo)致拔出蘿卜帶出泥的情況的鹃唯。而且也避免了自定義對象帶來的額外代碼體積爱榕。
另外,關(guān)于交付的NSDictionary坡慌,其實(shí)具體還是看view的需求黔酥,reformer的設(shè)計(jì)初衷是:通過reformer轉(zhuǎn)化出來的可以直接是View,或者是view直接可以使用的對象(包括NSDictionary)洪橘。比如地圖標(biāo)點(diǎn)列表API的數(shù)據(jù)跪者,通過reformer轉(zhuǎn)化之后就可以直接變成MKAnnotation,然后MKMapView就可以直接使用了熄求。這里說的只是當(dāng)你的需求是交付NSDictionary時渣玲,如何保證可讀性的情況,再強(qiáng)調(diào)一下哈弟晚,reformer交付的是view直接可以使用的對象忘衍,交付出去的可以是NSDictionary逾苫,也可以是UIView,跟DataSource結(jié)合之后交付的甚至可以是UITableViewCell/UICollectionViewCell枚钓。不要被NSDictionary或所謂的轉(zhuǎn)化成model再交付的思想局限铅搓。
綜上,我對交付什么樣的數(shù)據(jù)給業(yè)務(wù)層搀捷?這個問題的回答就是這樣:
對于業(yè)務(wù)層而言星掰,由Controller根據(jù)View和APIManager之間的關(guān)系,選擇合適的reformer將View可以直接使用的數(shù)據(jù)(甚至reformer可以用來直接生成view)轉(zhuǎn)化好之后交付給View指煎。對于網(wǎng)絡(luò)層而言蹋偏,只需要保持住原始數(shù)據(jù)即可,不需要主動轉(zhuǎn)化成數(shù)據(jù)原型至壤。然后數(shù)據(jù)采用NSDictionary加Const字符串key來表征,避免了使用對象來表征帶來的遷移困難枢纠,同時不失去可讀性像街。
集約型API調(diào)用方式和離散型API調(diào)用方式的選擇?
集約型API調(diào)用其實(shí)就是所有API的調(diào)用只有一個類晋渺,然后這個類接收API名字镰绎,API參數(shù),以及回調(diào)著陸點(diǎn)(可以是target-action木西,或者block畴栖,或者delegate等各種模式的著陸點(diǎn))作為參數(shù)。然后執(zhí)行類似startRequest這樣的方法八千,它就會去根據(jù)這些參數(shù)起飛去調(diào)用API了吗讶,然后獲得API數(shù)據(jù)之后再根據(jù)指定的著陸點(diǎn)去著陸。比如這樣:
集約型API調(diào)用方式:[APIRequeststartRequestWithApiName:@"itemList.v1"params:paramssuccess:@selector(success:)fail:@selector(fail:)target:self];
離散型API調(diào)用是這樣的恋捆,一個API對應(yīng)于一個APIManager照皆,然后這個APIManager只需要提供參數(shù)就能起飛,API名字沸停、著陸方式都已經(jīng)集成入APIManager中膜毁。比如這樣:
離散型API調(diào)用方式:@property(nonatomic,strong)ItemListAPIManager*itemListAPIManager;// getter-(ItemListAPIManager*)itemListAPIManager{if(_itemListAPIManager==nil){_itemListAPIManager=[[ItemListAPIManageralloc]init];_itemListAPIManager.delegate=self;}return_itemListAPIManager;}// 使用的時候就這么寫:[self.itemListAPIManagerloadDataWithParams:params];
集約型API調(diào)用和離散型API調(diào)用這兩者實(shí)現(xiàn)方案不是互斥的,單看下層愤钾,大家都是集約型瘟滨。因?yàn)榘l(fā)起一個API請求之后,除去業(yè)務(wù)相關(guān)的部分(比如參數(shù)和API名字等)能颁,剩下的都是要統(tǒng)一處理的:加密杂瘸,URL拼接,API請求的起飛和著陸劲装,這些處理如果不用集約化的方式來實(shí)現(xiàn)胧沫,作者非癲即癡昌简。然而對于整個網(wǎng)絡(luò)層來說,尤其是業(yè)務(wù)方使用的那部分绒怨,我傾向于提供離散型的API調(diào)用方式纯赎,并不建議在業(yè)務(wù)層的代碼直接使用集約型的API調(diào)用方式。原因如下:
原因1:當(dāng)前請求正在外面飛著的時候南蹂,根據(jù)不同的業(yè)務(wù)需求存在兩種不同的請求起飛策略:一個是取消新發(fā)起的請求犬金,等待外面飛著的請求著陸絮短。另一個是取消外面飛著的請求挎扰,讓新發(fā)起的請求起飛。集約化的API調(diào)用方式如果要滿足這樣的需求新翎,那么每次要調(diào)用的時候都要多寫一部分判斷和取消的代碼疗疟,手段就做不到很干凈该默。
前者的業(yè)務(wù)場景舉個例子就是刷新頁面的請求,刷新詳情策彤,刷新列表等栓袖。后者的業(yè)務(wù)場景舉個例子是列表多維度篩選,比如你先篩選了商品類型店诗,然后篩選了價格區(qū)間裹刮。當(dāng)然,后者的情況不一定每次篩選都要調(diào)用API庞瘸,我們先假設(shè)這種篩選每次都必須要通過調(diào)用API才能獲得數(shù)據(jù)捧弃。
如果是離散型的API調(diào)用,在編寫不同的APIManager時候就可以針對不同的API設(shè)置不同的起飛策略擦囊,在實(shí)際使用的時候违霞,就可以不必關(guān)心起飛策略了,因?yàn)锳PIMananger里面已經(jīng)寫好了霜第。
原因2:便于針對某個API請求來進(jìn)行AOP葛家。在集約型的API調(diào)用方式下,如果要針對某個API請求的起飛和著陸過程進(jìn)行AOP泌类,這代碼得寫成什么樣癞谒。。刃榨。噢弹砚,尼瑪這畫面太美別說看了,我都不敢想枢希。
原因3:當(dāng)API請求的著陸點(diǎn)消失時桌吃,離散型的API調(diào)用方式能夠更加透明地處理這種情況。
當(dāng)一個頁面的請求正在天上飛的時候苞轿,用戶等了好久不耐煩了茅诱,小手點(diǎn)了個back逗物,然后ViewController被pop被回收。此時請求的著陸點(diǎn)就沒了瑟俭。這是很危險(xiǎn)的情況翎卓,著陸點(diǎn)要是沒了,就很容易crash的摆寄。一般來說處理這個情況都是在dealloc的時候取消當(dāng)前頁面所有的請求失暴。如果是集約型的API調(diào)用,這個代碼就要寫到ViewController的dealloc里面微饥,但如果是離散型的API調(diào)用逗扒,這個代碼寫到APIManager里面就可以了,然后隨著ViewController的回收進(jìn)程欠橘,APIManager也會被跟著回收矩肩,這部分代碼就得到了調(diào)用的機(jī)會。這樣業(yè)務(wù)方在使用的時候就可以不必關(guān)心著陸點(diǎn)消失的情況了肃续,從而更加關(guān)注業(yè)務(wù)蛮拔。
原因4:離散型的API調(diào)用方式能夠最大程度地給業(yè)務(wù)方提供靈活性,比如reformer機(jī)制就是基于離散型的API調(diào)用方式的痹升。另外,如果是針對提供翻頁機(jī)制的API畦韭,APIManager就能簡單地提供loadNextPage方法去加載下一頁疼蛾,頁碼的管理就不用業(yè)務(wù)方去管理了。還有就是艺配,如果要針對業(yè)務(wù)請求參數(shù)進(jìn)行驗(yàn)證察郁,比如用戶填寫注冊信息,在離散型的APIManager里面實(shí)現(xiàn)就會非常輕松转唉。
綜上皮钠,關(guān)于集約型的API調(diào)用和離散型的API調(diào)用,我傾向于這樣:對外提供一個BaseAPIManager來給業(yè)務(wù)方做派生赠法,在BaseManager里面采用集約化的手段組裝請求麦轰,放飛請求,然而業(yè)務(wù)方調(diào)用API的時候砖织,則是以離散的API調(diào)用方式來調(diào)用款侵。如果你的App只提供了集約化的方式,而沒有離散方式的通道侧纯,那么我建議你再封裝一層新锈,便于業(yè)務(wù)方使用離散的API調(diào)用方式來放飛請求。
怎么做APIManager的繼承眶熬?
如果要做成離散型的API調(diào)用妹笆,那么使用繼承是逃不掉的块请。BaseAPIManager里面負(fù)責(zé)集約化的部分,外部派生的XXXAPIManager負(fù)責(zé)離散的部分拳缠,對于BaseAPIManager來說墩新,離散的部分有一些是必要的,比如API名字等脊凰,而我們派生的目的抖棘,也是為了提供這些數(shù)據(jù)。
我在這篇文章里面列舉了種種繼承的壞處狸涌,呼吁大家盡量不要使用繼承切省。但是現(xiàn)在到了不得不用繼承的時候,所以我得提醒一下大家別把繼承用壞了帕胆。
在APIManager的情況下朝捆,我們最直覺的思路是BaseAPIManager提供一些空方法來給子類做重載,比如apiMethodName這樣的函數(shù)懒豹,然而我的建議是芙盘,不要這么做。我們可以用IOP的方式來限制派生類的重載脸秽。
大概就是長這樣:
BaseAPIManager的init方法里這么寫:// 注意是weak儒老。@property(nonatomic,weak)idchild;-(instancetype)init{self=[superinit];if([selfconformsToProtocol:@protocol(APIManager)]){self.child=(id)self;}else{// 不遵守這個protocol的就讓他crash,防止派生類亂來记餐。NSAssert(NO,"子類必須要實(shí)現(xiàn)APIManager這個protocol驮樊。");}returnself;}protocol這么寫,把原本要重載的函數(shù)都定義在這個protocol里面片酝,就不用在父類里面寫空方法了:@protocolAPIManager@required-(NSString*)apiMethodName;...@end然后在父類里面如果要使用的話囚衔,就這么寫:[selfrequestWithAPIName:[self.childapiMethodName]......];
簡單說就是在init的時候檢查自己是否符合預(yù)先設(shè)計(jì)的子類的protocol,這就要求所有子類必須遵守這個protocol雕沿,所有針對父類的重載练湿、覆蓋也都以這個protocol為準(zhǔn),protocol以外的方法不允許重載审轮、覆蓋肥哎。而在父類的代碼里,可以不必遵守這個protocol断国,保持了未來維護(hù)的靈活性贤姆。
這么做的好處就是避免了父類寫空方法,同時也給子類帶上了緊箍咒:要想當(dāng)我的孩子稳衬,就要遵守這些規(guī)矩霞捡,不能亂來。業(yè)務(wù)方在實(shí)現(xiàn)子類的時候薄疚,就可以根據(jù)protocol中的方法去一一實(shí)現(xiàn)碧信,然后約定就比較好做了:不允許重載父類方法赊琳,只允許選擇實(shí)現(xiàn)或不實(shí)現(xiàn)protocol中的方法。
關(guān)于這個的具體的論述在這篇文章里面有砰碴,感興趣的話可以看看躏筏。
網(wǎng)絡(luò)層與業(yè)務(wù)層對接部分的小總結(jié)
這一節(jié)主要是講了以下這些點(diǎn):
使用delegate來做數(shù)據(jù)對接,僅在必要時采用Notification來做跨層訪問
交付NSDictionary給業(yè)務(wù)層呈枉,使用Const字符串作為Key來保持可讀性
提供reformer機(jī)制來處理網(wǎng)絡(luò)層反饋的數(shù)據(jù)趁尼,這個機(jī)制很重要,好處極多
網(wǎng)絡(luò)層上部分使用離散型設(shè)計(jì)猖辫,下部分使用集約型設(shè)計(jì)
設(shè)計(jì)合理的繼承機(jī)制酥泞,讓派生出來的APIManager受到限制,避免混亂
應(yīng)該不止這5點(diǎn)...
網(wǎng)絡(luò)層的安全機(jī)制
判斷API的調(diào)用請求是來自于經(jīng)過授權(quán)的APP
使用這個機(jī)制的目的主要有兩點(diǎn):
確保API的調(diào)用者是來自你自己的APP啃憎,防止競爭對手爬你的API
如果你對外提供了需要注冊才能使用的API平臺芝囤,那么你需要有這個機(jī)制來識別是否是注冊用戶調(diào)用了你的API
解決方案:設(shè)計(jì)簽名
要達(dá)到第一個目的其實(shí)很簡單,服務(wù)端需要給你一個密鑰辛萍,每次調(diào)用API時悯姊,你使用這個密鑰再加上API名字和API請求參數(shù)算一個hash出來,然后請求的時候帶上這個hash贩毕。服務(wù)端收到請求之后悯许,按照同樣的密鑰同樣的算法也算一個hash出來,然后跟請求帶來的hash做一個比較辉阶,如果一致岸晦,那么就表示這個API的調(diào)用者確實(shí)是你的APP。為了不讓別人也獲取到這個密鑰睛藻,你最好不要把這個密鑰存儲在本地,直接寫死在代碼里面就好了邢隧。另外適當(dāng)增加一下求Hash的算法的復(fù)雜度店印,那就是各種Hash算法(比如MD5)加點(diǎn)鹽,再回爐跑一次Hash啥的倒慧。這樣就能解決第一個目的了:確保你的API是來自于你自己的App按摘。
一般情況下大部分公司不會出現(xiàn)需要滿足第二種情況的需求,除非公司開發(fā)了自己的API平臺給第三方使用纫谅。這個需求跟上面的需求有一點(diǎn)不同:符合授權(quán)的API請求者不只是一個炫贤。所以在這種情況下,需要的安全機(jī)制會更加復(fù)雜一點(diǎn)付秕。
這里有一個較容易實(shí)現(xiàn)的方案:客戶端調(diào)用API的時候兰珍,把自己的密鑰通過一個可逆的加密算法加密后連著請求和加密之后的Hash一起送上去。當(dāng)然询吴,這個可逆的加密算法肯定是放在在調(diào)用API的SDK里面掠河,編譯好的亮元。然后服務(wù)端拿到加密后的密鑰和加密的Hash之后,解碼得到原始密鑰唠摹,然后再用它去算Hash爆捞,最后再進(jìn)行比對。
保證傳輸數(shù)據(jù)的安全
使用這個機(jī)制的主要目的有兩點(diǎn):
防止中間人攻擊勾拉,比如說運(yùn)營商很喜歡往用戶的Http請求里面塞廣告...
SPDY依賴于HTTPS煮甥,而且是未來HTTP/2的基礎(chǔ),他們能夠提高你APP在網(wǎng)絡(luò)層整體的性能藕赞。
解決方案:HTTPS
目前使用HTTPS的主要目的在于防止運(yùn)營商往你的Response Data里面加廣告啥的(中間人攻擊)成肘,面對的威脅范圍更廣。從2011年開始找默,國外業(yè)界就已經(jīng)提倡所有的請求(不光是API艇劫,還有網(wǎng)站)都走HTTPS,國內(nèi)差不多晚了兩年(2013年左右)才開始提倡這事惩激,天貓是這兩個月才開始做HTTPS的全APP遷移店煞。
關(guān)于速度,HTTPS肯定是比HTTP慢的风钻,畢竟多了一次握手顷蟀,但掛上SPDY之后,有了鏈接復(fù)用骡技,這方面的性能就有了較大提升鸣个。這里的性能提升并不是說一個請求原來要500ms能完成,然后現(xiàn)在只要300ms布朦,這是不對的囤萤。所謂整體性能是基于大量請求去討論的:同樣的請求量(假設(shè)100個)在短期發(fā)生時,掛上SPDY之后完成這些任務(wù)所要花的時間比不用SPDY要少是趴。SPDY還有Header壓縮的功能涛舍,不過因?yàn)橐粋€API請求本身已經(jīng)比較小了,壓縮數(shù)據(jù)量所帶來的性能提升不會特別明顯唆途,所以就單個請求來看富雅,性能的提升是比較小的。不過這是下一節(jié)要討論的事兒了肛搬,這兒只是順帶說一下没佑。
安全機(jī)制小總結(jié)
這一節(jié)說了兩種安全機(jī)制,一般來說第一種是標(biāo)配温赔,第二種屬于可選配置蛤奢。不過隨著我國互聯(lián)網(wǎng)基礎(chǔ)設(shè)施的完善,移動設(shè)備性能的提高,以及優(yōu)化技術(shù)的提高远剩,第二種配置的缺點(diǎn)(速度慢)正在越來越微不足道扣溺,因此HTTPS也會成為不久之后的未來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)化手段主要從以下三方面考慮:
針對鏈接建立環(huán)節(jié)的優(yōu)化
針對鏈接傳輸數(shù)據(jù)量的優(yōu)化
針對鏈接復(fù)用的優(yōu)化
這三方面是所有優(yōu)化手段的內(nèi)容痢掠,各種五花八門的優(yōu)化手段基本上都不會逃脫這三方面驱犹,下面我就會分別針對這三方面講一下各自對應(yīng)的優(yōu)化手段。
1. 針對鏈接建立環(huán)節(jié)的優(yōu)化
在API發(fā)起請求建立鏈接的環(huán)節(jié)足画,大致會分這些步驟:
發(fā)起請求
DNS域名解析得到IP
根據(jù)IP進(jìn)行三次握手(HTTPS四次握手)雄驹,鏈接建立成功
其實(shí)第三步的優(yōu)化手段跟第二步的優(yōu)化手段是一致的,我會在講第二步的時候一起講掉淹辞。
1.1 針對發(fā)起請求的優(yōu)化手段
其實(shí)要解決的問題就是網(wǎng)絡(luò)層該不該為此API調(diào)用發(fā)起請求医舆。
1.1.1 使用緩存手段減少請求的發(fā)起次數(shù)
對于大部分API調(diào)用請求來說,有些API請求所帶來的數(shù)據(jù)的時效性是比較長的象缀,比如商品詳情蔬将,比如App皮膚等。那么我們就可以針對這些數(shù)據(jù)做本地緩存央星,這樣下次請求這些數(shù)據(jù)的時候就可以不必再發(fā)起新的請求霞怀。
一般是把API名字和參數(shù)拼成一個字符串然后取MD5作為key,存儲對應(yīng)返回的數(shù)據(jù)莉给。這樣下次有同樣請求的時候就可以直接讀取這里面的數(shù)據(jù)毙石。關(guān)于這里有一個緩存策略的問題需要討論:什么時候清理緩存?要么就是根據(jù)超時時間限制進(jìn)行清理颓遏,要么就是根據(jù)緩存數(shù)據(jù)大小進(jìn)行清理徐矩。這個策略的選擇要根據(jù)具體App的操作日志來決定。
比如安居客App叁幢,日志數(shù)據(jù)記錄顯示用戶平均使用時長不到3分鐘丧蘸,但是用戶查看房源詳情的次數(shù)比較多,而房源詳情數(shù)據(jù)量較大遥皂。那么這個時候,就適合根據(jù)使用時長來做緩存刽漂,我當(dāng)時給安居客設(shè)置的緩存超時時間就是3分鐘演训,這樣能夠保證這個緩存能夠在大部分用戶使用時間產(chǎn)生作用。嗯贝咙,極端情況下做什么緩存手段不考慮样悟,只要能夠服務(wù)好80%的用戶就可以了,而且針對極端情況采用的優(yōu)化手段對大部分普通用戶而言是不必要的,做了反而會對他們有影響窟她。
再比如網(wǎng)絡(luò)圖片緩存陈症,數(shù)據(jù)量基本上都特別大,這種就比較適合針對緩存大小來清理緩存的策略震糖。
另外录肯,之前的緩存的前提都是基于內(nèi)存的。我們也可以把需要清理的緩存存儲在硬盤上(APP的本地存儲吊说,我就先用硬盤來表示了论咏,雖然很少有手機(jī)硬盤的說法,哈哈)颁井,比如前面提到的圖片緩存厅贪,因?yàn)閳D片很有可能在很長時間之后,再被顯示的雅宾,那么原本需要被清理的圖片緩存养涮,我們就可以考慮存到硬盤上去。當(dāng)下次再有顯示網(wǎng)絡(luò)圖片的需求的時候眉抬,我們可以先從內(nèi)存中找贯吓,內(nèi)存找不到那就從硬盤上找,這都找不到吐辙,那就發(fā)起請求吧宣决。
當(dāng)然,有些時效性非常短的API數(shù)據(jù)昏苏,就不能使用這個方法了尊沸,比如用戶的資金數(shù)據(jù),那就需要每次都調(diào)用了贤惯。
1.1.2 使用策略來減少請求的發(fā)起次數(shù)
這個我在前面提到過洼专,就是針對重復(fù)請求的發(fā)起和取消,是有對應(yīng)的請求策略的孵构。我們先說取消策略屁商。
如果是界面刷新請求這種,而且存在重復(fù)請求的情況(下拉刷新時颈墅,在請求著陸之前用戶不斷執(zhí)行下拉操作)蜡镶,那么這個時候,后面重復(fù)操作導(dǎo)致的API請求就可以不必發(fā)送了恤筛。
如果是條件篩選這種官还,那就取消前面已經(jīng)發(fā)送的請求。雖然很有可能這個請求已經(jīng)被執(zhí)行了毒坛,那么取消所帶來的性能提升就基本沒有了望伦。但如果這個請求還在隊(duì)列中待執(zhí)行的話林说,那么對應(yīng)的這次鏈接就可以省掉了。
以上是一種屯伞,另外一種情況就是請求策略:類似用戶操作日志的請求策略腿箩。
用戶操作會觸發(fā)操作日志上報(bào)Server,這種請求特別頻繁劣摇,但是是暗地里進(jìn)行的珠移,不需要用戶對此有所感知。所以也沒必要操作一次就發(fā)起一次的請求饵撑。在這里就可以采用這樣的策略:在本地記錄用戶的操作記錄剑梳,當(dāng)記錄滿30條的時候發(fā)起一次請求將操作記錄上傳到服務(wù)器。然后每次App啟動的時候滑潘,上傳一次上次遺留下來沒上傳的操作記錄垢乙。這樣能夠有效降低用戶設(shè)備的耗電量,同時提升網(wǎng)絡(luò)層的性能语卤。
小總結(jié)
針對建立連接這部分的優(yōu)化就是這樣的原則:能不發(fā)請求的就盡量不發(fā)請求追逮,必須要發(fā)請求時,能合并請求的就盡量合并請求粹舵。然而钮孵,任何優(yōu)化手段都是有前提的,而且也不能保證對所有需求都能起作用眼滤,有些API請求就是不符合這些優(yōu)化手段前提的巴席,那就老老實(shí)實(shí)發(fā)請求吧。不過這類API請求所占比例一般不大诅需,大部分的請求都或多或少符合優(yōu)化條件漾唉,所以針對發(fā)送請求的優(yōu)化手段還是值得做的。
1.2 & 1.3 針對DNS域名解析做的優(yōu)化堰塌,以及建立鏈接的優(yōu)化
其實(shí)在整個DNS鏈路上也是有DNS緩存的赵刑,理論上也是能夠提高速度的。這個鏈路上的DNS緩存在PC用戶上效果明顯场刑,因?yàn)镻C用戶的DNS鏈路相對穩(wěn)定般此,信號源不會變來變?nèi)ァ5窃谝苿釉O(shè)備的用戶這邊牵现,鏈路上的DNS緩存所帶來的性能提升就不太明顯了铐懊。因?yàn)橐苿釉O(shè)備的實(shí)際使用場景比較復(fù)雜,網(wǎng)絡(luò)信號源會經(jīng)常變換瞎疼,信號源每變換一次科乎,對應(yīng)的DNS解析鏈路就會變換一次,那么原鏈路上的DNS緩存就不起作用了丑慎。而且信號源變換的情況特別特別頻繁,所以對于移動設(shè)備用戶來說,鏈路的DNS緩存我們基本上可以默認(rèn)為沒有竿裂。所以大部分時間是手機(jī)系統(tǒng)自帶的本地DNS緩存在起作用玉吁,但是一般來說,移動設(shè)備上網(wǎng)的需求也特別頻繁腻异,專門為我們這個App所做的DNS緩存很有可能會被別的DNS緩存給擠出去被清理掉进副,這種情況是特別多的,用戶看一會兒知乎刷一下微博查一下地圖逛一逛點(diǎn)評再聊個Q悔常,回來之后很有可能屬于你自己的App的本地DNS緩存就沒了影斑。這還沒完,這里還有一個只有在中國特色社會主義的互聯(lián)網(wǎng)環(huán)境中才會有的問題:國內(nèi)的互聯(lián)網(wǎng)環(huán)境由于GFW的存在机打,就使得DNS服務(wù)速度會比正常情況慢不少矫户。
基于以上三個原因所導(dǎo)致的最終結(jié)果就是,API請求在DNS解析階段的耗時會很多残邀。
那么針對這個的優(yōu)化方案就是皆辽,索性直接走IP請求,那不就繞過DNS服務(wù)的耗時了嘛芥挣。
另外一個驱闷,就是上面提到的建立鏈接時候的第三步,國內(nèi)的網(wǎng)絡(luò)環(huán)境分北網(wǎng)通南電信(當(dāng)然實(shí)際情況更復(fù)雜空免,這里隨便說說)空另,不同服務(wù)商之間的連接,延時是很大的蹋砚,我們需要想辦法讓用戶在最適合他的IP上給他提供服務(wù)扼菠,那么就針對我們繞過DNS服務(wù)的手段有一個額外要求:盡可能不要讓用戶使用對他來說很慢的IP。
所以綜上所述都弹,方案就應(yīng)該是這樣:本地有一份IP列表娇豫,這些IP是所有提供API的服務(wù)器的IP,每次應(yīng)用啟動的時候畅厢,針對這個列表里的所有IP取ping延時時間冯痢,然后取延時時間最小的那個IP作為今后發(fā)起請求的IP地址。
針對建立連接的優(yōu)化手段其實(shí)是跟DNS域名解析的優(yōu)化手段是一樣的框杜。不過這需要你的服務(wù)器提供服務(wù)的網(wǎng)絡(luò)情況要多浦楣,一般現(xiàn)在的服務(wù)器都是雙網(wǎng)卡,電信和網(wǎng)通咪辱。由于中國特色的互聯(lián)網(wǎng)ISP分布振劳,南北網(wǎng)絡(luò)之間存在瓶頸,而我們App針對鏈接的優(yōu)化手段主要就是著手于如何減輕這個瓶頸對App產(chǎn)生的影響油狂,所以需要維護(hù)一個IP列表历恐,這樣就能就近連接了寸癌,就起到了優(yōu)化的效果。
我們一般都是在應(yīng)用啟動的時候獲得本地列表中所有IP的ping值弱贼,然后通過NSURLProtocol的手段將URL中的HOST修改為我們找到的最快的IP蒸苇。另外,這個本地IP列表也會需要通過一個API來維護(hù)吮旅,一般是每天第一次啟動的時候讀一次API溪烤,然后更新到本地。
如果你還不熟悉NSURLProtocol應(yīng)該怎么玩庇勃,看完官方文檔和這篇文章以及這個Demo之后檬嘀,你肯定就會了,其實(shí)很簡單的责嚷。另外鸳兽,剛才提到那篇文章的作者(mattt)還寫了這個基于NSURLProtocol的工具,相當(dāng)好用再层,是可以直接拿來集成到項(xiàng)目中的贸铜。
不用NSURLProtocol的話,用其他手段也可以做到這一點(diǎn)聂受,但那些手段未免又比較愚蠢蒿秦。
2. 針對鏈接傳輸數(shù)據(jù)量的優(yōu)化
這個很好理解,傳輸?shù)臄?shù)據(jù)少了蛋济,那么自然速度就上去了棍鳖。這里沒什么花樣可以講的,就是壓縮唄碗旅。各種壓縮渡处。
3. 針對鏈接復(fù)用的優(yōu)化
建立鏈接本身是屬于比較消耗資源的操作,耗電耗時祟辟。SPDY自帶鏈接復(fù)用以及數(shù)據(jù)壓縮的功能医瘫,所以服務(wù)端支持SPDY的時候,App直接掛SPDY就可以了旧困。如果服務(wù)端不支持SPDY醇份,也可以使用PipeLine,蘋果原生自帶這個功能吼具。
一般來說業(yè)界內(nèi)普遍的認(rèn)識是SPDY優(yōu)于PipeLine僚纷,然后即便如此,SPDY能夠帶來的網(wǎng)絡(luò)層效率提升其實(shí)也沒有文獻(xiàn)上的圖表那么明顯拗盒,但還是有性能提升的怖竭。還有另外一種比較笨的鏈接復(fù)用的方法,就是維護(hù)一個隊(duì)列陡蝇,然后將隊(duì)列里的請求壓縮成一個請求發(fā)出去痊臭,之所以會存在滯留在隊(duì)列中的請求哮肚,是因?yàn)樵谏弦粋€請求還在外面飄的時候。這種做法最終的效果表面上看跟鏈接復(fù)用差別不大广匙,但并不是真正的鏈接復(fù)用绽左,只能說是請求合并。
還是說回來艇潭,我建議最好是用SPDY,SPDY和pipeline雖然都屬于鏈接復(fù)用的范疇戏蔑,但是pipeline并不是真正意義上的鏈接復(fù)用蹋凝,SPDY的鏈接復(fù)用相對pipeline而言更為徹底。SPDY目前也有現(xiàn)成的客戶端SDK可以使用总棵,一個是twitter的CocoaSPDY鳍寂,另一個是Voxer/iSPDY,這兩個庫都很活躍情龄,大家可以挑合適的采用迄汛。
不過目前業(yè)界趨勢是傾向于使用HTTP/2.0來代替SPDY,不過目前HTTP/2.0還沒有正式出臺骤视,相關(guān)實(shí)現(xiàn)大部分都處在demo階段鞍爱,所以我們還是先SPDY搞起就好了。未來很有可能會放棄SPDY专酗,轉(zhuǎn)而采用HTTP/2.0來實(shí)現(xiàn)網(wǎng)絡(luò)的優(yōu)化睹逃。這是要提醒各位架構(gòu)師注意的事情。嗯祷肯,我也不知道HTTP/2.0什么時候能出來沉填。
漁說完了,魚來了
這里是我當(dāng)年設(shè)計(jì)并實(shí)現(xiàn)的安居客的網(wǎng)絡(luò)層架構(gòu)代碼佑笋。當(dāng)然翼闹,該脫敏的地方我都已經(jīng)脫敏了,所以編不過是正常的蒋纬,哈哈哈猎荠。但是代碼比較齊全,重要地方注釋我也寫了很多颠锉。另外法牲,為了讓大家能夠把這些代碼看明白,我還附帶了當(dāng)年介紹這個框架演講時的PPT琼掠。(補(bǔ)充說明一下拒垃,評論區(qū)好多人問PPT找不著在哪兒,PPT也在上面提到的repo里面瓷蛙,是個key后綴名的文件悼瓮,用keynote打開)
然后就是戈毒,當(dāng)年也有很多問題其實(shí)考慮得并沒有現(xiàn)在清楚,所以有些地方還是做得不夠好横堡,比如攔截器和繼承埋市。而且當(dāng)時的優(yōu)化手段只有本地cache,安居客沒有那么多IP可以給我ping命贴,當(dāng)年也沒流行SPDY道宅,而且API也還不支持HTTPS,所以當(dāng)時的代碼里面沒有在這些地方做優(yōu)化胸蛛,比較原始污茵。然而整個架構(gòu)的基本思路一直沒有變化:優(yōu)先服務(wù)于業(yè)務(wù)方。另外葬项,安居客的網(wǎng)絡(luò)層多了一個service的概念泞当,這是我這篇文章中沒有講的。主要是因?yàn)榘簿涌偷腁PI提供方很多民珍,二手房襟士,租房,新房嚷量,X項(xiàng)目等等API都是不同的API team提供的陋桂,以service作區(qū)分,如果你的app也是類似的情況蝶溶,我也建議你設(shè)計(jì)一套service機(jī)制≌潞恚現(xiàn)在這些service被我刪得只剩下一個google的service,因?yàn)槠渌鹲ervice都屬于敏感內(nèi)容身坐。
另外秸脱,這里面提供的PPT我很希望大家能夠花時間去看看,在PPT里面有些更加細(xì)的東西我在博客里沒有寫部蛇,主要是我比較懶摊唇,然后這篇文章拖的時間比較長了,花時間搬運(yùn)這個沒什么意思涯鲁,不過內(nèi)容還是值得各位讀者去看的巷查。關(guān)于PPT里面大家有什么問題的,也可以在評論區(qū)問抹腿,我都會回答岛请。
總結(jié)
第一部分主要講了網(wǎng)絡(luò)層應(yīng)當(dāng)如何跟業(yè)務(wù)層進(jìn)行數(shù)據(jù)交互,進(jìn)行數(shù)據(jù)交互時采用怎樣的數(shù)據(jù)格式警绩,以及設(shè)計(jì)時代碼結(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é)的事情也還會有很多舅逸,比如做一些代碼混淆,盡可能避免代碼中明文展示key皇筛。不過大頭主要就是這兩個,而且也都是需要服務(wù)端同學(xué)去配合的坠七。主要偏重于介紹水醋。(主要是也沒啥好實(shí)踐的,google一下教程照著來就好了)彪置。
第三部分講了優(yōu)化拄踪,優(yōu)化的所有方面都已經(jīng)列出來了,如果業(yè)界再有七七八八的別的手段拳魁,也基本逃離不出本文的范圍惶桐。這里有些優(yōu)化手段是需要服務(wù)端同學(xué)配合的,有些不需要潘懊,大家看各自情況來決定姚糊。主要偏重于實(shí)踐。
最后給出了我之前在安居客做的網(wǎng)絡(luò)層架構(gòu)的主要代碼圾亏,以及當(dāng)時演講時的PPT肺然。關(guān)于代碼或PPT中有任何問題强窖,都可以在評論區(qū)問我。
這一篇文章出得比較晚肠槽,因?yàn)楣镜氖虑椋虚g間隔了一個禮拜奢啥,希望大家諒解秸仙。另外,隔了一個禮拜之后我再寫桩盲,發(fā)現(xiàn)有些地方我已經(jīng)想不起來當(dāng)初是應(yīng)該怎么行文下去的了寂纪,然后發(fā)之前我把文章又看了幾遍,盡可能把斷片的地方抹平了赌结,如果大家讀起來有什么地方感覺奇怪的弊攘,或者講到一半就沒了的抢腐,那應(yīng)該就是斷片了。在評論區(qū)跟我說一下襟交,我補(bǔ)上去迈倍。
然后如果有需要勘誤的地方,也請?jiān)谠u論區(qū)指出捣域,幫助我把錯的地方訂正回來啼染,如果有沒講到的地方,但你又特別想要了解的焕梅,也可以在評論區(qū)提出來迹鹅,我會補(bǔ)上去。說不定看完之后你腦袋里還會有很多個問號贞言,也請?jiān)谠u論區(qū)問出來哈斜棚,說不定別人也有跟你一樣的問題,他就能在評論區(qū)找到答案了该窗。
在第二篇文章的評論區(qū)里面出現(xiàn)了噴子弟蚀,遇到這種情況我怎么可能刪帖呢?那根本就不是我的風(fēng)格哇酗失,哈哈哈义钉。我肯定是會噴回去的,并且還會把鏈接傳播給周圍人规肴,發(fā)動周圍朋友來看:"快看捶闸,這兒有2B,哈哈哈"拖刃。
嗯删壮,所以評論的時候你一定要想清楚哈,我寫代碼的實(shí)力不差兑牡,打嘴仗的實(shí)力那可比寫代碼強(qiáng)多了醉锅。評論區(qū)同樣歡迎切磋。
有任何問題建議直接在評論區(qū)提問发绢,這樣后來的人如果有相同的問題硬耍,就能直接找到答案了。提問之前也可以先看看評論區(qū)有沒有人問過類似問題了边酒。
所有評論和問題我都會在第一時間回復(fù)经柴,QQ上我是不回答問題的哈。