溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

AFNetworking到底長啥樣(下)

發(fā)布時間:2020-07-27 09:34:35 來源:網絡 閱讀:414 作者:zlayne 欄目:移動開發(fā)

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

一、環(huán)境搭建

  1. 服務器配置

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

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

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

    - (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是如何優(yōu)雅的管理網絡請求的。

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 (監(jiān)聽"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.發(fā)起請求

(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);}

其內部執(zhí)行的步驟如下:

Step1:調用requestSerializer的如下方法創(chuàng)建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協(xié)議方法對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];
}

執(zhí)行之后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以下的系統(tǒng)上,若是在并行隊列上創(chuàng)建dataTask會導致completionHandler調用出錯。因此為了解決這個問題,針對iOS8以下系統(tǒng),AFN使用自己維護的一個串行隊列來創(chuàng)建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監(jiān)聽uploadProgress和downloadProgress的進度變化,從而調用用戶自定義的block:uploadProgressBlockdownloadProgressBlock

b. delegate弱引用當前的manager。

c.delegate保存完成回調。

③返回最終的dataTask,并啟動。

(3) 處理Response

AFN是基于NSURLSession的,涉及以下幾個協(xié)議:

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時執(zhí)行。邏輯:找到task對應的delegate=>調用delegate的URLSession:task:didCompleteWithError:來更新delegate內部維護的downloadProgress并使用mutableData保存response data。若用戶自定義了dataTaskDidReceiveData block,則調用。

④ URLSession:task:didCompleteWithError:

這是最后執(zhí)行的回調。邏輯:找到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>"http://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的整個工作流程,但還有一些細節(jié)我覺得還是值得我們學習的。

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

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

    - (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的通知,可以通過監(jiān)聽該通知獲取網絡請求結果。AFURLSessionManagerTaskDelegate的URLSession:task:didCompleteWithError:中在調用completeHandler之后會發(fā)送通知:

    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。這樣回調就會在其他線程中執(zhí)行。

    _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. 若我想取消一個剛發(fā)起的網絡請求該如何做?

    答: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是如何工作的我們就知道了??戳嗽创a之后不得不感嘆作者真是神人,不僅優(yōu)雅的給出了NSURLSession的使用范例,而且還包含了業(yè)務層面的巧妙設計。嗯,閱讀源碼使我快樂^_^。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI