與時(shí)俱進(jìn),HTTP/2下的iOS網(wǎng)絡(luò)層架構(gòu)設(shè)計(jì)

0 引言

HTTP/2,HTTP協(xié)議的第二個(gè)主要版本,是HTTP協(xié)議自1999年HTTP1.1發(fā)布后的首個(gè)更新。于2015年2月17日被批準(zhǔn)后,標(biāo)準(zhǔn)也于2015年5月以RFC 7540正式發(fā)表(來自維基百科)。

HTTP/2,采用了一系列優(yōu)化技術(shù)來整體提升HTTP協(xié)議的傳輸性能,如異步連接復(fù)用、頭壓縮等等,可謂是當(dāng)前互聯(lián)網(wǎng)應(yīng)用開發(fā)中,網(wǎng)絡(luò)層次架構(gòu)優(yōu)化的必選方案之一。Apple對(duì)于HTTP/2的態(tài)度也非常積極,5月HTTP/2正式發(fā)表后不久,便在緊接著6月召開的WWDC 2015大會(huì)中,向全球開發(fā)者宣布,iOS 9 開始支持HTTP/2。

好東西自然人人都想用。然而盡管Apple早早地宣布支持HTTP/2,但是現(xiàn)在整個(gè)技術(shù)圈內(nèi)提及的iOS網(wǎng)絡(luò)層架構(gòu)設(shè)計(jì)還大多數(shù)停留在HTTP 1.1時(shí)代,并沒有一個(gè)與時(shí)俱進(jìn)的、包含HTTP/2優(yōu)化的網(wǎng)絡(luò)層架構(gòu)設(shè)計(jì)策略。對(duì)于架構(gòu)設(shè)計(jì),我在《餓了么移動(dòng)APP的架構(gòu)演進(jìn)》中說過,脫離業(yè)務(wù)談架構(gòu)就是純粹的耍流氓;因此,架構(gòu)的設(shè)計(jì)一定要結(jié)合當(dāng)前的業(yè)務(wù)需求來進(jìn)行設(shè)計(jì)和規(guī)劃,并且做好一定的可擴(kuò)展性,以應(yīng)對(duì)未來的變化。

本文會(huì)結(jié)合當(dāng)前的業(yè)務(wù)談?wù)勔韵聨讉€(gè)方面內(nèi)容:
1、如何在iOS下使用HTTP/2?
2、如何設(shè)計(jì)一個(gè)iOS的網(wǎng)絡(luò)層架構(gòu)?
3、與時(shí)俱進(jìn)下,我們的解決方案?

1 HTTP/2下的iOS網(wǎng)絡(luò)庫

移動(dòng)端的APP,網(wǎng)絡(luò)層是一個(gè)幾乎完全不可或缺的角色。而也正是在網(wǎng)絡(luò)層,由于不同家的業(yè)務(wù)模型、業(yè)務(wù)結(jié)構(gòu)不一樣,使得網(wǎng)絡(luò)層的架構(gòu)呈現(xiàn)一種百家爭鳴的局面。另一方面,Apple對(duì)網(wǎng)絡(luò)層的API也是有比較好的封裝;即使你不是太熟悉Apple的網(wǎng)絡(luò)API,使用業(yè)界流行的AFNetworking或者ASIHttpRequest也是可以簡化不少的操作。不過后者的作者已經(jīng)多年不維護(hù)了,因此AFNetworking基本上已經(jīng)成為了iOS APP的標(biāo)配。

Apple在CocoaTouch層基于CFNetworking庫提供的網(wǎng)絡(luò)API有兩個(gè)大類:NSURLConnection和NSURLSession。后者從iOS7開始出現(xiàn),并宣稱是NSURLConnection的替代者。隨著時(shí)間的推移,WWDC 2015的召開也正式宣布了iOS9中NSURLConnection的deprecated標(biāo)注,完成了其歷史使命,也兌現(xiàn)了之前的承諾。不過在實(shí)際的開發(fā)過程中,我們可以發(fā)現(xiàn),盡管標(biāo)注了deprecated,并不意味著NSURLConnection的庫不可以繼續(xù)使用;而且,由于習(xí)慣性問題,還是有很多的工程師仍舊執(zhí)著于NSURLConnection所帶來的熟悉味道;特別是使用AFNetworking庫的工程師們,由于AFNetworking對(duì)于NSURLConnection的封裝非常精美,并且可以根據(jù)業(yè)務(wù)需要自定義添加相應(yīng)的依賴關(guān)系,使得其在實(shí)際應(yīng)用中讓人感到無比的“舒服”。

然而,魚與熊掌總是不可兼得。WWDC 2015 Session711 告訴我們,從 iOS 9 才開始支持的 HTTP / 2 協(xié)議只能在 NSURLSession 中使用。這也就意味著,要想進(jìn)化到 HTTP / 2 就不得不舍棄陪伴我們多年的 NSURLConnection ,而且還要將設(shè)計(jì)網(wǎng)絡(luò)層架構(gòu)的思維方式調(diào)整到 NSURLSession 上來。不過慶幸的是,AFNetworking從2.0開始也提供了NSURLSession的實(shí)現(xiàn)版本,并且相關(guān)的 API 并沒有太大的變動(dòng),對(duì)于一般性的遷移還是能夠輕松應(yīng)對(duì);并且,從AFNetworking 3.0開始,正式拋棄NSURLConnection,全面投入NSURLSession的懷抱。

如果在您的網(wǎng)絡(luò)層設(shè)計(jì)中,采用了AFNetworking來降低設(shè)計(jì)的復(fù)雜性,那么正如前面提到的,由于兩者在 API 方面并沒有太大的差異,因此在一般的網(wǎng)絡(luò)層遷移過程中可以平滑地過渡。而如果你在網(wǎng)絡(luò)層設(shè)計(jì)中直接采用了原生的 API,也不需要擔(dān)心,因?yàn)?NSURLSession 的 API 被設(shè)計(jì)的更加美妙,也更加易用。不過,盡管NSURLSession有非常多值得稱贊的地方,但是它畢竟是一種新的設(shè)計(jì)思想和理念。因此在實(shí)際的使用過程中我們會(huì)發(fā)現(xiàn)很多與之前設(shè)計(jì)思維相抵觸的地方,尤其是在網(wǎng)絡(luò)依賴性處理上,就連AFNetworking也做得不是太好,這點(diǎn)我們?cè)诤竺娴恼鹿?jié)中還會(huì)再次提到。

綜合以上而言,技術(shù)的腳步永遠(yuǎn)向前,僅憑 HTTP / 2 這一點(diǎn),我們相信 Apple 也一定會(huì)把重心向 NSURLSession 偏移,繼續(xù)優(yōu)化她,而我們也要跟上歷史的車輪,是時(shí)候讓遲暮的 NSURLConnection 休息了。

2 iOS網(wǎng)絡(luò)層的架構(gòu)設(shè)計(jì)

架構(gòu)的設(shè)計(jì)總是和業(yè)務(wù)的發(fā)展相結(jié)合和適應(yīng)的。在餓了么移動(dòng)多款A(yù)pp的發(fā)展過程中,由于不同業(yè)務(wù)的差異性導(dǎo)致接口、協(xié)議等都有不同的需求,給多款A(yù)pp設(shè)計(jì)出一個(gè)擁有干凈API和高度內(nèi)耦合的網(wǎng)絡(luò)層成為了一項(xiàng)挑戰(zhàn),而這個(gè)設(shè)計(jì)也將直接影響我們APP業(yè)務(wù)工程師們的開發(fā)效率。這節(jié)主要闡述理論,拋出一些問題。在下一節(jié)會(huì)給出結(jié)合餓了么多款A(yù)PP的業(yè)務(wù)下所設(shè)計(jì)的網(wǎng)絡(luò)層的解決方案。

本節(jié)我們主要討論兩點(diǎn):

  1. 與業(yè)務(wù)相結(jié)合的網(wǎng)絡(luò)層設(shè)計(jì)
  2. 與安全相關(guān)的網(wǎng)絡(luò)層設(shè)計(jì)

與業(yè)務(wù)相結(jié)合的網(wǎng)絡(luò)層設(shè)計(jì)

先來看與業(yè)務(wù)相結(jié)合的網(wǎng)絡(luò)層設(shè)計(jì)。

與業(yè)務(wù)相連接最緊密的地方必然是輸入與輸出,而網(wǎng)絡(luò)層的功能無疑是接受輸入的數(shù)據(jù),挑選一個(gè)相應(yīng)的通道組裝數(shù)據(jù)發(fā)送給服務(wù)器,然后將服務(wù)器返回的數(shù)據(jù)返回給上一層,即輸出數(shù)據(jù)。接下來我們從數(shù)據(jù)的角度來看看在網(wǎng)絡(luò)層設(shè)計(jì)中需要考慮些什么樣的問題。這里我們會(huì)從以下三個(gè)方面來進(jìn)行闡述:

  • 數(shù)據(jù)輸入
  • 數(shù)據(jù)回調(diào)
  • 數(shù)據(jù)轉(zhuǎn)換

數(shù)據(jù)輸入

首先是輸入過程。業(yè)務(wù)數(shù)據(jù)調(diào)用網(wǎng)絡(luò)層接口時(shí)可以稱之為輸入,這里一般會(huì)有兩種形式的設(shè)計(jì)。

第一種比較常見,很多時(shí)候會(huì)被稱為集中式的API處理,即將一些經(jīng)常使用的網(wǎng)絡(luò)層調(diào)用的代碼封裝成一到兩個(gè)函數(shù)供上層調(diào)用。上層輸入相關(guān)的參數(shù)便能取得相關(guān)的回調(diào)。如以下函數(shù):

+ (void)networkTransferWithURLString:(NSString *)urlString
                       andParameters:(NSDictionary *)parameters
                              isPOST:(BOOL)isPost
                        transferType:(NETWORK_TRANSFER_TYPE)transferType
                   andSuccessHandler:(void (^)(id responseObject))successHandler
                   andFailureHandler:(void (^)(NSError *error))failureHandler {
                   // 封裝AFN
                   }

另一種形式的設(shè)計(jì),則采用一種繼承形式設(shè)計(jì)每一個(gè)API,而每一個(gè)API都對(duì)應(yīng)一個(gè)類,這個(gè)類中將該API的所有參數(shù)都設(shè)定好,并提供“開始”接口和“返回”的Block,很多時(shí)候我們稱這種為分布式的API處理。一個(gè)比較通用的BaseAPI可以有下列可配置項(xiàng):

typedef NS_ENUM(NSUInteger, DRDRequestMethodType) {
    DRDRequestMethodTypeGET     = 0,
    DRDRequestMethodTypePOST    = 1,
    DRDRequestMethodTypeHEAD    = 2,
    DRDRequestMethodTypePUT     = 3,
    DRDRequestMethodTypePATCH   = 4,
    DRDRequestMethodTypeDELETE  = 5
};

@interface DRDBaseAPI : NSObject
@property (nonatomic, copy, nullable) NSString *baseUrl;
@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject,  NSError * _Nullable error);

- (DRDRequestMethodType)apiRequestMethodType;
- (DRDRequestSerializerType)apiRequestSerializerType;
- (DRDResponseSerializerType)apiResponseSerializerType;
- (void)start;
- (void)cancel;

...

@end

每一個(gè)具體的API都可以繼承自這個(gè)BaseAPI,當(dāng)上層業(yè)務(wù)需要進(jìn)行網(wǎng)絡(luò)調(diào)用時(shí),實(shí)例化一個(gè)需要調(diào)用的API接口,對(duì)返回的Block進(jìn)行編碼,同時(shí)開啟接口。如以下代碼:

DRDAPIPostCall *apiPost = [[DRDAPIPostCall alloc] init];
[apiPost setApiCompletionHandler:^(id responseObject, NSError * error) {
}];
[apiPost start];

這兩種網(wǎng)絡(luò)層接口的設(shè)計(jì)其實(shí)都是對(duì)應(yīng)不同的業(yè)務(wù)所產(chǎn)生出來的思維,因此必然都有其優(yōu)缺點(diǎn)。例如,第一種形式的接口其優(yōu)點(diǎn)在于簡單粗暴,適用于業(yè)務(wù)邏輯相對(duì)簡單并且統(tǒng)一的RESTFUL API網(wǎng)絡(luò)接口。但是缺點(diǎn)也非常明顯,一旦遇上稍微復(fù)雜一些的網(wǎng)絡(luò)接口情況,便需要在ViewController里寫入大量的邏輯來達(dá)到目的。這也同時(shí)會(huì)使得原本就臃腫不堪的ViewController變得更加的龐大。

第二種的設(shè)計(jì)則優(yōu)雅得多。將大量的配置邏輯都放在了另一個(gè)類文件中進(jìn)行設(shè)計(jì),ViewController中的代碼會(huì)變得更輕盈一些。而將配置放在類中還有另一個(gè)好處,那便是可以增加許多平時(shí)不使用的可配置項(xiàng),來增加整個(gè)網(wǎng)絡(luò)層的可擴(kuò)展性;與此同時(shí),每一個(gè)API對(duì)應(yīng)了一個(gè)不同的類的設(shè)計(jì),又可以讓不同的API可以有不同的表象,如分別遵循不同的JSON-RPC版本。不過這種形式的設(shè)計(jì)也存在觸目驚心的問題,那便是類爆炸。如果是小型的APP,則問題并不是那么明顯;而如果是中大型APP的話,動(dòng)則上百個(gè)API,會(huì)使得后期的維護(hù)變得有些吃力。

數(shù)據(jù)回調(diào)

說完了輸入問題,接下來輸出的設(shè)計(jì)。在輸出部分的設(shè)計(jì)中,可以說是八仙過海各顯神通。

網(wǎng)絡(luò)層的傳輸大多以異步加載為主,即服務(wù)器響應(yīng)后由網(wǎng)絡(luò)層來負(fù)責(zé)將數(shù)據(jù)推給上層業(yè)務(wù)線程。在iOS的體系中,也提供了很多種方式用于這種場景的處理,例如直接廣播的Notification、函數(shù)回調(diào)的delegate以及最具特色的Block,都能夠完成這種任務(wù)。那么采用哪種方式呢?在回答這個(gè)問題前我們先來看看這幾種方式其各自的優(yōu)缺點(diǎn)。

Notification

Notification,顧名思義的廣播,其特點(diǎn)在于一對(duì)多地發(fā)送相關(guān)數(shù)據(jù)的通知。優(yōu)點(diǎn)非常明顯,易于實(shí)現(xiàn);但缺點(diǎn)也很明顯,會(huì)破壞整個(gè)APP架構(gòu)設(shè)計(jì)中的層次結(jié)構(gòu),造成跨層的調(diào)用和處理。

Delegate

Delegate,最常用的的回調(diào)方式。優(yōu)點(diǎn)是后期易于維護(hù)且不會(huì)造成跨層的調(diào)用;缺點(diǎn)則是回調(diào)代碼與輸入的邏輯代碼大部分時(shí)候不會(huì)放在一起,增加了一些后期閱讀上的成本。

Block

Block是OC語言中的特性,其優(yōu)點(diǎn)恰好是Delegate的缺點(diǎn),即它讓回調(diào)的代碼能夠和調(diào)用的代碼保持在相同位置,利于靜態(tài)代碼追蹤和邏輯思維的延續(xù)。缺點(diǎn)則在于容易造成循環(huán)引用(Retain Cycle);并且對(duì)于大型APP來說,埋點(diǎn)這種AOP行為通常在Block中難以為繼,且會(huì)造成Debug上的一些困難。

在Block的使用過程中,一定要注意使用weakSelf和strongSelf來打破循環(huán)引用。否則造成的內(nèi)存泄漏會(huì)造成后期排查的困難。

小結(jié)

也許讀者看到這會(huì)更困惑了,究竟什么樣的方案更佳?個(gè)人認(rèn)為還是要從業(yè)務(wù)需求出發(fā)來進(jìn)行設(shè)計(jì),從我自身而言我更喜歡Block+Notification的形式,然后在適當(dāng)?shù)臅r(shí)候輔以Delegate完成。

數(shù)據(jù)轉(zhuǎn)換

數(shù)據(jù)回調(diào)的問題已經(jīng)基本解決,但是新的問題也擺在了我們的面前:上層該看到怎樣的數(shù)據(jù)?

在這里我們會(huì)發(fā)現(xiàn)非常多的應(yīng)用場景,比如大多數(shù)情況下,業(yè)務(wù)層都希望返回的是與其自身相關(guān)的數(shù)據(jù)結(jié)構(gòu)(Model實(shí)例),在這樣的前提下能夠非常地方便地對(duì)本地的數(shù)據(jù)進(jìn)行相關(guān)的操作;而又比如說查詢一個(gè)操作結(jié)果的是與非,那么本身數(shù)據(jù)就只有一個(gè)yes或者no,這時(shí)候采用一個(gè)數(shù)據(jù)結(jié)構(gòu)來囊括便會(huì)顯得復(fù)雜和臃腫;又或是網(wǎng)絡(luò)層采取了JSON-RPC這樣的協(xié)議,返回回來的信息存在大量的冗余數(shù)據(jù),但上層業(yè)務(wù)卻是若水萬千只取一瓢飲。

從以上各種場景中我們可以看到,業(yè)務(wù)所需的數(shù)據(jù)形式非常多變,因此最好的方式還是交給上層自己去處理。一種常見的方法就是設(shè)定一個(gè)Delegate或者Block進(jìn)行返回?cái)?shù)據(jù)的轉(zhuǎn)換,將JSON或者XML等格式轉(zhuǎn)成所需要的數(shù)據(jù)格式以方便上層業(yè)務(wù)繼續(xù)處理。不過我個(gè)人更傾向于在API本身就實(shí)現(xiàn)好這個(gè)Delegate或者Block所描述的轉(zhuǎn)換函數(shù),這樣會(huì)讓API的層次更加清晰。下一節(jié)我會(huì)談到我們的處理方式。

與安全相關(guān)的網(wǎng)絡(luò)層設(shè)計(jì)

接下來我們來看看與安全性相關(guān)的設(shè)計(jì)。其實(shí)總體來說,使用了HTTPS基本上就已經(jīng)足夠保證你的網(wǎng)絡(luò)安全性了,這里我也就不一一列舉其好處。事實(shí)上國外多數(shù)的大公司以及國內(nèi)的BAT幾乎都已經(jīng)是全站HTTPS了,免費(fèi)的SSL證書的申請(qǐng)難度也在不斷降低,門檻上已經(jīng)可以說是沒有門檻。因此為了站點(diǎn)的安全性,上HTTPS吧。

不過,HTTPS如果使用不當(dāng)仍然會(huì)存在一些的小缺陷,MITMA(Man-in-the-middle attack)攻擊便是其中的一種。嘗試這樣一種情況,使用Charles這樣的抓包工具來抓取HTTPS的包,Charles會(huì)讓我們?nèi)グ惭b它自己頒發(fā)的根證書。一旦我們選擇和信任了這個(gè)根證書,我們會(huì)發(fā)現(xiàn)Charles能夠順利地顯示整個(gè)HTTPS通信的情況了。對(duì)于這種中間人攻擊,目前一般的解決方案即采取SSL Pinning,即將服務(wù)器的公鑰證書與整個(gè)APP打包在一起發(fā)出,然后在網(wǎng)絡(luò)請(qǐng)求時(shí)候?qū)⒎?wù)器發(fā)送過來的證書與本地證書進(jìn)行比較,從而避免中間人攻擊的可能性。關(guān)于這部分的設(shè)計(jì),AFNetworking已經(jīng)有相關(guān)的實(shí)現(xiàn)了,我在《正確使用AFNetworking的SSL保證網(wǎng)絡(luò)安全》有過詳細(xì)闡述,這里就不再贅述了。

3 與時(shí)俱進(jìn)下的解決方案

說完了理論,現(xiàn)在結(jié)合實(shí)際來談?wù)勎覀兊慕鉀Q方案。

我們要使用HTTP/2,那么在網(wǎng)絡(luò)庫的選擇上必然需要使用NSURLSession來達(dá)到目的,并且我們也不希望自己去實(shí)現(xiàn)序列化以及RESTFUL的復(fù)雜性,因此AFNetworking3.0成了一個(gè)比較不錯(cuò)的選擇。但是似乎僅僅有這些還不夠。接下來會(huì)分為以下幾個(gè)部分來談?wù)勎覀兊慕鉀Q方案:

  • 業(yè)務(wù)協(xié)議
  • 輸入與配置
  • 數(shù)據(jù)轉(zhuǎn)換與輸出
  • 安全

業(yè)務(wù)協(xié)議

從業(yè)務(wù)協(xié)議上來說,餓了么眾多APP中,每款A(yù)PP都有其自身的特點(diǎn),例如有些采取RESTFUL的設(shè)計(jì),也有采用JSON-RPC的設(shè)計(jì)來達(dá)到業(yè)務(wù)目的。這時(shí)候如果采取集中式的API設(shè)計(jì),相對(duì)應(yīng)JSON-RPC會(huì)產(chǎn)生大量的RPC協(xié)議封裝代碼。并且對(duì)于不同版本、類型的RPC協(xié)議,需要有不同的集中函數(shù)或者增加大量的參數(shù)來處理其中的差異性。如果采取分布式的API設(shè)計(jì),則可以將這部分協(xié)議代碼放進(jìn)API自身類中來進(jìn)行處理。在這里,我設(shè)計(jì)了一個(gè)RPCProtocol,由業(yè)務(wù)方自己來定義所需要遵循的業(yè)務(wù)RPC標(biāo)準(zhǔn)。而每個(gè)API都保存一個(gè)rpcDelegate字段來自定義自己的上層協(xié)議,而如果為空時(shí),即代表著不進(jìn)行RPC封裝而是直接發(fā)送,從而達(dá)到JSON-RPCRESTFUL在一個(gè)APP共存的目的;并且由于每個(gè)API都可以指定不同的rpcDelegate,因此可以適用于服務(wù)器端不同的RPC版本兼容性。這里,RPCProtocol會(huì)有一些這樣的闡述:

NS_ASSUME_NONNULL_BEGIN
@protocol DRDRPCProtocol <NSObject>
- (nullable NSString *)rpcRequestUrlWithAPI:(DRDBaseAPI *)api;
- (nullable id)rpcRequestParamsWithAPI:(DRDBaseAPI *)api;
- (nullable id)rpcResponseObjReformer:(id)responseObject withAPI:(DRDBaseAPI *)api;
- (nullable id)rpcResultWithFormattedResponse:(id)formattedResponseObj withAPI:(DRDBaseAPI *)api;
- (NSError *)rpcErrorWithFormattedResponse:(id)formattedResponseObj withAPI:(DRDBaseAPI *)api;
@end
NS_ASSUME_NONNULL_END

Protocol中會(huì)對(duì)RequestURLRequestParams進(jìn)行RPC裝箱設(shè)計(jì),并且對(duì)于回包,也有rpcResponseObjReformer進(jìn)行拆箱,將可用值和錯(cuò)誤值交給rpcResultWithFormattedResponse以及rpcErrorWithFormattedResponse處理后,再返回給業(yè)務(wù)上層。

通過使用RPCProtocol,我們保持了整個(gè)網(wǎng)絡(luò)層上層協(xié)議的一種可擴(kuò)展性。

輸入與配置

解決完協(xié)議的問題,我們?cè)賮砜纯摧斎牒团渲玫膯栴}。

簡單一看,似乎輸入和配置的關(guān)系并不大;的確如此,在集中式的API設(shè)計(jì)時(shí),更多的時(shí)候是傳參,那么現(xiàn)在采取分散式API設(shè)計(jì)時(shí),由于每個(gè)API先繼承BaseAPI,然后再在子類中去覆蓋每個(gè)需要配置的函數(shù),因此看起來每個(gè)API都更像是一個(gè)配置的過程。

配置完成后的每個(gè)API,過去的方式可能是每個(gè)API都對(duì)應(yīng)一個(gè)APIManager,反饋到AFNetworking上呢,可能就是每個(gè)API都使用一個(gè)AFHTTPRequestOperationManager,然后在這個(gè)Manager去發(fā)起請(qǐng)求。

不過,這種形式在HTTP/2上會(huì)顯得愚笨。我們都知道HTTP/2是復(fù)用TCP管道連接的,這點(diǎn)體現(xiàn)在NSURLSession底層對(duì)于每個(gè)session是對(duì)多個(gè)task進(jìn)行連接的復(fù)用。如果繼續(xù)采取過去的方式多個(gè)AFHTTPSession來請(qǐng)求,會(huì)導(dǎo)致多個(gè)TCP連接,并且連接數(shù)不可控。而復(fù)用Session的話,可以充分利用NSURLSession的并發(fā)控制以及HTTP/2的高復(fù)用來提高性能。這點(diǎn)我在我的另一片文章《別說你會(huì)AFNetworking3.0/NSURLSession》有過詳細(xì)闡述,這里也不再贅述了。

因此,我這里將每個(gè)配置好的API都扔到一個(gè)共享的APIManager中,即分散式API回歸到集中式調(diào)用的懷抱。由APIManager來負(fù)責(zé)提供SessionManager的策略。并且通過Global的配置,來決定每個(gè)Session的最大并發(fā)數(shù)。同時(shí)將AFNetworking封裝進(jìn)整個(gè)APIManager,保持對(duì)外透明。這樣如果未來如果升級(jí)AFNetworking版本,或者打算切換到直接使用NSURLSession來處理網(wǎng)絡(luò)連接,上層業(yè)務(wù)API也不需要有任何的改動(dòng),進(jìn)一步增強(qiáng)了未來的可配置性。

上節(jié)我們提到了分散式API,它具有一個(gè)最大的缺點(diǎn),那便是導(dǎo)致類的爆炸,產(chǎn)生出成白上千的API的文件。在這一點(diǎn)上,我設(shè)計(jì)了另一個(gè)BaseAPI,我稱之為GeneralAPI。這個(gè)API與之前的有什么不同呢?先來看一個(gè)典型的GET請(qǐng)求API的類文件內(nèi)容:

- (NSString *)requestMethod {
    return @"get";
}

- (id)requestParameters {
    return nil;
}

- (DRDRequestMethodType)apiRequestMethodType {
    return DRDRequestMethodTypeGET;
}

- (DRDRequestSerializerType)apiRequestSerializerType {
    return DRDRequestSerializerTypeHTTP;
}

- (DRDResponseSerializerType)apiResponseSerializerType {
    return DRDResponseSerializerTypeHTTP;
}

在這個(gè)API里,覆蓋好幾個(gè)函數(shù)即可以完成相應(yīng)的內(nèi)容。而在ViewController中進(jìn)行調(diào)用會(huì)有這樣的代碼。

    DRDAPIGetCall *apiGet = [[DRDAPIGetCall alloc] init];
    [apiGet setApiCompletionHandler:^(id responseObject, NSError * error) {
        NSLog(@"responseObject is %@", responseObject);
        if (error) {
            NSLog(@"Error is %@", error.localizedDescription);
        }
    }];
    [apiGet start];

而如果使用GeneralAPI,則在ViewController中,會(huì)是這樣的代碼。

DRDGeneralAPI *apiGeGet            = [[DRDGeneralAPI alloc] initWithRequestMethod:@"get"];
apiGeGet.apiRequestMethodType      = DRDRequestMethodTypeGET;
apiGeGet.apiRequestSerializerType  = DRDRequestSerializerTypeHTTP;
apiGeGet.apiResponseSerializerType = DRDResponseSerializerTypeHTTP;
[apiGeGet setApiCompletionHandler:^(id responseObject, NSError * error) {
    NSLog(@"responseObject is %@", responseObject);
    if (error) {
        NSLog(@"Error is %@", error.localizedDescription);
    }
}];
[apiGeGet start];

沒錯(cuò),使用GeneralAPI,將一些簡單的,不復(fù)雜的API的配置,直接使用property來直接在ViewController中賦值,這樣就降低了一些簡單的API生成類文件導(dǎo)致的爆炸。增強(qiáng)了整個(gè)網(wǎng)絡(luò)層的易用性和簡便性。

數(shù)據(jù)轉(zhuǎn)換與輸出

再來看看數(shù)據(jù)轉(zhuǎn)換與輸出。

這里我們將數(shù)據(jù)轉(zhuǎn)換和輸出放在一起來討論。上節(jié)提到的交付什么樣的數(shù)據(jù)給業(yè)務(wù)層、數(shù)據(jù)轉(zhuǎn)換的操作,但并沒有給出答案?,F(xiàn)在我們來結(jié)合業(yè)務(wù)來看看怎樣來操作數(shù)據(jù)的轉(zhuǎn)換。

前面提到過,餓了么各個(gè)APP產(chǎn)品線都會(huì)有自己的性格和特點(diǎn),數(shù)據(jù)轉(zhuǎn)換上也會(huì)呈現(xiàn)各自喜好的局面。有人喜歡用Mantle,有人用過MJExtension,也有采取YYModel,也有大牛自己實(shí)現(xiàn)JSON<-->Model的轉(zhuǎn)換。因此,網(wǎng)絡(luò)層最好并不過問數(shù)據(jù)的轉(zhuǎn)換方式以及過程,而是提供一個(gè)機(jī)會(huì)給上層業(yè)務(wù)來讓其自己采用自己的方式進(jìn)行轉(zhuǎn)換。因此,我在API的設(shè)計(jì)中,提供了這樣一個(gè)函數(shù):

- (nullable id)apiResponseObjReformer:(id)responseObject andError:(NSError * _Nullable)error;

GeneralAPI中對(duì)應(yīng)為:

@property (nonatomic, copy, nullable) id _Nullable (^apiResponseObjReformerBlock)(id responseObject, NSError * _Nullable error);

這個(gè)函數(shù)默認(rèn)為空,參數(shù)中的responseObjectrpcDelegate拆包后產(chǎn)生的resposneObject。在這個(gè)函數(shù)中,上層業(yè)務(wù)可以將responseObject進(jìn)行Model的轉(zhuǎn)換工作,將Model作為返回值交給apiCompletionHandler函數(shù)進(jìn)行操作。這樣既保持了ViewController中的簡潔性,也在保證了各個(gè)上層業(yè)務(wù)對(duì)于JSON<-->Model轉(zhuǎn)換的多樣性的同時(shí),保證了未來轉(zhuǎn)換方式的可擴(kuò)展性。

安全

最后來談?wù)劙踩?/p>

其實(shí)安全到這一塊可談的已經(jīng)不多了,該談的都在上節(jié)中談完了。由于APIManager中采用了AFNetworking簡化SSL Pinning的復(fù)雜度,因此在網(wǎng)絡(luò)層中只需要三步便可以完成SSL Pinning。

  1. 實(shí)例化一個(gè)DRDSecurityPolicy, 將SecurityPolicy中的DRDSSLPinningMode設(shè)置成為DRDSSLPinningModePublicKey或者DRDSSLPinningModeCertificate
  2. 將API的apiSecurityPolicy設(shè)定為以上實(shí)例。
  3. 將服務(wù)器的公鑰證書放到APP Bundle中。

結(jié)束!

One More Thing!

**多網(wǎng)絡(luò)請(qǐng)求的并發(fā)執(zhí)行。 **

設(shè)想這樣一個(gè)場景,我們希望有若干個(gè)網(wǎng)絡(luò)請(qǐng)求,當(dāng)這些請(qǐng)求都結(jié)束后才通知上層應(yīng)用工作的完成。

早期采取AFNetworking的AFHTTPRequestOperationManager方案時(shí),AFN對(duì)整個(gè)系統(tǒng)都采用了NSOperation以及NSOperationQueue來控制網(wǎng)絡(luò)請(qǐng)求的依賴性。但是HTTP/2后的NSURLSession由于無法使用這種設(shè)計(jì),因此造成這種并發(fā)依賴難以為繼。

值得慶幸的是,AFNetworking在SessionManager的實(shí)現(xiàn)中依舊保留有dispatch_group_t的接口。因此我們使用dispatch_group創(chuàng)建了DRDAPIBatchAPIRequests類,來達(dá)到并發(fā)網(wǎng)絡(luò)請(qǐng)求的目的。

@interface DRDAPIBatchAPIRequests : NSObject
@property (nonatomic, strong, readonly, nullable) NSMutableSet *apiRequestsSet;
@property (nonatomic, weak, nullable) id<DRDAPIBatchAPIRequestsProtocol> delegate;
- (void)addAPIRequest:(nonnull DRDBaseAPI *)api;
- (void)addBatchAPIRequests:(nonnull NSSet *)apis;
- (void)start;
@end

一如既往,保持API層面簡潔。這里我們使用了Delegate來處理多個(gè)網(wǎng)絡(luò)請(qǐng)求完成后的回調(diào)操作:

@protocol DRDAPIBatchAPIRequestsProtocol <NSObject>
- (void)batchAPIRequestsDidFinished:(nonnull DRDAPIBatchAPIRequests *)batchApis;
@end

在這個(gè)設(shè)計(jì)中,每個(gè)API完成后,都可以有自己的回調(diào)。所有的并發(fā)網(wǎng)絡(luò)請(qǐng)求完成后仍舊可以有一個(gè)公用的回調(diào),讓整體設(shè)計(jì)保持一個(gè)離散+集中都能得到很好處理的情形,保證上層的業(yè)務(wù)可擴(kuò)展性。

結(jié)語

結(jié)合業(yè)務(wù),才能談及架構(gòu);持續(xù)調(diào)優(yōu),才能不斷地與時(shí)俱進(jìn)。

最后我們也開源了我們的DRDNetworking庫,歡迎大家多提寶貴意見。

本文謝絕轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)簡信于我,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容