【原】AFNetworking源碼閱讀(二)

【原】AFNetworking源碼閱讀(二)

本文轉載請註明出處 —— polobymulberry-博客園html

1. 前言


上一篇中咱們在iOS Example代碼中提到了AFHTTPSessionManager中的一個函數:ios

- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
                            parameters:(nullable id)parameters
                              progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
                               success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                               failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

這個函數做用其實看函數名就明白了- 使用GET類型的Request建立並運行一個NSURLSessionDataTaskgit

2. dataTaskWithHTTPMethod

具體咱們看函數實現:github

NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
                                                    URLString:URLString
                                                   parameters:parameters
                                               uploadProgress:nil
                                             downloadProgress:downloadProgress
                                                      success:success
                                                      failure:failure];

[dataTask resume];

return dataTask;
  • 使用dataTaskWithHTTPMethod方法建立了一個NSURLSessionDataTask
  • 調用NSURLSessionDataTask的resume來開啓這個session task

    • 知識點:session task的幾種狀態的操做函數
      • suspend -- 可讓當前的任務暫停
      • resume ---- 方法不只能夠啓動任務,還能夠喚醒suspend狀態的任務
      • cancel ----- 方法能夠取消當前的任務,你也能夠向處於suspend狀態的任務發送cancel消息,任務若是被取消便不能再恢復到以前的狀態.

  • 最後返回這個task

咱們很天然想到了,全部的關鍵都在dataTaskWithHTTPMethod這個函數。咱們先不慌看這個函數的具體實現,先窮盡到這個函數的全部調用。咱們已經知道這個函數是建立一個NSURLSessionDataTask,而系統提供給咱們建立NSURLSessionDataTask的方法,有兩個:編程

  1. 1.–dataTaskWithRequest:
  2. 2.–dataTaskWithRequest:completionHandler:

好,那咱們就沿着這個線索一直找下去,一直找到有這兩個函數使用的地方。追蹤溯源,還真找到了這樣一條函數調用棧。api

QQ20160116-0

上圖能夠看出GET、HEAD、POST、PUT、PATCH、DELETE這些方法實現的不一樣之處只在於調用dataTaskWithHTTPMethod:傳遞的method名稱不一樣。另外在調用dataTaskWithRequest:時候,其實已經在上一級函數dataTaskWithHTTPMethod:中構建好了一個NSMutableURLRequest類型的request。因此咱們主要研究dataTaskWithHTTPMethod:函數實現。緩存

dataTaskWithHTTPMethod函數的實現主要分兩部分,一部分是構建NSMutableURLRequest,另外一部分是根據已構建好的Request來構建NSURLSessionDataTasksass

2.1 構建NSMutableURLRequest

此處構建request分爲兩個部分:cookie

  1. 1.先調用AFHTTPRequestSerializer的requestWithMethod函數構建request
  2. 2.處理request構建產生的錯誤 – serializationError

2.1.1 requestWithMethod構建request

先直接暴力列出requestWithMethod的函數聲明(注:requestWithMethod是AFHTTPRequestSerializer的一個成員函數,而且AFHTTPRequestSerializer遵循AFURLRequestSerialization協議)網絡

/**
 使用指定的HTTP method和URLString來構建一個NSMutableURLRequest對象實例

 若是method是GET、HEAD、DELETE,那parameter將會被用來構建一個基於url編碼的查詢字符串(query url)
 ,而且這個字符串會直接加到request的url後面。對於其餘的Method,好比POST/PUT,它們會根
 據parameterEncoding屬性進行編碼,然後加到request的http body上。
 @param method request的HTTP methodt,好比 `GET`, `POST`, `PUT`, or `DELETE`. 該參數不能爲空
 @param URLString 用來建立request的URL
 @param parameters 既能夠對method爲GET的request設置一個查詢字符串(query string),也能夠設置到request的HTTP body上
 @param error 構建request時發生的錯誤

 @return  一個NSMutableURLRequest的對象
 */
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(nullable id)parameters
                                     error:(NSError * _Nullable __autoreleasing *)error;

接着咱們來看requestWithMethod的具體實現:

  • 第一步:進行url轉化和參數化斷言
NSParameterAssert(method);
NSParameterAssert(URLString);

NSURL *url = [NSURL URLWithString:URLString];

NSParameterAssert(url);

其中NSParameterAssert(method) <=> NSParameterAssert(method != nil),同理NSParameterAssert(URLString)和NSParameterAssert(url)也同樣。這裏NShipster給出了一個金科玉律:

方法或函數應當在代碼最開始處使用 NSParameterAssert / NSCParameterAssert 來強制輸入的值知足先驗條件,這是一條金科玉律;其餘狀況下使用 NSAssert / NSCAssert

  • 第二步:使用url構建並初始化NSMutableURLRequest,而後設置HTTPMethod
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
  • 第三步:給NSMutableURLRequest自帶的屬性賦值

NSURLRequest/NSMutableURLRequest須要賦值的屬性能夠在AFHTTPRequestSerializerObservedKeyPaths()中找到,咱們能夠進去看一下:

// 定義了一個static的方法,表示該方法只能在本文件中使用
// 函數總體上使用了單例模式
static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
    static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
    static dispatch_once_t onceToken;
    // 此處須要observer的keypath爲allowsCellularAccesscachePolicyHTTPShouldHandleCookies
    // HTTPShouldUsePipeliningnetworkServiceTypetimeoutInterval
    dispatch_once(&onceToken, ^{
        _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
    });

    return _AFHTTPRequestSerializerObservedKeyPaths;
}

簡單介紹下上面添加的keypath:

/**
 是否容許使用設備的蜂窩移動網絡來建立request,默認爲容許:
 */
@property (nonatomic, assign) BOOL allowsCellularAccess;

/**
 建立的request所使用的緩存策略,默認使用`NSURLRequestUseProtocolCachePolicy`,該策略表示 
 若是緩存不存在,直接從服務端獲取。若是緩存存在,會根據response中的Cache-Control字段判斷
 下一步操做,如: Cache-Control字段爲must-revalidata, 則 詢問服務端該數據是否有更新,無更新話
 直接返回給用戶緩存數據,若已更新,則請求服務端.
 */
@property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy;

/**
 若是設置HTTPShouldHandleCookies爲YES,就處理存儲在NSHTTPCookieStore中的cookies
 HTTPShouldHandleCookies表示是否應該給request設置cookie並隨request一塊兒發送出去
 */
@property (nonatomic, assign) BOOL HTTPShouldHandleCookies;

/**
 HTTPShouldUsePipelining表示receiver(理解爲iOS客戶端)的下一個信息是否必須等到上一個請求回覆才能發送。
 若是爲YES表示能夠,NO表示必須等receiver收到先前的回覆才能發送下個信息。
 */
@property (nonatomic, assign) BOOL HTTPShouldUsePipelining;

/**
 設定request的network service類型. 默認是`NSURLNetworkServiceTypeDefault`.
 這個network service是爲了告訴系統網絡層這個request使用的目的
 好比NSURLNetworkServiceTypeVoIP表示的就這個request是用來請求網際協議通話技術(Voice over IP)。
 系統能根據提供的信息來優化網絡處理,從而優化電池壽命,網絡性能等等
 */
@property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType;

/**
 超時機制,默認60秒
 */
@property (nonatomic, assign) NSTimeInterval timeoutInterval;

而後經過判斷mutableObservedChangedKeyPaths(NSMutableSet)中是否有這個keyPath,來設定mutableRequest對應的keyPath值。

for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
    if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
        [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
    }
}

至於mutableObservedChangedKeyPaths是什麼,咱們能夠在AFURLRequestSerialization文件中的observeValueForKeyPath函數中獲得答案。整個過程是這樣的

image

關鍵就是在哪裏會產生keypath的值變化了的消息

image

也就是說你只要使用了keyPath對應的的setter方法,就會響應observerValueForKeyPath這個方法,從而將對應的keyPath添加到了mutableObservedChangedKeyPaths。至於添加keyPath到observer中,那是在AFHTTPRequestSerializer的init中乾的活:

image

  1. 第四步:將傳入的parameters進行編碼,並添加到request中

此過程主要集中在requestBySerializingRequest這個函數中。在介紹requestBySerializingRequest以前,先簡單介紹下,爲何會有這個函數的存在?


引用自AFNetworking2.0源碼解析<二>

通常咱們請求都會按key=value的方式帶上各類參數,GET方法參數直接加在URL上,POST方法放在body上,NSURLRequest沒有封裝好這個參數的解析,只能咱們本身拼好字符串。AFNetworking提供了接口,讓參數能夠是NSDictionary, NSArray, NSSet這些類型,再由內部解析成字符串後賦給NSURLRequest。

轉化過程大體是這樣的:

@{ 
     @"name" : @"bang", 
     @"phone": @{@"mobile": @"xx", @"home": @"xx"}, 
     @"families": @[@"father", @"mother"], 
     @"nums": [NSSet setWithObjects:@"1", @"2", nil] 
} 
-> 
@[ 
     field: @"name", value: @"bang", 
     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=bang&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&num=2

或者看下面這段解釋:

好比說我定義了下面這個parameter:

NSString *URLString = @"http://example.com";
NSDictionary *parameters = @{@"foo": @"bar", @"baz": @[@1, @2, @3]};

使用GET方式,最後獲得的request是這樣的:

[[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:URLString parameters:parameters error:nil];

GET http://example.com?foo=bar&baz[]=1&baz[]=2&baz[]=3

或者使用POST方式,最後獲得的request是這樣的:

[[AFHTTPRequestSerializer serializer] requestWithMethod:@"POST" URLString:URLString parameters:parameters];

POST http://example.com/ Content-Type: application/x-www-form-urlencoded foo=bar&baz[]=1&baz[]=2&baz[]=3

requestBySerializingRequest也分爲三個部分:

  • requestBySerializingRequest函數 - 第一部分

設置request的http header field:

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

這裏關於http header field的值都存放在了HTTPRequestHeaders中了。至於HTTPRequestHeaders的設置,是在多個函數中都有設置的。此處就不一一贅述,後面遇到會詳解。

  • requestBySerializingRequest函數 - 第二部分

根據parameter來構建查詢字符串,這裏一開始parameter以下:

Printing description of parameters:
{
    baz =     (
        1,
        2,
        3
    );
    foo = bar;
}

通過構建後,獲得query爲(這個例子中的構建方式使用的是AFQueryStringFromParameters()函數):

Printing description of query:
baz[]=1&baz[]=2&baz[]=3&foo=bar

事實上代碼中有兩種構建query的方式:

其中一種就是,若是自定義了queryStringSerialization(AFQueryStringSerializationBlock的block變量)。那麼就使用自定義的queryStringSerialization構建方式(此方法在AFNetworking的test中用的比較多)

還有一種就是上面的那個AFQueryStringFromParameters()函數,咱們能夠看到AFQueryStringFromParameters的調用結構是下圖這樣的:

image

講解的話,我以爲根據上圖從後往前講比較好:

首先看NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value)這個函數

該函數首先定義了一個NSSortDescriptor *sortDescriptor:

// 根據須要排列的對象的description來進行升序排列,而且selector使用的是compare:
// 由於對象的description返回的是NSString,因此此處compare:使用的是NSString的compare函數
// 即@[@"foo", @"bar", @"bae"] ----> @[@"bae", @"bar",@"foo"]
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

接着會對value的類型進行判斷,有NSDictionary、NSArray、NSSet類型。不過有人就會問了,在AFQueryStringPairsFromDictionary中給AFQueryStringPairsFromKeyAndValue函數傳入的value不是NSDictionary嘛?還要判斷那麼多類型幹啥?對,問得很好,這就是AFQueryStringPairsFromKeyAndValue的核心----遞歸調用並解析,你不能保證NSDictionary的value中存放的是一個NSArray、NSSet。

既然是遞歸,那麼就要有結束遞歸的狀況,好比解析到最後,對應value是一個NSString,那麼就得調用函數中最後的else語句:

else {
    [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
}

注意此處定義了一個AFQueryStringPair:

@interface AFQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;
// ...
@end

而initWithField作的就是將key賦給field,value賦值給value。你們能夠回頭看一下最開始舉的那個例子,就產生了對應的field-value。

接着回到AFQueryStringPairsFromDictionary函數,好像沒啥好說的。再回到AFQueryStringFromParameters函數,這個函數就是把這些構建好的AFQueryStringPair一個個用&鏈接好。這裏注意一點就是,此處會對AFQueryStringPair使用其URLEncodedStringValue函數作必定的處理,其實就是Percent-encoding(百分號編碼)。


知識點:百分號編碼

根據RFC 3986,如下字符爲保留字:

image

另外,在RFC 3989 – Section 3.4部分,「?」和「/」看成爲URL中的query string的時候,再也不當作保留字。

此處主要是經過stringByAddingPercentEncodingWithAllowedCharacters函數來給咱們的string進行百分號編碼的。其中stringByAddingPercentEncodingWithAllowedCharacters函數須要傳入不須要百分號編碼的字符集(也就是不包括上面說的保留字,即函數中構建的allowedCharacterSet)。另外,爲了防止image字符形成的問題,此處還須要使用rangeOfComposedCharacterSequencesForRange函數來處理字符長度。

舉個例子,若是我傳入的字符串爲image,那麼最終獲得的百分號編碼的字符串爲

poloby%3A%23mulberry%5B%5D%40%F0%9F%91%B4%F0%9F%8F%BB%F0%9F%91%AE%F0%9F%8F%BD

下圖是保留字的百分號編碼:

image


因此事實上,上面最終生成的query url中,[]都會被%5B%5D所代替。

  • requestBySerializingRequest函數 - 第三部分

最後判斷該request中是否包含了GET、HEAD、DELETE(都包含在HTTPMethodsEncodingParametersInURI)。由於這幾個method的quey是拼接到url後面的。而POST、PUT是把query拼接到http body中的。

若是method是GET、HEAD、DELETE等。最後將query合併到mutbleRequest的query url上。不過這裏仍是要分狀況討論,若是request的query url不爲空,就在生成的query前拼接&字符,再拼接到原先的query url上,若是request的query url爲空,就將生成的的query前拼接?字符,再拼接到request的url上

Printing description of mutableRequest:
<NSMutableURLRequest: 0x7f9a63f237c0> { URL: https://api.app.net/stream/0/posts/stream/global?baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3&foo=bar }

若是method是POST、PUT等。最後將query設置到http body上。另外,在此以前,函數會判斷request的Content-Type是否設置了,若是沒有,就默認設置爲application/x-www-form-urlencoded。

2.1.2 處理serializationError

生成request出錯了怎麼辦?這裏冒出了一個completionQueue,暫時無論它,由於我並不知道這個東西是怎麼用的。通常的話,咱們都是在main queue來執行自定義的failure函數處理error。

2.2 構建NSURLSessionDataTask

有了request後,就能夠調用-[AFURLSessionManager dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:]來構建session data task。

一樣地,dataTaskWithRequest函數也分爲兩個部分。第一部分是建立一個dataTask,第二個部分是調用addDelegateForDataTask這個函數,具體這個函數是作什麼的,目前我也不是很清楚。

2.2.1 建立dataTask

使用了url_session_manager_create_task_safely(dispatch_block_t block)這個函數。這個函數主要的目的是爲了解決iOS8以前的一個bug,詳見https://github.com/AFNetworking/AFNetworking/issues/2093。在這個issue中,提問者建議版本小於iOS8的使用QUEUE_SERIAL的dispatch。因此纔有了url_session_manager_create_task_safely這個函數,注意函數名中的create task和safely。因爲在iOS8以後,這個bug被修復了,因此直接調用block()便可。

static void url_session_manager_create_task_safely(dispatch_block_t block) {
    if (NSFoundationVersionNumber < NSFoundationVersionNumber_With_Fixed_5871104061079552_bug) {
        // Fix of bug
        // Open Radar:http://openradar.appspot.com/radar?id=5871104061079552 (status: Fixed in iOS8)
        // Issue about:https://github.com/AFNetworking/AFNetworking/issues/2093
        dispatch_sync(url_session_manager_creation_queue(), block);
    } else {
        block();
    }
}

2.2.2 addDelegateForDataTask

字面上理解的話,就是給data task添加了一個delegate,而這個delegate的類型爲AFURLSessionManagerTaskDelegate。爲何要給task加一個delegate?

咱們看看AFURLSessionManagerTaskDelegate的定義:

@interface AFURLSessionManagerTaskDelegate : NSObject <NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>

這裏我比較疑惑,NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate這三個delegate應該NSURLSession的delegate,你這邊出現了一個AFURLSessionManagerTaskDelegate也來實現這三個delegate是幾個意思?我猜想這裏是否是一種分離的代碼的方式,就是說把NSURLSession的delegate的實現分離出來給AFURLSessionManagerTaskDelegate實現。可是搜索了一下AFURLSessionManager中的session屬性的構建:

self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

這裏的delegate並非使用了AFURLSessionManagerTaskDelegate的那個delegate,因此上述猜想錯誤。不過我仍是找到了點蛛絲馬跡:

AFURLSessionManager中session(NSURLSession)的delegate設置爲了AFURLSessionManager的self,而且AFURLSessionManager確實也遵循了NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate這三個協議,也實現了其中的方法。關鍵是實現這些方法時用到了AFURLSessionManagerTaskDelegate的delegate中實現的方法。至於爲何要這麼作,話說我也是剛看,因此還須要消化一下。

這一篇就到此爲止,下面一篇會詳細介紹實現的NSURLSession的delegate方法了。

參考文章


相關文章
相關標籤/搜索