iOS網絡層架構設計分享

聲明:
轉載請註明出處:http://www.jianshu.com/p/05a59197a7c7

前些天幫公司做了網絡層的重構,當時就想做好了就分享給大家,後來接着做了新版本的需求,現在纔有時間整理一下。
之前的網絡層使用的是直接拖拽導入項目的方式導入了AF,然後還修改了大量的源碼,時隔2年,AF已經更新換代很多次了,導致整個重構遷移非常的麻煩。不過看着前輩寫的代碼,肯定也是一個高人,許多思路和我的一樣,但是實現方式又不同,給我很好的參考。
在做網絡層架構的時候也參考了Casa大神的架構思想,但是還是有所不同。
本文沒有太多的理論,沒有太多的專業術語,一來是方便大家閱讀,二來我的基礎也沒那麼好,沒有太多華麗的詞彙,對於架構來說主要是思路,有思路在,具體的實現就沒有問題了
本文主要介紹以下幾點
1.網絡接口規範
2.多服務器多環境設置
3.網絡層數據傳遞(請求和返回)
4.業務層對接方式
5.網絡請求怎麼自動取消
6.網絡層錯誤處理

無Demo無文章  Demo下載

網絡接口規範

demo裏面的請求示例是在網上找的,不符合我說的這套規範,僅作示例用
規範很重要,有合理的規範就可以精簡很多代碼邏輯,特別是接口的兼容,是最底層最基礎的設計,把接口規範放在前面來說
在做這次重構時,我提出了一些規範點,可以給大家參考

1.兩層三部分數據結構
接口返回數據第一次爲字典,分爲兩層三部分:code、msg、data

    "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)。這裏有一個比較繞的邏輯,宏定義的正式環境設置高於手動切換環境設置,手動切換環境設置高於宏定義其他環境

//宏定義環境設置
#if !defined YA_BUILD_FOR_DEVELOP && !defined YA_BUILD_FOR_TEST && !defined YA_BUILD_FOR_RELEASE && !defined YA_BUILD_FOR_PRERELEASE

#define YA_BUILD_FOR_DEVELOP
//#define YA_BUILD_FOR_TEST
//#define YA_BUILD_FOR_PRERELEASE
//#define YA_BUILD_FOR_HOTFIX
//#define YA_BUILD_FOR_RELEASE      //該環境的優先級最高

#endif
//手動環境切換設置
#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_DEVELOP
                self.environmentType = EnvironmentTypeDevelop;
#elif defined YA_BUILD_FOR_TEST
                self.environmentType = EnvironmentTypeTest;
#elif defined YA_BUILD_FOR_PRERELEASE
                self.environmentType = EnvironmentTypePreRelease;
#elif defined YA_BUILD_FOR_HOTFIX
                self.environmentType = EnvironmentTypeHotFix;
#endif
            }
#endif

所以當宏定義正式環境存在的時候是不能手動切換環境的,用於普通用戶的發佈版本,但是其他宏定義環境時是可以切換到正式環境的。

半個坑
另外手動切換自定義的環境是在基類中實現的,而其他的環境配置是在協議中實現的,這就和其他環境地址的配置不統一了。
可以這樣理解,這裏的基類是爲了提供已返回值,協議是爲了返回值的靈活,既然自定義環境的地址配置不需要靈活性,自然是放在基類好。思路是大方向,實現是靈活的,如果非要放在協議中實現也無不可以,無非是賦值粘貼幾次一樣的代碼,但是一模一樣的代碼是我最不喜歡看到的,所以就放在基類了。如果有更好的解決方案歡迎提供

2.擴展性
model提供的是高擴展性,針對不同的不服務器添加更多的配置,比如加密方法,比如數據解析方法...前面提到了,統一的規範有的時候不是一時半會就能做好的,兼容就成了需求,這個時候不同服務器的個性化設置就可以在協議中聲明並實現了,基類提供返回值就好

網絡層數據傳遞(請求和返回)


網絡層數據傳遞
Client、BaseEngine/DataEngine、RequestDataModel數據傳遞

網絡請求的發生在我理解中分兩步,一步是數據的整理,一步是生成Request併發起請求,基於這個思想我拆分出了Client和Engine,然後又把URLRequestGenerator從Client中拆分出來,Engine拆分出了下層的BaseEngine和麪向不同業務的DataEngine,
而從BaseEngine到Client,再到URLRequestGenerator是要做數據傳遞的,請求參數和返回參數,所以又有了RequestDataModel

RequestDataModel
@interface YAAPIBaseRequestDataModel : 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屬性都是網絡請求發起和返回的必要參數,這樣做的好處真的是太大了,不知道大家有沒有這樣的場景:因爲請求參數的不同做了好多方法接口暴露出去,最後調起的還是同一個方法,而且一旦方法寫的多了,最後連應該調用哪個方法都不知道了。我就遇到過,所以現在我的網絡請求調起是這樣的:

//沒有回調,沒有其他的參數,只有一個dataModel,節省了你所有的方法
[[YAAPIClient sharedInstance] callRequestWithRequestModel:dataModel];

生成NSURLRequest是這樣的:

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都是單例

- (void)callRequestWithRequestModel:(YAAPIBaseRequestDataModel *)requestModel{
    NSURLRequest *request = [[YAAPIURLRequestGenerator sharedInstance] 
generateWithRequestDataModel:requestModel];
    AFURLSessionManager *sessionManager = self.sessionManager;
    NSURLSessionDataTask *task = [sessionManager
                                  dataTaskWithRequest:request
                                  uploadProgress:requestModel.uploadProgressBlock
                                  downloadProgress:requestModel.downloadProgressBlock
                                  completionHandler:^(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數據之後再根據指定的着陸點去着陸。比如這樣:

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

離散型API調用是這樣的,一個API對應於一個APIManager,然後這個APIManager只需要提供參數就能起飛,API名字、着陸方式都已經集成入APIManager中。比如這樣:

@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只傳遞必要的參數進來,不使用字典,比如

@interface SearchDataEngine : NSObject
+ (YABaseDataEngine *)control:(NSObject *)control
                    searchKey:(NSString *)searchKey
                     complete:(CompletionDataBlock)responseBlock;
@end

control暫時先不管,是做自動取消的,後面再介紹。
searchKey就是搜索的關鍵字
在調用的時候就是這樣

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設計,那畫面太美我不敢看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章