AFNetworking(v3.1.0)源碼解析

注:本文始發于個人 GitHub 項目 ShannonChenCHN/iOSDevLevelingUp

源碼倉庫地址:https://github.com/AFNetworking/AFNetworking

AFNetworking 作為我們最基礎的網絡框架,目前在 GitHub 上 Objective-C 語言類排名第一,幾乎每個涉及到網絡請求的 APP 都會用到,其重要性可見一斑。再者,作為 iOS 開發領域最受歡迎的開源項目,其中凝聚了眾多大神的智慧,無論是在技術點上,還是架構設計上、問題處理方式上,都具有很高的學習價值。

這兩天正好趁著假期有空,可以跟著前人總結的一些精華,仔細研讀一下這個優秀的網絡框架的實現。站在巨人的肩膀上,才能看得遠。

這篇文章先從整體架構開始,再從實際使用案例入手,梳理一下核心邏輯,然后再依次了解下各個具體模塊的實現,最后再回顧一下 2.x 版本的實現,總結一下 AFNetworking 的價值。

注:這篇文章不會逐行分析源碼,具體的代碼注釋見 這里

目錄

  • 一、架構
  • 二、核心邏輯
  • 三、AFURLSessionManager
    • 3.1 線程
    • 3.2 AFURLSessionManagerTaskDelegate
    • 3.3 NSProgress
    • 3.4 NSSecureCoding
    • 3.5 _AFURLSessionTaskSwizzling
  • 四、AFURLRequestSerialization
    • 4.1 構建普通請求
    • 4.2 構建 multipart 請求
  • 五、AFURLResponseSerialization
    • 5.1 -validateResponse:data:error: 方法
    • 5.2 -responseObjectForResponse:data:error: 方法
  • 六、AFSecurityPolicy
    • 6.1 預備知識點
      • 6.1.1 為什么要使用 HTTPS
      • 6.1.2 HTTPS 的出現
      • 6.1.3 SSL/TLS 協議
      • 6.1.4 HTTPS 與 HTTP 的區別是什么?
      • 6.1.5 HTTPS 連接的建立過程
      • 6.1.6 HTTPS 傳輸時是如何驗證證書的?怎樣應對中間人偽造證書?
      • 6.1.7 Certificate Pinning 是什么?
    • 6.2 AFSecurityPolicy 的實現
  • 七、AFNetworkReachabilityManager
  • 八、UIKit 擴展
  • 九、AFNetworking 2.x
  • 十、AFNetworking 的價值
    • 10.1 請求調度:NSURLConnection + NSOperation
    • 10.2 更高層次的抽象
    • 10.3 block
    • 10.4 模塊化
  • 十一、問題
  • 十二、收獲

一、架構

AFNetworking 一共分為 5 個模塊,2 個核心模塊和 3 個輔助模塊:

  • Core
    • NSURLSession(網絡通信模塊)
      • AFURLSessionManager(封裝 NSURLSession)
      • AFHTTPSessionManager(繼承自 AFURLSessionManager,實現了 HTTP 請求相關的配置)
    • Serialization
      • AFURLRequestSerialization(請求參數序列化)
        • AFHTTPRequestSerializer
        • AFJSONRequestSerializer
        • AFPropertyListRequestSerializer
      • AFURLResponseSerialization(驗證返回數據和反序列化)
        • AFHTTPResponseSerializer
        • AFJSONResponseSerializer
        • AFXMLParserResponseSerializer
        • AFXMLDocumentResponseSerializer (Mac OS X)
        • AFPropertyListResponseSerializer
        • AFImageResponseSerializer
        • AFCompoundResponseSerializer
  • Additional Functionality
    • Security(網絡通信安全策略模塊)
    • Reachability(網絡狀態監聽模塊)
    • UIKit(對 iOS 系統 UI 控件的擴展)
image

<div align="center">圖 1 AFNetworking 整體架構</div>

二、核心邏輯

先來看一下如何使用 AFNetworking 發送一個 GET 請求:

NSURL *url = [[NSURL alloc] initWithString:@"https://news-at.zhihu.com"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
[manager GET:@"api/4/news/latest" parameters:nil progress:nil
    success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"%@" ,responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"%@", error);
    }];

首先使用一個 URL,通過調用 -initWithBaseURL: 方法創建了一個 AFHTTPSessionManager 的實例,然后再調用 -GET:parameters:progress:success:failure: 方法發起請求。

-initWithBaseURL: 方法的調用棧如下:

- [AFHTTPSessionManager initWithBaseURL:]
    - [AFHTTPSessionManager initWithBaseURL:sessionConfiguration:]
        - [AFURLSessionManager initWithSessionConfiguration:]
            - [NSURLSession sessionWithConfiguration:delegate:delegateQueue:]
            - [AFJSONResponseSerializer serializer] // 負責序列化響應
            - [AFSecurityPolicy defaultPolicy] // 負責身份認證
            - [AFNetworkReachabilityManager sharedManager] // 查看網絡連接情況
            - [AFHTTPRequestSerializer serializer] // 負責序列化請求
            - [AFJSONResponseSerializer serializer] // 負責序列化響應

AFURLSessionManager 是 AFHTTPSessionManager 的父類,
AFURLSessionManager 負責創建和管理 NSURLSession 的實例,管理 AFSecurityPolicy 和初始化 AFNetworkReachabilityManager,來保證請求的安全和查看網絡連接情況,它有一個 AFJSONResponseSerializer 的實例來序列化 HTTP 響應。

AFHTTPSessionManager 有著自己的 AFHTTPRequestSerializer 和 AFJSONResponseSerializer 來管理請求和響應的序列化,同時依賴父類實現發出 HTTP 請求、管理 Session 這一核心功能。

-GET:parameters:progress:success:failure: 方法的調用棧:

 - [AFHTTPSessionManager GET:parameters:process:success:failure:]
    - [AFHTTPSessionManager dataTaskWithHTTPMethod:parameters:uploadProgress:downloadProgress:success:failure:] // 返回一個 NSURLSessionDataTask 對象
        - [AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:] // 返回 NSMutableURLRequest
        - [AFURLSessionManager dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:] 返回一個 NSURLSessionDataTask 對象
            - [NSURLSession dataTaskWithRequest:] 返回一個 NSURLSessionDataTask 對象
            - [AFURLSessionManager addDelegateForDataTask:uploadProgress:downloadProgress:completionHandler:]
                - [AFURLSessionManagerTaskDelegate init]
                - [AFURLSessionManager setDelegate:forTask:] // 為每個 task 創建一個對應的 delegate
    - [NSURLSessionDataTask resume]

發送請求的核心在于創建和啟動一個 data task,AFHTTPSessionManager 只是提供了 HTTP 請求的接口,內部最終還是調用了父類 AFURLSessionManager 來創建 data task(其實也就是通過 NSURLSession 創建的 task),AFURLSessionManager 中會為每個 task 創建一個對應的 AFURLSessionManagerTaskDelegate 對象,用來處理回調。

在請求發起時有一個序列化的工具類 AFHTTPRequestSerializer 來處理請求參數。

請求回調時的方法調用棧:

- [AFURLSessionManager  URLSession:task:didCompleteWithError:]
  - [AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]
    - [AFJSONResponseSerializer responseObjectForResponse:data:error:]  // 解析 JSON 數據
      - [AFHTTPResponseSerializer validateResponse:data:]  // 驗證數據
    - [AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]_block_invoke_2.150
      - [AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:]_block_invoke

AFURLSessionManager 在代理方法中收到服務器返回數據的后,會交給 AFURLSessionManagerTaskDelegate 去處理,接著就是用 AFJSONResponseSerializer 去驗證和解析 JSON 數據,最后再通過 block 回調的方式返回最終結果。

三、AFURLSessionManager

AFURLSessionManager 是 AFHTTPSessionManager 的父類,主要有以下幾個功能:

  • 負責創建和管理 NSURLSession
  • 管理 NSURLSessionTask
  • 實現 NSURLSessionDelegate 等協議中的代理方法
  • 使用 AFURLSessionManagerTaskDelegate 管理上傳、下載進度,以及請求完成的回調
  • 將整個請求流程相關的組件串聯起來
  • 負責整個請求過程的線程調度
  • 使用 AFSecurityPolicy 驗證 HTTPS 請求的證書

1. 線程

一般調用 AFNetworking 的請求 API 時,都是在主線程,也是主隊列。然后直到調用 NSURLSession 的 -resume 方法,一直都是在主線程。

在 AFURLSessionManager 的初始化方法中,設置了 NSURLSession 代理回調線程的最大并發數為 1,因為就像 NSURLSession 的 -sessionWithConfiguration:delegate:delegateQueue: 方法的官方文檔中所說的那樣,所有的代理方法回調都應該在一個串行隊列中,因為只有這樣才能保證代理方法的回調順序。

NSURLSession 代理方法回調是異步的,所以收到回調時的線程模式是“異步+串行隊列”,這個時候可以理解為處于回調線程。

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    ...
    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;  // 代理回調線程最大并發數為 1

    // 初始化 NSURLSession 對象
    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
    
    ...
    return self;
}

收到代理回調后,接著在 AFURLSessionManagerTaskDelegate 的 -URLSession:task:didCompleteWithError: 方法中,異步切換到 processing queue 進行數據解析,數據解析完成后再異步回到主隊列或者自定義隊列。

- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    
    ...
    
    // 如果請求成功,則在一個 AF 的并行 queue 中,去做數據解析等后續操作
    dispatch_async(url_session_manager_processing_queue(), ^{
        NSError *serializationError = nil;
        responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
        
        ...
        
        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_group_async 將任務放到隊列組中去執行,搜了一下也沒看到這個組中的任務執行完了要做什么,難道是要留給外面的調用方用的?

image

<div align="center">圖 2 AFNetworking 中的線程調度</div>

2. AFURLSessionManagerTaskDelegate

AFURLSessionManager 中幾乎實現了所有的 NSURLSession 相關的協議方法:

  • NSURLSessionDelegate
  • NSURLSessionTaskDelegate
  • NSURLSessionDataDelegate
  • NSURLSessionDownloadDelegate

但是AFURLSessionManager 中實現的這些代理方法都只是做一些非核心邏輯的處理,每個代理方法中都回調了一個自定義邏輯的 block,如果 block 被賦值了,那么就調用它。

AFURLSessionManager 把最核心的代理回調處理交給 AFURLSessionManagerTaskDelegate 類去實現了,AFURLSessionManagerTaskDelegate 可以根據對應的 task 去進行上傳、下載進度回調和請求完成的回調處理:

- URLSession:task:didCompleteWithError:
- URLSession:dataTask:didReceiveData:
- URLSession:downloadTask:didFinishDownloadingToURL:

AFURLSessionManager 通過屬性 mutableTaskDelegatesKeyedByTaskIdentifier (一個 NSDictionary 對象)來存儲并管理每一個 NSURLSessionTask 所對應的 AFURLSessionManagerTaskDelegate,它以 taskIdentifier 為鍵存儲 task。在請求最終完成后,又將 AFURLSessionManagerTaskDelegate 移除。

image

<div align="center">圖 3 AFNetworking 中的代理回調邏輯</div>

3. NSProgress

AFURLSessionManagerTaskDelegate 借助了 NSProgress 這個類來實現進度的管理,NSProgress 是 iOS 7 引進的一個用來管理任務進度的類,可以表示一個任務的進度信息,我們還可以對其進行開始
暫停、取消等操作,完整的對應了 task 的各種狀態。

AFURLSessionManagerTaskDelegate 通過 KVO 監聽 task 的進度更新,來同步更新 NSProgress 的進度數據。同時,還用 KVO 監聽了 NSProgress 的 fractionCompleted 屬性的變化,用來更新最外面的進度回調 block,回調時將這個 NSProgress 對象作為參數帶過去。

另一方面,AFURLSessionManagerTaskDelegate 中還分別對下載和上傳的 NSProgress 對象設置了開始、暫停、取消等操作的 handler,將 task 跟 NSProgress 的狀態關聯起來。這樣一來,就可以通過控制 NSProgress 對象的這些操作就可以控制 task 的狀態。

延伸閱讀:

4. NSSecureCoding

AFNetworking 的大多數類都支持歸檔解檔,但實現的是 NSSecureCoding 協議,而不是 NSCoding 協議,這兩個協議的區別在于 NSSecureCoding 協議中定義的解碼的方法是 -decodeObjectOfClass:forKey: 方法,而不是 -decodeObjectForKey:,這就要求解數據時要指定 Class。在 bang 的文章中看到說是這樣做更安全,因為序列化后的數據有可能被篡改,若不指定 Class,decode 出來的對象可能不是原來的對象,有潛在風險。(不過暫時還是沒能理解。)

5. _AFURLSessionTaskSwizzling

_AFURLSessionTaskSwizzling 的唯一作用就是將 NSURLSessionTask 的 -resume-suspend 方法實現替換成自己的實現,_AFURLSessionTaskSwizzling 中這兩個方法的實現是先調用原方法,然后再發出一個通知。

_AFURLSessionTaskSwizzling 是通過在 +load 方法中進行 Method Swizzling 來實現方法交換的,由于 NSURLSessionTask 的實現是類簇,不能直接通過調用 +class 來獲取真正的類,而且在 iOS 7 和 iOS 8 下的實現不同,所以這里的 swizzling 實現起來有點復雜。具體原因見 GitHub 上的討論

問題:
有點不明白的是,NSURLSessionTask 有三個子類:NSURLSessionDataTask、NSURLSessionDownloadTask 和 NSURLSessionUploadTask,為什么不用考慮這三個子類自己也實現了自己的 -resume-suspend 方法的情況呢?

四、AFURLRequestSerialization

AFURLRequestSerialization 是一個抽象的協議,用于構建一個規范的 NSURLRequest。基于 AFURLRequestSerialization 協議,AFNetworking 提供了 3 中不同數據形式的序列化工具(當然你也可以自定義其他數據格式的序列化類):

  • AFHTTPRequestSerializer:普通的 HTTP 請求,默認數據格式是 application/x-www-form-urlencoded,也就是 key-value 形式的 url 編碼字符串
  • AFJSONRequestSerializer:參數格式是 json
  • AFPropertyListRequestSerializer:參數格式是蘋果的 plist 格式
image

<div align="center">圖 4 AFURLRequestSerialization 類圖</div>

AFHTTPRequestSerializer 主要實現了兩個功能:

  • 構建普通請求:格式化請求參數,生成 HTTP Header。
  • 構建 multipart 請求,上傳數據時會用到。

1. 構建普通請求

AFHTTPRequestSerializer 在構建普通請求時,做了以下幾件事:

  • 創建 NSURLRequest
  • 設置 NSURLRequest 相關屬性
  • 設置 HTTP Method
  • 設置 HTTP Header
  • 序列化請求參數
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters
                                     error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(URLString);

    NSURL *url = [NSURL URLWithString:URLString];

    NSParameterAssert(url);

    // 創建請求
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method; // 設置 Method

    // 這里本來是直接把 self 的一些屬性值直接傳給 request 的,但是因為初始默認情況下,
    // 當前類中與 NSURLRequest 相關的那些屬性值為 0,而導致外面業務方使用 NSURLSessionConfiguration 設置屬性時失效,
    // 所以通過對這些屬性添加了 KVO 監聽判斷是否有值來解決這個傳值的有效性問題
    // 詳見 https://github.com/AFNetworking/AFNetworking/commit/49f2f8c9a907977ec1b3afb182404ae0a6bce883
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }

    // 設置 HTTP header;請求參數序列化,再添加到 query string 或者 body 中
    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];

    return mutableRequest;
}

在設置 NSURLRequest 相關屬性時,有點繞,本來可以直接將 AFHTTPRequestSerializer 自己的屬性值傳給 NSURLRequest 對象的,但是后來改成了 KVO 的形式,主要是因為 NSURLRequest 對象有些屬性是純量數據類型(比如 timeoutInterval),在 AFHTTPRequestSerializer 初始化后,這些跟 NSURLRequest 相關的屬性值初始默認值是 0,所以是不知道外面有沒有設置過值,如果將 AFHTTPRequestSerializer 的值都傳給 NSURLRequest 對象的話,很有可能會導致 NSURLSessionConfiguration 中設置的相同屬性失效。

AFNetworking 幫我們組裝好了一些 HTTP 請求頭,包括:

  • Content-Type,請求參數類型
  • Accept-Language,根據 [NSLocale preferredLanguages] 方法讀取本地語言,告訴服務端自己能接受的語言。
  • User-Agent
  • Authorization,提供 Basic Auth 認證接口,幫我們把用戶名密碼做 base64 編碼后放入 HTTP 請求頭。

一般我們請求都會按 key=value 的方式帶上各種參數,GET 方法參數直接拼在 URL 后面,POST 方法放在 body 上,NSURLRequest 沒有封裝好這個參數的序列化,只能我們自己拼好字符串。AFHTTPRequestSerializer 提供了接口,讓參數可以是 NSDictionary, NSArray, NSSet 這些類型,再由內部解析成字符串后賦給 NSURLRequest。

參數序列化流程大概是這樣的:

  • 用戶傳進來的數據,支持包含 NSArray,NSDictionary,NSSet 這三種數據結構。
  • 先將每組 key-value 轉成 AFQueryStringPair 對象的形式,保存到數組中(這樣做的目的是因為最后可以根據不同的字符串編碼生成對應的 key=value 字符串)
  • 然后取出數組中的 AFQueryStringPair 對象,轉成一個個 NSString 對象再保存到新數組中
  • 最后再將這些 key=value 的字符串用 & 符號拼接起來
@{
     @"name"    : @"steve",
     @"phone"   : @{@"mobile": @"xx", @"home": @"xx"},
     @"families": @[@"father", @"mother"],
     @"nums"    : [NSSet setWithObjects:@"1", @"2", nil]
}
                    ||
                    \/
@[
     field: @"name",          value: @"steve",
     field: @"phone[mobile]", value: @"xx",
     field: @"phone[home]",   value: @"xx",
     field: @"families[]",    value: @"father",
     field: @"families[]",    value: @"mother",
     field: @"nums",          value: @"1",
     field: @"nums",          value: @"2",
]
                    ||
                    \/
                    
@[
    @"name=steve",        // 注:實際代碼中這里的 “=” 會被編碼
    @"phone[mobile]=xx",
    @"phone[home]=xx",
    @"families[]=father",
    @"families[]=mother",
    @"nums=1",
    @"nums=2"
]
                    ||
                    \/
                    
@"name=steve&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&nums=2"

請求參數序列化完成后,再根據不同的 HTTP 請求方法分別處理,對于 GET/HEAD/DELETE 方法,把參數直接加到 URL 后面,對于其他如 POST/PUT 等方法,把數據加到 body 上,并設好 HTTP 頭中的 Content-Typeapplication/x-www-form-urlencoded,告訴服務端字符串的編碼是什么。

2. 構建 multipart 請求

這部分有點復雜,暫時還沒看。

五、AFURLResponseSerialization

AFURLResponseSerialization 模塊負責解析網絡返回數據,檢查數據是否合法,把服務器返回的 NSData 數據轉成相應的對象。
AFURLResponseSerialization 模塊包括一個協議、一個基類和多個解析特定格式數據的子類,用戶可以很方便地繼承基類 AFHTTPResponseSerializer 去解析更多的數據格式:

  • AFURLResponseSerialization 協議,定義了解析響應數據的接口
  • AFHTTPResponseSerializer, HTTP 請求響應數據解析器的基類
  • AFJSONResponseSerializer,專門解析 JSON 數據的解析器
  • 其他數據格式(XML、image、plist等)的響應解析器
  • AFCompoundResponseSerializer,組合解析器,可以將多個解析器組合起來,以同時支持多種格式的數據解析

image

<div align="center">圖 5 AFURLResponseSerialization 類圖</div>

AFURLResponseSerialization 模塊響應解析機制主要涉及到兩個核心方法:

  • AFHTTPResponseSerializer 中定義、實現的 -validateResponse:data:error: 方法
  • AFURLResponseSerialization 協議定義的 -responseObjectForResponse:data:error: 方法

1. -validateResponse:data:error: 方法

AFHTTPResponseSerializer 作為解析器基類,提供了 acceptableContentTypesacceptableStatusCodes 兩個屬性,并提供了 acceptableStatusCodes 的默認值,子類可以通過設置這兩個屬性的值來進行自定義配置。AFHTTPResponseSerializer 中的 -validateResponse:data:error: 方法會根據這兩個屬性值來判斷響應的文件類型 MIMEType 和狀態碼 statusCode 是否合法。

比如 AFJSONResponseSerializer 中設置了 acceptableContentTypes 的值為 [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil],如果服務器返回的 Content-Type 不是這三者之一,-validateResponse:data:error: 方法就會返回解析失敗的錯誤信息。

案例:在網上看到有開發者就曾經遇到過相關的問題
——服務器返回的數據是 JSON 數據,但是 Content-Type 卻不符合要求,結果導致解析失敗。

2. -responseObjectForResponse:data:error: 方法

AFJSONResponseSerializer 等子類中實現的 -responseObjectForResponse:data:error: 方法會先調用 -validateResponse:data:error: 方法驗證數據是否合法,拿到驗證結果后,接著這里有個補充判斷條件——如果是 content type 的錯誤就直接返回 nil,因為數據類型不符合要求,就沒必要再繼續解析數據了,如果是 status code 的錯誤就繼續解析,因為數據本身沒問題,而錯誤信息有可能就在返回的數據中,所以這種情況下會將 status code 產生的錯誤信息和解析后的數據一起“打包”返回。

AFJSONResponseSerializer 在解析數據后還提供了移除 NSNull 的功能,主要是為了防止服務端返回 null 時導致解析后的數據中有了脆弱的 NSNull,這樣很容易導致崩潰(但是之前一直沒發現這個功能[捂臉])。

- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
        
        // 如果是 content type 的錯誤就直接返回,因為數據類型不符合要求
        // 如果是 status code 的錯誤就繼續解析,因為錯誤信息有可能就在返回的數據中
        if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
            return nil;
        }
    }

    NSError *serializationError = nil;
    id responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];

    ...

    // 移除 NSNull(如果需要的話),默認是 NO
    if (self.removesKeysWithNullValues && responseObject) {
        responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
    }
    
    ...

    return responseObject;
}

六、AFSecurityPolicy

幾個關鍵字:HTTPS,TSL,SSL,SSL Pinning,非對稱加密算法

1. 預備知識點

1.1 為什么要使用 HTTPS

因為直接使用 HTTP 請求,就會有可能遇到以下幾個安全問題:

  • 傳輸數據被竊聽:HTTP 報文使用明文方式發送,而且 HTTP 本身不具備加密的功能,而互聯網是由聯通世界各個地方的網絡設施組成,所有發送和接收經過某些設備的數據都可能被截獲或窺視。
  • 認證問題:
    • 無法確認你發送到的服務器就是真正的目標服務器(可能服務器是偽裝的)
    • 無法確定返回的客戶端是否是按照真實意圖接收的客戶端(可能是偽裝的客戶端)
    • 無法確定正在通信的對方是否具備訪問權限,Web 服務器上某些重要的信息,只想發給特定用戶
  • 傳輸內容可能被篡改:請求或響應在傳輸途中,可能會被攻擊者攔截并篡改內容,也就是所謂的中間人攻擊(Man-in-the-Middle attack,MITM)。

1.2 HTTPS 的出現

HTTPS,也稱作 HTTP over TLS,HTTPS 就是基于 TLS 的 HTTP 請求。TLS 是一種基于 TCP 的加密協議,它主要做了兩件事:傳輸的兩端可以互相驗證對方的身份,以及驗證后加密所傳輸的數據。

HTTPS 通過驗證和加密兩種手段的結合解決了上面 HTTP 所面臨的 3 個安全問題。

1.3 SSL/TLS 協議

SSL(Secure Sockets Layer):SSL 協議是一種數據加密協議,為了保證網絡數據傳輸的安全性,網景公司設計了 SSL 協議用于對 HTTP 協議傳輸的數據進行加密,從而就誕生了 HTTPS。

TLS(Transport Layer Security):TLS 協議是 SSL 協議的升級版。1999年,互聯網標準化組織 ISOC 接替 NetScape 公司,發布了 SSL 的升級版 TLS 1.0版。

1.4 HTTPS 與 HTTP 的區別是什么?

HTTP HTTPS
URL http:// 開頭,并且默認使用端口 80 https:// 開頭,并且默認使用端口 443
數據隱私性 明文傳輸,不加密傳輸數據 基于 TLS 的加密傳輸
身份認證 不認證 正式傳輸數據前會進行證書認證,第三方無法偽造服務端(客戶端)身份
數據完整性 沒有完整性校驗過程 內容傳輸經過完整性校驗

HTTP協議和安全協議(SSL/TLS)同屬于應用層(OSI模型的最高層),具體來講,安全協議(SSL/TLS)工作在 HTTP 之下,傳輸層之上:安全協議向運行 HTTP 的進程提供一個類似于 TCP 的套接字,供進程向其中注入報文,安全協議將報文加密并注入傳輸層套接字;或是從運輸層獲取加密報文,解密后交給對應的進程。嚴格地講,HTTPS 并不是一個單獨的協議,而是對工作在一加密連接(TLS或SSL)上的常規 HTTP 協議的稱呼。

image

<div align="center">圖 6 協議層</div>

HTTPS 報文中的任何東西都被加密,包括所有報頭和荷載(payload)。除了可能的選擇密文攻擊之外,一個攻擊者所能知道的只有在兩者之間有一連接這件事。

1.5 HTTPS 連接的建立過程

HTTPS在傳輸數據之前需要客戶端與服務端之間進行一次握手,在握手過程中將確立雙方加密傳輸數據的密碼信息。(握手過程采用的非對稱加密,正式傳輸數據時采用的是對稱加密)

HTTPS 的認證有單向認證和雙向認證,這里簡單梳理一下客戶端單向認證時的握手流程:

(1)客戶端發起一個請求,服務端響應后返回一個證書,證書中包含一些基本信息和公鑰。
(2)客戶端里存有各個受信任的證書機構根證書,用這些根證書對服務端返回的證書進行驗證,如果不可信任,則請求終止。
(3)如果證書受信任,或者是用戶接受了不受信的證書,客戶端會生成一串隨機數的密碼 random key,并用證書中提供的公鑰加密,再返回給服務器。
(4)服務器拿到加密后的隨機數,利用私鑰解密,然后再用解密后的隨機數 random key,對需要返回的數據加密,加密完成后將數據返回給客戶端。
(5)最后用戶拿到被加密過的數據,用客戶端一開始生成的那個隨機數 random key,進行數據解密。整個 TLS/SSL 握手過程完成。

image

<div align="center">圖 7 TLS/SSL 握手過程(單向認證)</div>

完整的 HTTPS 連接的建立過程,包括下面三個步驟:

(1)TCP 協議的三次握手;
(2)TLS/SSL 協議的握手、密鑰協商;
(3)使用共同約定的密鑰開始通信。

image

<div align="center">圖 8 完整的 HTTPS 連接的建立過程</div>

1.6 HTTPS 傳輸時是如何驗證證書的?怎樣應對中間人偽造證書?

先來看看維基百科上對對稱加密和非對稱加密的解釋:

對稱密鑰加密(英語:Symmetric-key algorithm)又稱為對稱加密、私鑰加密、共享密鑰加密,是密碼學中的一類加密算法。<u>這類算法在加密和解密時使用相同的密鑰,或是使用兩個可以簡單地相互推算的密鑰</u>。實務上,這組密鑰成為在兩個或多個成員間的共同秘密,以便維持專屬的通訊聯系。與公開密鑰加密相比,要求雙方取得相同的密鑰是對稱密鑰加密的主要缺點之一。

公開密鑰加密(英語:public-key cryptography,又譯為公開密鑰加密),也稱為非對稱加密(asymmetric cryptography),一種密碼學算法類型,在這種密碼學方法中,需要一對密鑰(其實這里密鑰說法不好,就是“鑰”),一個是私人密鑰,另一個則是公開密鑰。<u>這兩個密鑰是數學相關,用某用戶密鑰加密后所得的信息,只能用該用戶的解密密鑰才能解密。如果知道了其中一個,并不能計算出另外一個。因此如果公開了一對密鑰中的一個,并不會危害到另外一個的秘密性質</u>。稱公開的密鑰為公鑰;不公開的密鑰為私鑰。

從上面可以看出非對稱加密的特點:非對稱加密有一對公鑰私鑰,用公鑰加密的數據只能通過對應的私鑰解密,用私鑰加密的數據只能通過對應的公鑰解密。這種加密是單向的。

(1)HTTPS 傳輸時是如何驗證證書的呢?

我們以最簡單的為例:一個證書頒發機構(CA),頒發了一個證書 Cer,服務器用這個證書建立 HTTPS 連接,同時客戶端在信任列表里有這個 CA 機構的根證書。

CA 機構頒發的證書 Cer 里包含有證書內容 Content,以及證書加密內容 Crypted Content(數字簽名),這個加密內容 Crypted Content 就是用這個證書機構的私鑰對內容 Content 加密的結果。

+-------------------+
|      Content      |
+-------------------+
|   Crypted Content |
+-------------------+
     證書 Cer

建立 HTTPS 連接時,服務端會把證書 Cer 返回給客戶端,客戶端系統里的 CA 機構根證書有這個 CA 機構的公鑰,用這個公鑰對證書 Cer 的加密內容 Crypted Content 解密得到 Content,跟證書 Cer 里的內容 Content 對比,若相等就通過驗證。大概的流程如下:

       +-----------------------------------------------------+
       |           crypt with private key                    |
       |  Content ------------------------> Crypted Content  |
Server |                                                     |
       |                     證書 Cer                        |
       +-----------------------------------------------------+

                          ||        
                          ||
                          \/
                        
       +-----------------------------------------------------+
       |                                                     |
       |               Content  &  Crypted Content           |
Client |                  |               |                  |
       |                  |  證書 Cer     |                 |
       +------------------|---------------|------------------+ 
                    |         | 
                    |         | derypt with public key   
                    |         | 
                    \/    相等?  \/
                 Content ------ Decrypted Content  
                 
                                   

(2)怎樣應對中間人偽造證書?

因為中間人不會有 CA 機構的私鑰,即便偽造了一張證書,但是私鑰不對,加密出來的內容也就不對,客戶端也就無法通過 CA 公鑰解密,所以偽造的證書肯定無法通過驗證。

1.7 Certificate Pinning 是什么?

如果一個客戶端通過 TLS 和服務器建立連接,操作系統會驗證服務器證書的有效性(一般是按照 X.509 標準)。當然,有很多手段可以繞開這個校驗,最直接的是在 iOS 設備上安裝證書并且將其設置為可信的。這種情況下,實施中間人攻擊也不是什么難事。不過通過 Certificate Pinning 可以解決這個問題。

A client that does key pinning adds an extra step beyond the normal X.509 certificate validation.

—— Wikipedia:Certificate Pinning

Certificate Pinning ,可以理解為證書綁定,有時候又叫 SSL Pinning,其實更準確的叫法應該是 Public Key Pinning(公鑰綁定)。證書綁定是一種檢測和防止“中間人攻擊”的方式,客戶端直接保存服務端的證書,當建立 TLS 連接后,應立即檢查服務器的證書,<u>不僅要驗證證書的有效性,還需要確定證書是不是跟客戶端本地的證書相匹配</u>。考慮到應用和服務器需要同時升級證書的要求,這種方式比較適合應用在訪問自家服務器的情況下。

為什么直接對比就能保證證書沒問題?

如果中間人從客戶端取出證書,再偽裝成服務端跟其他客戶端通信,它發送給客戶端的這個證書不就能通過驗證嗎?確實可以通過驗證,但后續的流程走不下去,因為下一步客戶端會用證書里的公鑰加密,中間人沒有這個證書的私鑰就解不出內容,也就截獲不到數據,這個證書的私鑰只有真正的服務端有,中間人偽造證書主要偽造的是公鑰。

什么情況下需要使用 Certificate Pinning?

  • 就像前面所說的,常規的驗證方式并不能避免遭遇中間人攻擊,因為如果所訪問網站的證書是自制的,而且在客戶端上通過手動安裝根證書信任了,此時就很容易被惡意攻擊了(還記得你訪問 12306 時收到的證書驗證提醒嗎)。
  • 如果服務端的證書是從受信任的的 CA 機構頒發的,驗證是沒問題的,但 CA 機構頒發證書比較昂貴,小企業或個人用戶可能會選擇自己頒發證書,這樣就無法通過系統受信任的 CA 機構列表驗證這個證書的真偽了。

2. AFSecurityPolicy 的實現

2.1 AFSecurityPolicy 的作用

NSURLConnection 和 NSURLSession 已經封裝了 HTTPS 連接的建立、數據的加密解密功能,我們直接使用 NSURLConnection 或者 NSURLSession 也是可以訪問 HTTPS 網站的,但 NSURLConnection 和 NSURLSession 并沒有驗證證書是否合法,無法避免中間人攻擊。要做到真正安全通訊,需要我們手動去驗證服務端返回的證書(系統提供了 SecTrustEvaluate函數供我們驗證證書使用)。

AFSecurityPolicy 幫我們封裝了證書驗證的邏輯,讓用戶可以輕易使用,除了在系統的信任機構列表里驗證,還支持 SSL Pinning 方式的驗證。

2.2 使用方法

如果是權威機構頒發的證書,不需要任何設置。

如果是自簽名證書,但是不做證書綁定,直接按照下面的代碼實現即可:

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
// 允許無效證書(包括自簽名證書),必須的
policy.allowInvalidCertificates = YES;
// 是否驗證域名的CN字段
// 不是必須的,但是如果寫YES,則必須導入證書。
policy.validatesDomainName = NO;

AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:<#MyAPIBaseURLString#>]];
manager.securityPolicy = securityPolicy;

如果是自簽名證書,而且還要做證書綁定,就需要把自簽的服務端證書,或者自簽的CA根證書導入到項目中(把 cer 格式的服務端證書放到 APP 項目資源里,AFSecurityPolicy 會自動尋找根目錄下所有 cer 文件,當然你也可以自己讀取),然后再選擇驗證證書或者公鑰。


AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
// 允許無效證書(包括自簽名證書),必須的
policy.allowInvalidCertificates = YES;
// 是否驗證域名的CN字段,不是必須的
policy.validatesDomainName = NO;

AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:<#MyAPIBaseURLString#>]];
manager.securityPolicy = securityPolicy;

2.3 AFSecurityPolicy 的實現

詳細說明見源碼注釋。

在 AFURLSessionManager 中實現的 -URLSession:didReceiveChallenge:completionHandler: 方法中,根據 NSURLAuthenticationChallenge 對象中的 authenticationMethod,來決定是否需要驗證服務器證書,如果需要驗證,則借助 AFSecurityPolicy 來驗證證書,驗證通過則創建 NSURLCredential,并回調 handler:

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    /*
     NSURLSessionAuthChallengeUseCredential:使用指定的證書
     NSURLSessionAuthChallengePerformDefaultHandling:默認方式處理
     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消整個請求
     NSURLSessionAuthChallengeRejectProtectionSpace:
     */
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        
        // 此處服務器要求客戶端的接收認證挑戰方法是 NSURLAuthenticationMethodServerTrust,也就是說服務器端需要客戶端驗證服務器返回的證書信息
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            
            // 客戶端根據安全策略驗證服務器返回的證書
            // AFSecurityPolicy 在這里的作用就是,使得在系統底層自己去驗證之前,AF可以先去驗證服務端的證書。如果通不過,則直接越過系統的驗證,取消https的網絡請求。否則,繼續去走系統根證書的驗證(??)。
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                // 信任的話,就創建驗證憑證去做系統根證書驗證
                
                // 創建 NSURLCredential 前需要調用 SecTrustEvaluate 方法來驗證證書,這件事情其實 AFSecurityPolicy 已經幫我們做了
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                // 不信任的話,就直接取消整個請求
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }

    if (completionHandler) {
        // 疑問:這個 completionHandler 是用來干什么的呢?credential 又是用來干什么的呢?
        completionHandler(disposition, credential);
    }
}

而 AFSecurityPolicy 的核心就在于 -evaluateServerTrust:forDomain: 方法,該方法中主要做了四件事:

  • 設置驗證標準(SecTrustSetPolicies),為認證做準備
  • 處理 SSLPinningMode 為 AFSSLPinningModeNone 的情況——如果允許無效的證書(包括自簽名證書)就直接返回 YES,不允許的話就在系統的信任機構列表里驗證服務端證書。
  • 處理 SSLPinningMode 為 AFSSLPinningModeCertificate 的情況,認證證書——設置證書錨點->驗證服務端證書->匹配服務端證書鏈
  • 處理 SSLPinningMode 為 AFSSLPinningModePublicKey 的情況,認證公鑰——匹配服務端證書公鑰
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     AFSecurityPolicy 的四個主要屬性:
     SSLPinningMode - 證書認證模式
     pinnedCertificates - 用來匹配服務端證書信息的證書,這些證書保存在客戶端
     allowInvalidCertificates - 是否支持無效的證書(包括自簽名證書)
     validatesDomainName - 是否去驗證證書域名是否匹配
     
     
     SSLPinningMode 提供的三種證書認證模式:
     AFSSLPinningModeNone - 沒有 SSL pinning
     AFSSLPinningModePublicKey - 用證書綁定方式驗證,客戶端要有服務端的證書拷貝,只是驗證時只驗證證書里的公鑰,不驗證證書的有效期等信息
     AFSSLPinningModeCertificate - 用證書綁定方式驗證證書,需要客戶端保存有服務端的證書拷貝,這里驗證分兩步,第一步驗證證書的域名/有效期等信息,第二步是對比服務端返回的證書跟客戶端返回的是否一致。
     
     */
    
    
    // 判斷互相矛盾的情況:
    // 如果有域名,而且還要允許自簽證書,同時還要驗證域名的話,就一定要驗證服務器返回的證書是否匹配客戶端本地的證書了
    // 所以必須滿足兩個條件:A驗證模式不能為 FSSLPinningModeNone;添加到項目里的證書至少 1 個。
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        
        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
        return NO;
    }

    // 為 serverTrust 設置 policy,也就是告訴客戶端如何驗證 serverTrust
    // 如果要驗證域名的話,就以域名為參數創建一個 SecPolicyRef,否則會創建一個符合 X509 標準的默認 SecPolicyRef 對象
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    // 驗證證書是否有效
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        // 如果不做證書綁定,就會跟瀏覽器一樣在系統的信任機構列表里驗證服務端返回的證書(如果是自己買的證書,就不需要綁定證書了,可以直接在系統的信任機構列表里驗證就行了)
        // 如果允許無效的證書(包括自簽名證書)就會直接返回 YES,不允許的話就會對服務端證書在系統的信任機構列表里驗證。如果服務器證書無效,并且不允許無效證書,就會返回 NO
        
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
        
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        // 如果不是 AFSSLPinningModeNone,而且證書在系統的信任機構列表里驗證失敗,同時不允許無效的證書(包括自簽名證書)時,直接返回評估失敗
        // (如果是自簽名的證書,驗證時就需要做證書綁定,或者直接在系統的信任機構列表里中添加根證書)
        
        return NO;
    }

    // 根據 SSLPinningMode 對服務端返回的證書進行 SSL Pinning 驗證,也就是說拿本地的證書和服務端證書進行匹配
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            
            // 把證書 data 轉成 SecCertificateRef 類型的數據,保證返回的證書都是 DER 編碼的 X.509 證書
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            
            // 1.
            // 將 pinnedCertificates 設置成需要參與驗證的 Anchor Certificate(錨點證書,嵌入到操作系統中的根證書,也就是權威證書頒發機構頒發的自簽名證書),通過 SecTrustSetAnchorCertificates 設置了參與校驗錨點證書之后,假如驗證的數字證書是這個錨點證書的子節點,即驗證的數字證書是由錨點證書對應CA或子CA簽發的,或是該證書本身,則信任該證書,具體就是調用 SecTrustEvaluate 來驗證。
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            // 自簽證書在之前是驗證通過不了的,在這一步,把我們自己設置的證書加進去之后,就能驗證成功了。
            // 再去調用之前的 serverTrust 去驗證該證書是否有效,有可能經過這個方法過濾后,serverTrust 里面的 pinnedCertificates 被篩選到只有信任的那一個證書
            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // 注意,這個方法和我們之前的錨點證書沒關系了,是去從我們需要被驗證的服務端證書,去拿證書鏈。
            // 服務器端的證書鏈,注意此處返回的證書鏈順序是從葉節點到根節點
            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                // 如果我們的證書中,有一個和它證書鏈中的證書匹配的,就返回 YES
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            
            // 獲取服務器證書公鑰
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            // 判斷自己本地的證書的公鑰是否存在與服務器證書公鑰一樣的情況,只要有一組符合就為真
            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

2.4 技術點

(1) __Require_Quiet 宏中 do-while 的特殊使用

#ifndef __Require_Quiet
    #define __Require_Quiet(assertion, exceptionLabel)                            \
      do                                                                          \
      {                                                                           \
          if ( __builtin_expect(!(assertion), 0) )                                \
          {                                                                       \
              goto exceptionLabel;                                                \
          }                                                                       \
      } while ( 0 )
#endif

__Require_Quiet 宏中使用了一個 do...while(0) 的循環語句,從邏輯上看這個 do-while 語句完全可以不需要,但是實際上是不能去掉的,原因是為了防止在某種情況下使用該宏時出現語法錯誤。比如,在下面這種情況下,如果沒有 do...while(0) 就在編譯時報錯:

if (xxx) 
    __Require_Quiet();
else 
    NSLog("This is else");

參考:宏定義的黑魔法 - 宏菜鳥起飛手冊

(2) Core Foundation 和 Security 框架的 API 的使用

延伸閱讀:

七、AFNetworkReachabilityManager

暫時還沒看

八、UIKit 擴展

暫時還沒看

九、AFNetworking 2.x

十、AFNetworking 的價值

1. 請求調度:NSURLConnection + NSOperation

在 NSURLConnection 時代,AFNetworking 1.x 的最核心的作用在于多線程下的請求調度——將 NSURLConnection 和 NSOperation 結合,AFURLConnectionOperation 作為 NSOperation 的子類,遵循 NSURLConnectionDelegate 的方法,可以從頭到尾監聽請求的狀態,并儲存請求、響應、響應數據等中間狀態。

2. 更高層次的抽象

顯然,在 NSURLSession 出現之后,AFNetworking 的意義似乎不如以前那么重要了。實際上,雖然它們有一些重疊,AFNetworking 還是可以提供更高層次的抽象。

AFNetworking 幫我們完成了很多繁瑣的工作,這使得我們在業務層的網絡請求變得非常輕松:

  • 請求參數和返回數據的序列化,支持多種不同格式的數據解析
  • multipart 請求拼接數據
  • 驗證 HTTPS 請求的證書
  • 請求成功和失敗的回調處理,下載、上傳進度的回調處理

3. block

AFNetworking 將 NSURLSession 散亂的代理回調方法都轉成了 block 形式的 API,除此之外,還提供了一些用于自定義配置的 block,比如發起 multipart 請求時,提供 constructingBody 的 block 接口來拼接數據。

4. 模塊化

AFNetworking 在架構上采用了模塊化的設計,各模塊的職責是明確的、功能是獨立的,我們可以根據自己的需要,選擇合適的模塊組合使用:

  • 創建請求
  • 序列化 query string 參數
  • 確定響應解析行為
  • 管理 Session
  • HTTPS 認證
  • 監視網絡狀態
  • UIKit 擴展

十一、問題:

1.AFNetworking 的作用是什么?不用 AFNetworking 直接用系統的 NSURLSession 不可以嗎?AFNetworking 為什么要對 NSURLConnection/NSURLSession 進行封裝?它是如何封裝的?

2.AFNetworking 框架的設計思路和原理是什么?

3.AFNetworking 和 MKNetworkKit 以及 ASIHttpRequest 有什么不同?

4.AFNetworking 2.x 和 AFNetworking 3.x 的區別是什么?

十二、收獲

  • 開源項目、專業素養、規范
  • 完善的注釋、文檔
  • 忽略一些特定的clang的編譯警告
  • nullable
  • 規范,通過斷言檢測參數的合法性
  • 邏輯嚴謹、完善,擴展性好,比如針對用戶可能需要的各種自定義處理提供了 block 回調,基于協議的 serialization 設計
  • 萬物皆對象,比如請求 url 參數的解析時,使用了 AFQueryStringPair 對象來表征一個 Query 參數;還有 NSProgress 的使用
  • 面向協議編程,提高程序的可擴展性
  • 多線程編程時,腦海中要有清晰的線程調度圖
  • Unit Test,看到 GitHub 上有個 pr 的討論中多次提到了 Unit Test,原來 Unit Test 對于保證修改后的代碼功能有很大用處,另外就是,有些使用的示例也可以從 test case 中找到

延伸閱讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容