【原】AFNetworking源碼閱讀(二)
本文轉載請注明出處 —— polobymulberry-博客園
1. 前言
上一篇中我們在iOS Example代碼中提到了AFHTTPSessionManager中的一個函數:
- (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來創建並運行一個NSURLSessionDataTask。
2. dataTaskWithHTTPMethod
具體我們看函數實現:
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.–dataTaskWithRequest:
- 2.–dataTaskWithRequest:completionHandler:
好,那我們就沿着這個線索一直找下去,一直找到有這兩個函數使用的地方。追蹤溯源,還真找到了這樣一條函數調用棧。
上圖可以看出GET、HEAD、POST、PUT、PATCH、DELETE這些方法實現的不同之處只在於調用dataTaskWithHTTPMethod:傳遞的method名稱不同。另外在調用dataTaskWithRequest:時候,其實已經在上一級函數dataTaskWithHTTPMethod:中構建好了一個NSMutableURLRequest類型的request。所以我們主要研究dataTaskWithHTTPMethod:函數實現。
dataTaskWithHTTPMethod函數的實現主要分兩部分,一部分是構建NSMutableURLRequest,另一部分是根據已構建好的Request來構建NSURLSessionDataTask。
2.1 構建NSMutableURLRequest
此處構建request分為兩個部分:
- 1.先調用AFHTTPRequestSerializer的requestWithMethod函數構建request
- 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為allowsCellularAccess、cachePolicy、HTTPShouldHandleCookies // HTTPShouldUsePipelining、networkServiceType、timeoutInterval 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函數中得到答案。整個過程是這樣的:
關鍵就是在哪里會產生keypath的值變化了的消息?
也就是說你只要使用了keyPath對應的的setter方法,就會響應observerValueForKeyPath這個方法,從而將對應的keyPath添加到了mutableObservedChangedKeyPaths。至於添加keyPath到observer中,那是在AFHTTPRequestSerializer的init中干的活:
- 第四步:將傳入的parameters進行編碼,並添加到request中
此過程主要集中在requestBySerializingRequest這個函數中。在介紹requestBySerializingRequest之前,先簡單介紹下,為什么會有這個函數的存在?
一般我們請求都會按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的調用結構是下圖這樣的:
講解的話,我覺得根據上圖從后往前講比較好:
首先看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,以下字符為保留字:
另外,在RFC 3989 – Section 3.4部分,“?”和“/”當作為URL中的query string的時候,不再當做保留字。
此處主要是通過stringByAddingPercentEncodingWithAllowedCharacters函數來給我們的string進行百分號編碼的。其中stringByAddingPercentEncodingWithAllowedCharacters函數需要傳入不需要百分號編碼的字符集(也就是不包括上面說的保留字,即函數中構建的allowedCharacterSet)。另外,為了防止
字符造成的問題,此處還需要使用rangeOfComposedCharacterSequencesForRange函數來處理字符長度。
舉個例子,如果我傳入的字符串為
,那么最終得到的百分號編碼的字符串為
poloby%3A%23mulberry%5B%5D%40%F0%9F%91%B4%F0%9F%8F%BB%F0%9F%91%AE%F0%9F%8F%BD
下圖是保留字的百分號編碼:
所以事實上,上面最終生成的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方法了。





