AFNetworking到底長啥樣(下)

AFNetworking到底長啥樣(上)中簡單介紹了AFN涉及的主要類及其結構,接下來以一個簡單的POST請求探尋其內部是如何實現的。

一、環境搭建

  1. 服務器配置

    本例中直接使用iMac自帶的Apache,併爲其開啓PHP支持。在服務器目錄下編寫index.php文件如下:

    <?php
    echo @"This is Layne's Response";
    ?>
  2. 編寫測試App

    創建一個測試App,在主界面上增加一個按鈕,在按鈕的點擊函數中發起網絡請求,如下:

    - (AFHTTPSessionManager *)manager{//lazy
       if(!_manager){
           _manager = [AFHTTPSessionManager manager];
       }
       return _manager;
    }
    
    - (void)click{
       [self.manager POST:@"http://www.layne.com" 
               parameters:@{@"name":@"layne",@"age":@30} 
                  headers:@{@"TestName":@"myTest"} 
                 progress:nil 
            success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                    NSLog(@"success:%@",responseObject);
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                    NSLog(@"fail:%@",error);
       }];
    }

二、一個POST請求的前世今生

啓動測試App,點擊按鈕。接下里讓我們看看AFN是如何優雅的管理網絡請求的。

1.初始化

  • AFHTTPSessionManager的初始化

    self.managerAFHTTPSessionManager的實例,它使用類方法+ manager初始化。這個類方法最終調用如下方法:

    - (instancetype)initWithBaseURL:(NSURL *)url
             sessionConfiguration:(NSURLSessionConfiguration *)configuration
    {
      self = [super initWithSessionConfiguration:configuration];//①configuration=nil
      if (!self) {
          return nil;
      }
    
      // Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
      if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
          url = [url URLByAppendingPathComponent:@""];
      }
    
      self.baseURL = url;
    
      self.requestSerializer = [AFHTTPRequestSerializer serializer];//②
      self.responseSerializer = [AFJSONResponseSerializer serializer];//③
    
      return self;
    }

    ①中調用父類的方法初始化父類相關屬性,這包括:

    operationQueue
    responseSerializer(爲AFJSONResponseSerializer實例)
    securityPolicy(爲默認,即不進行SSL驗證)
    reachabilityManager
    mutableTaskDelegatesKeyedByTaskIdentifier(管理{taskID:delegate})
    lock(用於操作mutableTaskDelegatesKeyedByTaskIdentifier時保證線程安全)
    session(NSURLSession實例,設置其代理爲self,queue爲operationQueue)

    ②中設置其requestSerializer爲AFHTTPRequestSerializer實例,③重設其responseSerializer爲AFJSONResponseSerializer實例。

  • AFHTTPRequestSerializer的初始化

    requestSerializer被設置成了AFHTTPRequestSerializer的實例,初始化是通過類方法+serializer實現的。內容包括:

    stringEncoding(爲NSUTF8StringEncoding)
    mutableHTTPRequestHeaders(保存用戶設置的header,默認會添加"Accept-Language"和"User-Agent",其中"User-Agent"還進行了ICU轉換以保證必須爲ASCII字符)
    HTTPMethodsEncodingParametersInURI(爲GET、HEAD、DELETE)
    mutableObservedChangedKeyPaths (監聽"allowsCellularAccess"、"cachePolicy"、"HTTPShouldHandleCookies"、"HTTPShouldUsePipelining"、"networkServiceType"、"timeoutInterval",一旦用戶設置了這6個屬性,則對應屬性的名會被存入,最後其值會加到request中去,本質是增量添加:“誰改變添加誰”)
  • AFJSONResponseSerializer的初始化

    responseSerializer被設置成爲了AFJSONResponseSerializer的實例,初始化是通過類方法+serializer實現的。內容包括:

    readingOptions(爲0)
    acceptableStatusCodes(可接受的code,初始化爲[200~199])
    acceptableContentTypes(可接受的contenttype,設置爲application/json、text/json、text/javascript)
    注:其父類AFHTTPResponseSerializer的acceptableContentTypes=nil,即接受任何Content。

至此必要的初始化已完成。

2.發起請求

(1) POST

進入到POST函數中:

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
                                headers:(nullable NSDictionary <NSString *, NSString *> *)headers
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
{
    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters headers:headers uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure];

    [dataTask resume];

    return dataTask;
}

即最終生成一個dataTask,然後resume啓動,並將dataTask返回。

(2) 生成dataTask

生成的dataTask是通過如下函數實現的,本例中傳入的參數值也已標明:

-  dataTaskWithHTTPMethod:  //@"POST"
                URLString:  //@"http://www.layne.com"
               parameters:  //@{@"name":@"layne",@"age":@30}
                  headers:  //@{@"TestName":@"myTest"} 
           uploadProgress:  //nil
         downloadProgress:  //nil
                  success:  //successBlock{NSLog(@"success:%@",responseObject);}
                  failure:  //failureBlock{NSLog(@"fail:%@",error);}

其內部執行的步驟如下:

Step1:調用requestSerializer的如下方法創建mutableRequest:

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method   //@"POST"
                                 URLString:(NSString *)URLString//@"http://www.layne.com"
                                parameters:(id)parameters//@{@"name":@"layne",@"age":@30}
                                     error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(URLString);

    NSURL *url = [NSURL URLWithString:URLString];

    NSParameterAssert(url);

    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method;//設置爲POST

    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }//根據用戶是否設置了allowsCellularAccess、cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining、networkServiceType、timeoutInterval來設置mutableRequest對應字段值。本例中由於未進行任何設置,因此上述邏輯不會有效果。

    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];//①

    return mutableRequest;
}

①是調用AFURLRequestSerialization協議方法對mutableRequest進行進一步處理,包括:

  • 設置默認header,如在requestSerializer初始化時設置默認header:User-AgentAccept-Language

    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
          if (![request valueForHTTPHeaderField:field]) {
              [mutableRequest setValue:value forHTTPHeaderField:field];
          }
    }];
  • 處理參數

    若用戶自定義了處理參數的block(self.queryStringSerialization),則使用該block;否則使用默認的處理方式,會被最終處理成如下格式:name=layne&age=30

  • 根據HTTPMethod決定參數串放到哪裏:GET/HEAD/DELETE直接拼接到url;其他(如POST)方法放到HTTPBody中(使用stringEncoding(NSUTF8StringEncoding)編碼),並設置Content-Typeapplication/x-www-form-urlencoded

至此,生成的mutableRequest結構如下:

mutableRequest{
URL:          "http://www.layne.com"
Method:       "POST"
Header:       "Accept-Language" = "en;q=1";
              "User-Agent" = "TestApp/1.0 (iPhone; iOS 13.1; Scale/3.00)";
              "Content-Type" = "application/x-www-form-urlencoded";
HttpBody      <6167653d 3330266e 616d653d 6c61796e 65>  //name=layne&age=30(utf-8)
}

Step2:合併傳入的header

for (NSString *headerField in headers.keyEnumerator) {
        [request addValue:headers[headerField] forHTTPHeaderField:headerField];
}

執行之後mutableRequest結構如下:

mutableRequest{
URL:          "http://www.layne.com"
Method:       "POST"
Header:       "Accept-Language" = "en;q=1";
              "User-Agent" = "TestApp/1.0 (iPhone; iOS 13.1; Scale/3.00)";
              "Content-Type" = "application/x-www-form-urlencoded";
              "TestName":"myTest";
HttpBody      <6167653d 3330266e 616d653d 6c61796e 65>  //name=layne&age=30(utf-8)
}

Step3:判斷Step1中生成mutableRequest的過程是否出錯,若出錯,則調用failureBlock並返回。

if (serializationError) {
        if (failure) {
            dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
                failure(nil, serializationError);
            });
        }
        return nil;
 }

Step4:根據mutableRequest生成dataTask。

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request //request
                               uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock//nil
                             downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock//nil
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler /*block{
                                  if (error) {
                                      if (failure) {
                                          failure(dataTask, error);
                                      }
                                  } else {
                                      if (success) {
                                          success(dataTask, responseObject);
                                      }
                                  }        
                                             }*/
{

    __block NSURLSessionDataTask *dataTask = nil;
    url_session_manager_create_task_safely(^{ //①
        dataTask = [self.session dataTaskWithRequest:request];
    });

    [self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];//②

    return dataTask;//③
}

說明:

①中不直接生成dataTask,是因爲在iOS8以下的系統上,若是在並行隊列上創建dataTask會導致completionHandler調用出錯。因此爲了解決這個問題,針對iOS8以下系統,AFN使用自己維護的一個串行隊列來創建dataTask。具體問題描述如下:

Due to this bug: http://openradar.appspot.com/radar?id=5871104061079552 in NSURLSessionTask, creating tasks on a concurrent queue can cause incorrect completionHandlers to get called.

When a duplicate taskIdentifier is returned by the task, the previous completionHandler gets cleared out and replaced with the new one. If the data for the first request comes back before the second request's data, the first response is then called against the second completionHandler.

I'm not sure what AFNetworking should do here — it could enforce creating tasks on a serial queue or it could just advise people to do so. We could also add a test to assert that the taskIdentifier is not a duplicate?

②的作用是爲dataTask生成對應的delegate,以調用回調方法。

- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask //dataTask
                uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock//nil
              downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock//nil
             completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler/*block{
                                  if (error) {
                                      if (failure) {
                                          failure(dataTask, error);
                                      }
                                  } else {
                                      if (success) {
                                          success(dataTask, responseObject);
                                      }
                                  }        
                                             }*/
{
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];//a
    delegate.manager = self;//b
    delegate.completionHandler = completionHandler;//c

    dataTask.taskDescription = self.taskDescriptionForSessionTasks;//d
    [self setDelegate:delegate forTask:dataTask];//e

    delegate.uploadProgressBlock = uploadProgressBlock;//nil
    delegate.downloadProgressBlock = downloadProgressBlock;//nil
}

a. 爲dataTask生成AFURLSessionManagerTaskDelegate實例。delegate內部維護着:

一個mutableData用來保存收到的response data。

一個uploadProgress和downloadProgress用來標明上傳/下載進度。它們的cancel、suspend和resume操作與dataTask進行了綁定,即通過對它們進行cancel、suspend和resume操作就可以操作dataTask的cancel、suspend和resume。此外,還採用KVO監聽uploadProgress和downloadProgress的進度變化,從而調用用戶自定義的block:uploadProgressBlockdownloadProgressBlock

b. delegate弱引用當前的manager。

c.delegate保存完成回調。

③返回最終的dataTask,並啓動。

(3) 處理Response

AFN是基於NSURLSession的,涉及以下幾個協議:

NSURLSessionDelegate

 - URLSession:didBecomeInvalidWithError:
 - URLSession:didReceiveChallenge:completionHandler:
 - URLSessionDidFinishEventsForBackgroundURLSession:

NSURLSessionTaskDelegate

 - URLSession:willPerformHTTPRedirection:newRequest:completionHandler:
 - URLSession:task:didReceiveChallenge:completionHandler:
 - URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
 - URLSession:task:needNewBodyStream:
 - URLSession:task:didCompleteWithError:
 - URLSession:task:didFinishCollectingMetrics:

NSURLSessionDataDelegate

 - URLSession:dataTask:didReceiveResponse:completionHandler:
 - URLSession:dataTask:didBecomeDownloadTask:
 - URLSession:dataTask:didReceiveData:
 - URLSession:dataTask:willCacheResponse:completionHandler:

NSURLSessionDownloadDelegate

 - URLSession:downloadTask:didFinishDownloadingToURL:
 - URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:
 - URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:

回調調用順序:

① URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:

由於使用的是POST方式,因此該回調是首先調用的。邏輯:找到task對應的delegate=>調用delegate的URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:來更新delegate維護的uploadProgress。若用戶自定義了taskDidSendBodyData block,則調用。

② URLSession:task:didFinishCollectingMetrics:

該方法調用時機不定,用來收集整個請求的信息。邏輯:找到task對應的delegate=>調用delegate的URLSession:task:didFinishCollectingMetrics:來爲delegate.sessionTaskMetrics賦值。若用戶自定義了taskDidFinishCollectingMetrics block,則調用。

③ URLSession:dataTask:didReceiveData:
收到response data時執行。邏輯:找到task對應的delegate=>調用delegate的URLSession:task:didCompleteWithError:來更新delegate內部維護的downloadProgress並使用mutableData保存response data。若用戶自定義了dataTaskDidReceiveData block,則調用。

④ URLSession:task:didCompleteWithError:

這是最後執行的回調。邏輯:找到task對應的delegate=>調用delegate的URLSession:task:didCompleteWithError:,並刪除delegate和task的對應關係。若用戶自定義了taskDidComplete block,則調用。

數據的處理主要在delegate的URLSession:task:didCompleteWithError:中,主要做了以下幾件工作:

  • 構造userInfo字典:

    userInfo{
    AFNetworkingTaskDidCompleteResponseSerializerKey:<AFJSONResponseSerializer: 0x600003db6820>,
    AFNetworkingTaskDidCompleteSessionTaskMetrics:"...",
    AFNetworkingTaskDidCompleteResponseDataKey:"<length=24,bytes=0x567283d>"//response data
    /*若出錯則會包含以下key*/
    AFNetworkingTaskDidCompleteErrorKey:"error" //error數據
    }
  • 若出錯,則直接調用completeHandler回調,之後將上面的userInfo以通知AFNetworkingTaskDidCompleteNotification的形式廣播出去。

  • 若未出錯,則使用responseSerializer(AFJSONResponseSerializer實例)將data轉換爲NSDictionary。若轉換過程未出錯,則在userInfo中增加{AFNetworkingTaskDidCompleteSerializedResponseKey:responseObject};若出錯,則增加{AFNetworkingTaskDidCompleteErrorKey:error}。之後調用completeHandler回調,並將userInfo以通知AFNetworkingTaskDidCompleteNotification的形式廣播出去。

經過上面的處理,最終要麼error要麼responseObject會返回到POST的回調中去。整個網絡請求就完成了。

三、AFNetworking中的乾貨

上面我們跟着一個簡單的POST請求瞭解了AFN的整個工作流程,但還有一些細節我覺得還是值得我們學習的。

  1. 如果response不是標準格式的JSON數據或者我們需要對原始data進行加密解密操作該如何做?

    答:將responseSerializer設置爲AFHTTPResponseSerializer的實例,這樣response data 會以原始data的形式返回給上層。AFHTTPResponseSerializer僅針對code和contenttype進行校驗,而默認的AFJSONResponseSerializer除了校驗code和contenttype之外還對JSON格式進行校驗,並直接解析成NSDictionary。當然,如果想對response data想要做最全面的自定義處理,最直接的方式當然是自定義AFHTTPResponseSerializer的子類,並重寫協議方法

    - (id)responseObjectForResponse:(NSHTTPURLResponse *)response
                              data:(NSData *)data
                             error:(NSError *__autoreleasing *)error
  2. 如果返回的JSON數據中包含NSNull數據該如何處理?

    答:將responseSerializer(AFJSONResponseSerializer實例)的removesKeysWithNullValues屬性設置爲YES。這樣一來在JSON轉換爲NSDictionary之後會將NSDictionary中的NSNull值去除。

  3. 如果我想自己更改請求參數的格式(即不用默認的name=layne&age=30這種)該如何設置?

    答:調用requestSerializer的-setQueryStringSerializationWithBlock:設置自定義的格式。在requestSerializer處理參數的時候會先去判斷queryStringSerialization block是否爲空,若不爲空,則使用該block處理參數。若爲空,則使用默認的格式(如name=layne&age=30)生成參數串。

  4. 除了使用GET/POST方法的回調,還可以通過什麼方式獲得網絡請求結果?

    答:還可以使用notification。AFN中有一個名爲AFNetworkingTaskDidCompleteNotification的通知,可以通過監聽該通知獲取網絡請求結果。AFURLSessionManagerTaskDelegate的URLSession:task:didCompleteWithError:中在調用completeHandler之後會發送通知:

    dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
                   if (self.completionHandler) {
                       self.completionHandler(task.response, responseObject, serializationError);//回調
                   }
    
                   dispatch_async(dispatch_get_main_queue(), ^{
                       [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];//將task和userInfo一起廣播出去
                   });
               });
  5. 如何更改回調所在的線程?

    答:指定manager的completeQueue。這樣回調就會在其他線程中執行。

    _manager.completionQueue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
  6. AFURLSessionManager如何獲取NSURLSession維護的task?

    答:使用getTasksWithCompletionHandler:API並使用信號量保證線程安全。

    - (NSArray *)tasksForKeyPath:(NSString *)keyPath { //tasks、uploadTask、downloadTasks
       __block NSArray *tasks = nil;
       dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
       [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
           if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
               tasks = dataTasks;
           } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
               tasks = uploadTasks;
           } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
               tasks = downloadTasks;
           } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
               tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
           }
    
           dispatch_semaphore_signal(semaphore);
       }];
    
       dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
       return tasks;
    }
  7. 若我想取消一個剛發起的網絡請求該如何做?

    答:AFHTTPSessionManager的POST/GET方法返回的是task對象,直接[task cancel]即可。如:

    NSURLSessionDataTask *task = [self.manager POST:@"http://www.layne.com" 
               parameters:@{@"name":@"layne",@"age":@30} 
                  headers:@{@"TestName":@"myTest"} 
                 progress:nil 
            success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                    NSLog(@"success:%@",responseObject);
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                    NSLog(@"fail:%@",error);
       }];
    [task cancel];//直接cancel
  8. 若我想取消所有task該如何做?

    答:使用如下方法。建議resetSession傳入YES將session重置,否則下次網絡請求會crash,並提示:“Attempted to create a task in a session that has been invalidated

    - (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks resetSession:(BOOL)resetSession {
       if (cancelPendingTasks) {
           [self.session invalidateAndCancel];
       } else {
           [self.session finishTasksAndInvalidate];
       }
       if (resetSession) {
           self.session = nil;
       }
    }
    例如:
    [self.manager invalidateSessionCancelingTasks:YES resetSession:YES];

至此AFNetworking是如何工作的我們就知道了。看了源碼之後不得不感嘆作者真是神人,不僅優雅的給出了NSURLSession的使用範例,而且還包含了業務層面的巧妙設計。嗯,閱讀源碼使我快樂^_^。

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