iOS 網路層架構設計分享
前些天幫公司做了網路層的重構,當時就想做好了就分享給大家,後來接著做了新版本的需求,現在才有時間整理一下。
之前的網路層使用的是直接拖拽匯入專案的方式匯入了AF,然後還修改了大量的原始碼,時隔2年,AF已經更新換代很多次了,導致整個重構遷移非常的麻煩。不過看著前輩寫的程式碼,肯定也是一個高人,許多思路和我的一樣,但是實現方式又不同,給我很好的參考。
在做網路層架構的時候也參考了Casa大神的架構思想,但是還是有所不同。
本文沒有太多的理論,沒有太多的專業術語,一來是方便大家閱讀,二來我的基礎也沒那麼好,沒有太多華麗的詞彙,對於架構來說主要是思路,有思路在,具體的實現就沒有問題了
本文主要介紹以下幾點:
1.網路介面規範
2.多伺服器多環境設定
3.網路層資料傳遞(請求和返回)
4.業務層對接方式
5.網路請求怎麼自動取消
6.網路層錯誤處理
網路介面規範
demo裡面的請求示例是在網上找的,不符合我說的這套規範,僅作示例用。
規範很重要,有合理的規範就可以精簡很多程式碼邏輯,特別是介面的相容,是最底層最基礎的設計,把介面規範放在前面來說。
在做這次重構時,我提出了一些規範點,可以給大家參考
1.兩層三部分資料結構
介面返回資料第一次為字典,分為兩層三部分:code、msg、data
1234567 | "code":0,"msg":"","data":{"upload_log":true,"has_update":false,"admin_id":"529ecfd64"} |
code:錯誤碼,可以記錄下來快速定位介面錯誤原因,可以定義一套錯誤碼,比如200正常,1重新登入…
msg:介面文案提示,包括錯誤提示,用來直接顯示給使用者,所以這一套錯誤提示就不能是什麼一串英文錯誤了
data:需要返回的資料,可以是字典,可以是陣列
介面幫我們定義了code和msg,是不是我們就不需要做錯誤處理了?當然不是,服務端的錯誤邏輯畢竟是簡單的,具體到data裡面的資料處理可能還有錯誤,所以錯誤的處理是必不可少的,下面會單獨對錯誤處理做介紹
2.網路請求引數上傳方式統一
這裡一般都能做到,也有額外的,比如我們的一個伺服器介面做的比較早,當時POST介面使用的就不規範,普通的應用資訊channelID、device_id使用的是拼接在字串後面的方式,而真正的請求引數則需要轉成json放在一個欄位裡面傳遞,就是介面GET、POST並存的方式,造成網路層需要做特殊處理
所以說標準的GET、POST請求方式是很有必要的
3.關於Null型別
大家都知道Null型別在iOS裡面是很特殊的,我的建議是放在客戶端來做,原因有很多:
1)介面的規範定義並不是每個公司都是從一開始就能定義好的,老介面如果要把Null欄位去掉的改動非常大
2)客戶端用過一個介面過濾也可以解決,一勞永逸,不用再擔心因為某天介面的問題出現崩潰,而且通過一些Model的第三方庫也可以很好的解決這個問題。這裡不得說下swift的型別檢測真是太方便了,之前一個專案用swift寫的,程式碼規範一點,根本不會出現因為引數型別問題引起崩潰
多伺服器多環境設定
這部分基本上是照搬casa大神的設計,這裡我延伸了一個多環境的設計,小的專案一般都是一個伺服器,但是像淘寶之類的專案一個伺服器顯然是不可能的,多個伺服器的設計還是非常普遍的。根據一個列舉變數通過ServerFactory單例生成獲取對應的伺服器配置
1.伺服器環境
標準的APP是有4個環境的,開發、測試、預發、正式,特別是伺服器的程式碼,不能說所有的程式碼更改都在正式環境下,應該從開發->測試->預發->正式做程式碼的更新,開發就是新需求和優化的時候的更改,測試就是提交給測試人員後的更改,這個時候更改是在一個新的分支上,完成後要和合併到測試分支上併合併到開發分支上,預發這時候的變動就比較小了,一般會在測試人員完成後釋出給全公司的人來測試,有問題了才會更改,更改後同樣合併到開發分支,正式則是線上釋出版本的緊急BUG修復,修改完後同樣合併到開發分支上。所以開發分支是一直都是最新的。在此基礎上可能會有其他的環境,比如hotfix環境,自定義的h5/後臺本地除錯的環境。
客戶端同樣存在這些環境,並且要提供切換的入口。
在我的demo中提供了兩套設定,一套是第一次安裝應用的初始化環境(巨集定義),另外是手動切換環境的設定(列舉EnvironmentType)。這裡有一個比較繞的邏輯,巨集定義的正式環境設定高於手動切換環境設定,手動切換環境設定高於巨集定義其他環境
12 | //巨集定義環境設定#if !defined YA_BUILD_FOR_DEVELOP |
12345678910111213141516171819202122 | //手動環境切換設定#ifdef YA_BUILD_FOR_RELEASE//優先巨集定義正式環境self.environmentType=EnvironmentTypeRelease;#else//手動切換環境後會把設定儲存NSNumber *type=[[NSUserDefaults standardUserDefaults]objectForKey:@"environmentType"];if(type){//優先讀取手動切換設定self.environmentType=(EnvironmentType)[type integerValue];}else{#ifdef YA_BUILD_FOR_DEVELOPself.environmentType=EnvironmentTypeDevelop;#elif defined YA_BUILD_FOR_TESTself.environmentType=EnvironmentTypeTest;#elif defined YA_BUILD_FOR_PRERELEASEself.environmentType=EnvironmentTypePreRelease;#elif defined YA_BUILD_FOR_HOTFIXself.environmentType=EnvironmentTypeHotFix;#endif}#endif |
所以當巨集定義正式環境
存在的時候是不能手動切換環境的,用於普通使用者的釋出版本,但是其他巨集定義環境
時是可以切換到正式環境的。
半個坑
另外手動切換自定義的環境是在基類中實現的,而其他的環境配置是在協議中實現的,這就和其他環境地址的配置不統一了。
可以這樣理解,這裡的基類是為了提供已返回值,協議是為了返回值的靈活,既然自定義環境的地址配置不需要靈活性,自然是放在基類好。思路是大方向,實現是靈活的,如果非要放在協議中實現也無不可以,無非是賦值貼上幾次一樣的程式碼,但是一模一樣的程式碼是我最不喜歡看到的,所以就放在基類了。如果有更好的解決方案歡迎提供
2.擴充套件性
model提供的是高擴充套件性,針對不同的不伺服器新增更多的配置,比如加密方法,比如資料解析方法…前面提到了,統一的規範有的時候不是一時半會就能做好的,相容就成了需求,這個時候不同伺服器的個性化設定就可以在協議中宣告並實現了,基類提供返回值就好
網路層資料傳遞(請求和返回)
Client、BaseEngine/DataEngine、RequestDataModel資料傳遞
網路請求的發生在我理解中分兩步,一步是資料的整理,一步是生成Request併發起請求,基於這個思想我拆分出了Client和Engine,然後又把URLRequestGenerator從Client中拆分出來,Engine拆分出了下層的BaseEngine和麵向不同業務的DataEngine,
而從BaseEngine到Client,再到URLRequestGenerator是要做資料傳遞的,請求引數和返回引數,所以又有了RequestDataModel
RequestDataModel
12345678910111213141516171819202122232425 | @interfaceYAAPIBaseRequestDataModel:NSObject/** * 網路請求引數 */@property(nonatomic,strong)NSString *apiMethodPath;//網路請求地址@property(nonatomic,assign)YAServiceType serviceType;//伺服器標識@property(nonatomic,strong)NSDictionary *parameters;//請求引數@property(nonatomic,assign)YAAPIManagerRequestType requestType;//網路請求方式@property(nonatomic,copy)CompletionDataBlock responseBlock;//請求著陸回撥// upload// upload file@property(nonatomic,strong)NSString *dataFilePath;@property(nonatomic,strong)NSString *dataName;@property(nonatomic,strong)NSString *fileName;@property(nonatomic,strong)NSString *mimeType;// download// download file// progressBlock@property(nonatomic,copy)ProgressBlock uploadProgressBlock;@property(nonatomic,copy)ProgressBlock downloadProgressBlock;@end |
可以看出來RequestDataModel屬性都是網路請求發起和返回的必要引數,這樣做的好處真的是太大了,不知道大家有沒有這樣的場景:因為請求引數的不同做了好多方法介面暴露出去,最後調起的還是同一個方法,而且一旦方法寫的多了,最後連應該呼叫哪個方法都不知道了。我就遇到過,所以現在我的網路請求調起是這樣的:
12 | //沒有回撥,沒有其他的引數,只有一個dataModel,節省了你所有的方法[[YAAPIClient sharedInstance]callRequestWithRequestModel:dataModel]; |
生成NSURLRequest是這樣的:
1 | NSURLRequest *request=[[YAAPIURLRequestGenerator sharedInstance]generateWithYAAPIRequestWithRequestDataModel:requestModel]; |
可以看到我的demo裡面的YAAPIClient類和YAAPIURLRequestGenerator類方法至少,方法少就意味著邏輯簡單明瞭,方便閱讀,兩個類的程式碼行數都是120行,120行實現了網路請求的發起和著陸,你能想象嗎
另外RequestDataModel帶來的另外一個好處就是高擴充套件性,你有沒有遇到網路層需要新增刪除一個引數導致呼叫方法修改了,然後很多地方都要修改方法?用RequestDataModel只需要新增刪除引數就行了,只需要改方法體,這個改方法體和同時改方法名方法體是完全兩個工作量。哈哈,有點賣虎皮膏藥的感覺。這個的確是我的得意創新點。
Client
Client做兩個操作,一個是生成NSURLRequest,一個是生成NSURLSessionDataTask併發起,另外還要暴露取消操作給Engine,URLRequestGenerator是生成NSURLRequest,URLRequestGenerator會對dataModel進行加工解析,生成對應伺服器的NSURLRequest
然後Client通過NSURLRequest生成NSURLSessionDataTask。
Client和URLRequestGenerator都是單例
12345678910111213141516 | -(void)callRequestWithRequestModel:(YAAPIBaseRequestDataModel *)requestModel{NSURLRequest *request=[[YAAPIURLRequestGenerator sharedInstance]generateWithRequestDataModel:requestModel];AFURLSessionManager *sessionManager=self.sessionManager;NSURLSessionDataTask *task=[sessionManagerdataTaskWithRequest:requestuploadProgress:requestModel.uploadProgressBlockdownloadProgress:requestModel.downloadProgressBlockcompletionHandler:^(NSURLResponse *_Nonnull response,id _Nullable responseObject,NSError *_Nullable error){//請求著陸}];[task resume];} |
取消介面參考了casa大神的設計,使用NSNumber *requestID
來做task的繫結,就不多做介紹了
BaseEngine/DataEngine
Engine或者說是APIManager在我的設計中既不是離散的也不是集約的
casa大神的理論
集約型API呼叫其實就是所有API的呼叫只有一個類,然後這個類接收API名字,API引數,以及回撥著陸點(可以是target-action,或者block,或者delegate等各種模式的著陸點)作為引數。然後執行類似startRequest這樣的方法,它就會去根據這些引數起飛去呼叫API了,然後獲得API資料之後再根據指定的著陸點去著陸。比如這樣:
1 [APIRequest startRequestWithApiName:@"itemList.v1"params:params success:@selector(success:)fail:@selector(fail:)target:self]; 離散型API呼叫是這樣的,一個API對應於一個APIManager,然後這個APIManager只需要提供引數就能起飛,API名字、著陸方式都已經整合入APIManager中。比如這樣:
123456789101112 @property(nonatomic,strong)ItemListAPIManager *itemListAPIManager;// getter-(ItemListAPIManager *)itemListAPIManager{if(_itemListAPIManager==nil){_itemListAPIManager=[[ItemListAPIManager alloc]init];_itemListAPIManager.delegate=self;}return_itemListAPIManager;}// 使用的時候就這麼寫:[self.itemListAPIManager loadDataWithParams:params];
各自的優點就不說了,但是由此延伸出幾個問題:
1.引數的傳遞使用字典對於網路層來說是不可知的,而且業務層需要去關注介面欄位的變化,其實是沒有必要的
2.離散型API會造成Manager大爆炸
3.集約型會造成取消操作不方便
4.取消操作並不是每個介面必須的,如果寫成部分離散的部分集約的,程式碼的整體結構…我是個有強迫症的人,看不得這樣的程式碼
所以我的設計主要就解決了上面的這些問題
1.面向業務層的DataEngine只傳遞必要的引數進來,不使用字典,比如
12345 | @interfaceSearchDataEngine:NSObject+(YABaseDataEngine *)control:(NSObject *)controlsearchKey:(NSString *)searchKeycomplete:(CompletionDataBlock)responseBlock;@end |
control暫時先不管,是做自動取消的,後面再介紹。
searchKey就是搜尋的關鍵字
在呼叫的時候就是這樣
1234567 | self.searchDataEngine=[SearchDataEngine control:self searchKey:@"關鍵字"complete:^(id data,NSError *error){if(error){NSLog(@"%@",error.localizedDescription);}else{NSLog(@"%@",data);}}]; |
2.我按業務層來劃分DataEngine,比如BBSDataEngine、ShopDataEngine、UserInforDataEngine…每個DataEngine裡面包含各自業務的所有網路請求介面,這樣就不會出現DataEngine大爆炸,像我們的專案有300多個介面,拆分後有十幾個DataEngine,如果使用離散型API設計,那畫面太美我不敢看