ios-APP重構之路(一) 網路請求框架

ios8988發表於2018-09-12

前言

在現在的app,網路請求是一個很重要的部分,app中很多部分都有或多或少的網路請求,所以在一個專案重構時,我會選擇網路請求框架作為我重構的起點。在這篇文章中我所提出的架構,並不是所謂的 最好 的網路請求架構,因為我只基於我這個app原有架構進行改善,更多的情況下我是以app為出發點,讓這個網路架構能夠在原app的環境下給我一個完美的結果,當然如果有更好的改進意見,我會很樂於嘗試。

關於網路請求框架

一個好的網路請求框架對於一個團隊來說是十分重要的。如果一個網路請求框架沒有封裝好,或者是在設計上存在問題,那麼在開發上會造成許多問題,就拿這段程式碼作為例子:

[leaveAPI startWithCompletionBlockWith:^(BaseRequest *baseRequest, id responseObject) {
    //check the response object
    BOOL isSuccess = [leaveAPI validResponseObject:responseObject];
    if (isSuccess) {
        //do something...
    }   
}failure:^(BaseRequest *baseRequest) {
    //do something...
}];

 

上面這段程式碼存在著不少的問題,比如把請求資料的判斷放到了每一個請求中、在leaveAPI的塊方法中再次呼叫leaveAPI、塊引數中的baseRequest並沒有實質作用等等……針對這些問題我會一一進行修正。

不要讓其他人做請求資料有效與否的判斷

在上面的程式碼中,對resposeObject是否有效的判斷被設計成了BaseRequest類中的一個方法,程式設計師需要在呼叫網路請求後,再呼叫該方法對responseObject進行判斷,這樣的設計存在很大的弊端。

在實際應用中,很多時候程式設計師在呼叫網路請求後往往會忘記呼叫該方法對返回結果進行判斷,甚至忘記了存在這個方法,自行對responseObject進行判斷。首先這造成了大規模的程式碼重複,另一方面,不同程式設計師自己編寫的判斷方法散落在各個請求中,假如app在日後更新過程中改變了這個判斷標準,會給修改帶來很大困難。

注意在塊方法中的迴圈呼叫

上面的程式碼中,在leaveAPI的塊方法中,再次呼叫了leaveAPI中的方法,這樣導致了“retain cycle“,實際上正確的呼叫方法應該是:

[leaveAPI startWithCompletionBlockWith:^(LeaveAPI *api, id responseObject) {
    //check the response object
    BOOL isSuccess = [api validResponseObject:responseObject];
    if (isSuccess) {
        //do something...
    }
}];

 

為什麼會出現這樣的情況,首先主要是因為整個請求框架的註釋不清晰,導致其他程式設計師對方法的理解存在偏差,進而天馬行空,發揮自己的想象力來呼叫方法。另外由於各個API與BaseRequest的設計上存在問題,導致整個網路請求框架的混亂。

 

不要在單獨的API中實現上傳下載操作

在舊的網路請求框架中,BaseRequest一開始的設計中並沒有針對上傳和下載操作進行處理,而且整個BaseRequest的設計中並沒有AOP,這個導致了在日後需要增加上傳和下載功能的時候只能將他們寫到單獨的API中,這個導致了程式碼重複,程式碼的複用性降低,如:

//
//  FileAPI.m
//

...some methods...

#pragma mark - Upload & Download

-(void)uploadFile:(FileUploadCompleteBlock)uploadBlock errorBlock:(FileUploadFailBlock)errorBlock {
    NSString *url = self.url   
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    manager.requestSerializer = [AFHTTPRequestSerializer serializer];
    manager.operationQueue.maxConcurrentOperationCount = 5;
    manager.requestSerializer.timeoutInterval = 30;
    manager.responseSerializer.acceptableContentTypes =  [NSSet setWithObjects:@"application/json", @"text/html",@"text/json",@"text/javascript",@"text/plain",nil];

    [manager POST:url parameters:[self requestArgument] constructingBodyWithBlock:^(id formData) {

  // upload operation ...

    }success:^(AFHTTPRequestOperation *operation, id responseObject) {

     // do something ...
    }failure:^(AFHTTPRequestOperation *operation, NSError *error) {

  // do something ...
    }];
}

 

在FileAPI.m中,上傳操作是這樣實現的。寫下這段程式碼的時候是使用AFNetworking 2.0,而現在使用的是AFNetworking 3.0,AFHTTPRequestOperationManager也變成了AFHTTPSessionManger,這個時候散落在各個API的上傳方法修改起來就變的很麻煩。

BaseRequest中的設計缺陷

在上文中一直在指出各個API中的缺陷,而也提到很多地方是歸咎於BaseReuqest的問題,現在就來看一下它裡面的一些缺陷:

首先在整個BaseRequest中,它包括了地址的組裝、網路環境的判斷、請求的傳送等等,基本網路請求的所有操作都是由這一個類來實現。這樣就導致了整個類十分龐大,在需要新增新的請求型別如我上文提到的上傳與下載時,會難以下手,這就導致了我上文提到的種種問題。

另一方面BaseRequest中沒有針對返回資料的處理,這裡的處理是指返回資料的快取操作、資料過濾操作、請求資料為空的處理操作等等,如果這些問題都交給方法呼叫者來完成的話,會導致某一模組的程式碼量暴漲(在本app是VC),而且很多時候資料需要的只是一個預設的快取操作、預設的過濾操作,這個時候重複性的程式碼會很多,倒不如把這些操作統一處理好,假如有特殊的API需要進行特殊的配置,再由該API對這些配置進行修改,而不需要把這些預設操作交由其他程式設計師來完成。

我是如何設計新的網路請求框架

上文提到了各種各樣的不足,所以是時候針對這些不足進行改進了。

先看大局,再看細節。首先是整個架構的資料流向:

整個網路請求框架中最重要的是其中的NetworkManage,它主要是負責整個請求的處理。

設計中的一些關注重點

首先檢測網路狀態

當一個請求發起的時候,首先它會檢測網路是否聯通,假如沒有聯通的時候會直接彈出一個視窗提醒使用者需要先連線網路,而不會進行下一步的請求。而在舊的網路請求框架中,很多時候把這段程式碼放到了vc,現在將它整合進來。ios開發稽核交流群 869685378 歡迎各位大牛來分享交流 IOS,馬甲包,低要求,內容開發沒有限制,報酬豐厚,實力誠信 Q:782675105 

- (void)addRequest:(BaseRequest*)request {

    //TODO: 檢查網路是否通暢
    if(![self checkNetworkConnection])
    {
        [self showNetworkAlertForRequest:request];
        return;
    }

 

[self checkNetworkConnection]:

- (BOOL)checkNetworkConnection
{
    struct sockaddr zeroAddress;
    bzero(&zeroAddress, sizeof(zeroAddress));
    zeroAddress.sa_len = sizeof(zeroAddress);
    zeroAddress.sa_family = AF_INET;

    SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
    SCNetworkReachabilityFlags flags;

    BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags);
    CFRelease(defaultRouteReachability);

    if (!didRetrieveFlags) {
        printf("Error. Count not recover network reachability flags\n");
        return NO;
    }

    BOOL isReachable = flags & kSCNetworkFlagsReachable;
    BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired;
    return (isReachable && !needsConnection) ? YES : NO;
}

 

活性組裝請求地址

而在進行完網路聯通的判斷之後,就會對請求的地址進行組裝。組裝地址的方法並沒有太大的變化,但是在舊的請求框架開發的時候,我注意到一個問題:在增加新需求增加新的介面的時候,往往需要連線到測試伺服器上進行除錯,這時候就需要將請求的地址改成測試伺服器的地址。但這往往引發一些問題,因為測試伺服器上可能沒有正式伺服器的一些資料,在測試時往往沒有問題,但是轉移到正式伺服器上就出現了各種問題,所以我就想能不能改成程式設計師可以改變API連線的地址,而不改變全域性的請求框架,讓各個API在請求的時候判斷自己是否需要連線到測試伺服器。

- (NSString *)urlString{
    NSString *url = nil;
    //TODO: 使用副地址
    if ([self.child respondsToSelector:@selector(useViceUrl)] && [self.child useViceUrl]){
        baseUrl = self.config.viceBaseUrl;
    }
    //TODO: 使用主地址
    else{
        baseUrl = self.config.mainBaseUrl;
    }
}

 

讓API能夠獨立配置

組裝地址完畢之後,就開始根據API自身的設定來進行配置,在舊的請求框架中,API的是直接繼承自BaseRequest這個類,導致了BaseRequest需要完成大量的工作,或是存有大量空方法,可讀性與穩定性都很差,很多東西也沒有辦法讓API自己進行獨立設定。在新的框架中,我選擇將API的設定通過一個叫做APIProtocol的協議來完成,API需要配置的內容可以通過實現該協議的方法來進行配置,否則就會直接使用預設配置

//TODO: 檢查是否使用自定義超時時間
    if ([request respondsToSelector:@selector(requestTimeoutInterval)]) {
        self.manager.requestSerializer.timeoutInterval = [request requestTimeoutInterval];
    }
    else{
        self.manager.requestSerializer.timeoutInterval = 60.0;
    }

more methods ...

 

完善返回資料的基礎判斷

最後在進行完請求判斷後,將會對responseObject的有效性進行判斷。關於資料的判斷我一開始是打算放在BaseRequest中的,因為一開始的想法是希望能夠在BaseRequest中做一個預設的判斷,假如API自身需要再度對responseObject進行進一步的判斷時,可以通過協議方法來重新編寫該API獨立的判定方法。但這種方法最終被我棄用了,首先responseObject的基礎判斷在我看來是不應該放在BaseRequest中的,因為BaseRequest是作為一個請求的”中心”,不應該把資料處理的問題交給它處理。另一方面是因為我們需要設計的是基礎判斷,它和各個API獨立的判斷方式不是平行關係,而是層次關係,因為在設計的是每一個API都需要進行的判斷,假如在整個app中有很多API需要進行獨立判斷,就意味著需要編寫很多次基礎判斷邏輯,同時假如在日後需要修改這個基礎判斷內容,程式碼也散落在各個地方,這不是我們想要的結果。

所以在設計上我最終把這個判斷方法放到了NetworkConfig中,新增了一個BaseFilter類,專門用於返回資料的判斷,假如我的API需要增加獨特的判斷方法時,可以直接在請求方法中直接對responseObject進行進一步判斷。

NetworkConfig.m:

//NetworkManage.m

if([self.networkConfig.baseFilter validResponseObject:responseObject])
{
request.responseObject = responseObject;
[self handleSuccessRequest:task];
}
else
{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadServerResponse userInfo:nil];
request.responseObject = responseObject;
[self handleFailureRequest:task error:error];
}

 

BaseFilter.m

@implementation BaseFilter

- (BOOL)validResponseObject:(id)responseObject
{
    //TODO: 檢查是否返回了資料且資料是否正確
    if (!responseObject && ![responseObject isKindOfClass:[NSDictionary class]] && ![responseObject[@"success"] boolValue]) {
        return NO;
    }
    else
        return YES;
}

@end

 

結語

我相信在軟體設計中並不存在最好或者是最正確的架構,因為這是一個很抽象的工作,但我相信我們應該可以設計出一個擴充套件性良好和簡單明瞭的架構,能夠讓新加入的程式設計師快速上手,能夠適應軟體接下來的開發需要,那這大概是一個好的架構。

 杜瑋

相關文章