寫在開頭:
- 作爲一個iOS開發,也許你不知道NSUrlRequest、不知道NSUrlConnection、也不知道NSURLSession…(說不下去了…怎麼會什麼都不知道…)但是你一定知道AFNetworking。
- 大多數人習慣了只要是請求網絡都用AF,但是你真的知道AF做了什麼嗎?爲什麼我們不用原生的NSURLSession而選擇AFNetworking?
- 本文將從源碼的角度去分析AF的實際作用。
或許看完這篇文章,你心裏會有一個答案。
先從最新的AF3.x講起吧:
- 首先,我們就一起分析一下該框架的組成。
將AF下載導入工程後,下面是其包結構,相對於2.x變得非常簡單了:AF代碼結構圖.png
除去Support Files,可以看到AF分爲如下5個功能模塊:
- 網絡通信模塊(AFURLSessionManager、AFHTTPSessionManger)
- 網絡狀態監聽模塊(Reachability)
- 網絡通信安全策略模塊(Security)
- 網絡通信信息序列化/反序列化模塊(Serialization)
- 對於iOS UIKit庫的擴展(UIKit)
其核心當然是網絡通信模塊AFURLSessionManager。大家都知道,AF3.x是基於NSURLSession來封裝的。所以這個類圍繞着NSURLSession做了一系列的封裝。而其餘的四個模塊,均是爲了配合網絡通信或對已有UIKit的一個擴展工具包。
這五個模塊所對應的類的結構關係圖如下所示:
其中AFHTTPSessionManager是繼承於AFURLSessionManager的,我們一般做網絡請求都是用這個類,但是它本身是沒有做實事的,只是做了一些簡單的封裝,把請求邏輯分發給父類AFURLSessionManager或者其它類去做。
首先我們簡單的寫個get請求:
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]init];
[manager GET:@"http://localhost" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
首先我們我們調用了初始化方法生成了一個manager,我們點進去看看初始化做了什麼:
- (instancetype)init {
return [self initWithBaseURL:nil];
}
- (instancetype)initWithBaseURL:(NSURL *)url {
return [self initWithBaseURL:url sessionConfiguration:nil];
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
return [self initWithBaseURL:nil sessionConfiguration:configuration];
}
- (instancetype)initWithBaseURL:(NSURL *)url
sessionConfiguration:(NSURLSessionConfiguration *)configuration
{
self = [super initWithSessionConfiguration:configuration];
if (!self) {
return nil;
}
//對傳過來的BaseUrl進行處理,如果有值且最後不包含/,url加上"/"
//--經一位熱心讀者更正...以後註釋也一定要走心啊...不能誤導大家...
if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}
self.baseURL = url;
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];
return self;
}
- 初始化都調用到
- (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration
方法中來了。 - 其實初始化方法都調用父類的初始化方法。父類也就是AF3.x最最核心的類AFURLSessionManager。幾乎所有的類都是圍繞着這個類在處理業務邏輯。
- 除此之外,方法中把baseURL存了起來,還生成了一個請求序列對象和一個響應序列對象。後面再細說這兩個類是幹什麼用的。
直接來到父類AFURLSessionManager的初始化方法:
- (instancetype)init {
return [self initWithSessionConfiguration:nil];
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
self = [super init];
if (!self) {
return nil;
}
if (!configuration) {
configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
self.sessionConfiguration = configuration;
self.operationQueue = [[NSOperationQueue alloc] init];
//queue併發線程數設置爲1
self.operationQueue.maxConcurrentOperationCount = 1;
//注意代理,代理的繼承,實際上NSURLSession去判斷了,你實現了哪個方法會去調用,包括子代理的方法!
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
//各種響應轉碼
self.responseSerializer = [AFJSONResponseSerializer serializer];
//設置默認安全策略
self.securityPolicy = [AFSecurityPolicy defaultPolicy];
#if !TARGET_OS_WATCH
self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
#endif
// 設置存儲NSURL task與AFURLSessionManagerTaskDelegate的詞典(重點,在AFNet中,每一個task都會被匹配一個AFURLSessionManagerTaskDelegate 來做task的delegate事件處理) ===============
self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];
// 設置AFURLSessionManagerTaskDelegate 詞典的鎖,確保詞典在多線程訪問時的線程安全
self.lock = [[NSLock alloc] init];
self.lock.name = AFURLSessionManagerLockName;
// 置空task關聯的代理
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionDataTask *task in dataTasks) {
[self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
}
for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
[self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
}
for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
[self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
}
}];
return self;
}
- 這個就是最終的初始化方法了,註釋應該寫的很清楚,唯一需要說的就是三點:
-
self.operationQueue.maxConcurrentOperationCount = 1;
這個operationQueue就是我們代理回調的queue。這裏把代理回調的線程併發數設置爲1了。至於這裏爲什麼要這麼做,我們先留一個坑,等我們講完AF2.x之後再來分析這一塊。 - 第二就是我們初始化了一些屬性,其中包括
self.mutableTaskDelegatesKeyedByTaskIdentifier
,這個是用來讓每一個請求task和我們自定義的AF代理來建立映射用的,其實AF對task的代理進行了一個封裝,並且轉發代理到AF自定義的代理,這是AF比較重要的一部分,接下來我們會具體講這一塊。 - 第三就是下面這個方法:
首先說說這個方法是幹什麼用的:這個方法用來異步的獲取當前session的所有未完成的task。其實講道理來說在初始化中調用這個方法應該裏面一個task都不會有。我們打斷點去看,也確實如此,裏面的數組都是空的。[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { }];
但是想想也知道,AF大神不會把一段沒用的代碼放在這吧。輾轉多處,終於從AF的issue中找到了結論:github 。- 原來這是爲了從後臺回來,重新初始化session,防止一些之前的後臺請求任務,導致程序的crash。
-
初始化方法到這就全部完成了。
接着我們來看看網絡請求:
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(id)parameters
progress:(void (^)(NSProgress * _Nonnull))downloadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
//生成一個task
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
URLString:URLString
parameters:parameters
uploadProgress:nil
downloadProgress:downloadProgress
success:success
failure:failure];
//開始網絡請求
[dataTask resume];
return dataTask;
}
方法走到類AFHTTPSessionManager中來,調用父類,也就是我們整個AF3.x的核心類AFURLSessionManager的方法,生成了一個系統的NSURLSessionDataTask實例,並且開始網絡請求。
我們繼續往父類裏看,看看這個方法到底做了什麼:
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
NSError *serializationError = nil;
//把參數,還有各種東西轉化爲一個request
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//如果解析錯誤,直接返回
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request
uploadProgress:uploadProgress
downloadProgress:downloadProgress
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
- 這個方法做了兩件事:
1.用self.requestSerializer和各種參數去獲取了一個我們最終請求網絡需要的NSMutableURLRequest實例。
2.調用另外一個方法dataTaskWithRequest去拿到我們最終需要的NSURLSessionDataTask實例,並且在完成的回調裏,調用我們傳過來的成功和失敗的回調。 - 注意下面這個方法,我們常用來 push pop搭配,來忽略一些編譯器的警告:
這裏是用來忽略:?帶來的警告,具體的各種編譯器警告描述,可以參考這篇:各種編譯器的警告。#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu" #pragma clang diagnostic pop
- 說到底這個方法還是沒有做實事,我們繼續到requestSerializer方法裏去看,看看AF到底如何拼接成我們需要的request的:
接着我們跑到AFURLRequestSerialization類中:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
//斷言,debug模式下,如果缺少改參數,crash
NSParameterAssert(method);
NSParameterAssert(URLString);
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
//將request的各種屬性循環遍歷
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
//如果自己觀察到的發生變化的屬性,在這些方法裏
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
//把給自己設置的屬性給request設置
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
//將傳入的parameters進行編碼,並添加到request中
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
-
講一下這個方法,這個方法做了3件事:
1)設置request的請求類型,get,post,put…等
2)往request裏添加一些參數設置,其中AFHTTPRequestSerializerObservedKeyPaths()
是一個c函數,返回一個數組,我們來看看這個函數: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; }
其實這個函數就是封裝了一些屬性的名字,這些都是NSUrlRequest的屬性。
再來看看self.mutableObservedChangedKeyPaths
,這個是當前類的一個屬性:@property (readwrite, nonatomic, strong) NSMutableSet *mutableObservedChangedKeyPaths;
在-init方法對這個集合進行了初始化,並且對當前類的和NSUrlRequest相關的那些屬性添加了KVO監聽:
//每次都會重置變化 self.mutableObservedChangedKeyPaths = [NSMutableSet set]; //給這自己些方法添加觀察者爲自己,就是request的各種屬性,set方法 for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { if ([self respondsToSelector:NSSelectorFromString(keyPath)]) { [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext]; } }
KVO觸發的方法:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(__unused id)object change:(NSDictionary *)change context:(void *)context { //當觀察到這些set方法被調用了,而且不爲Null就會添加到集合裏,否則移除 if (context == AFHTTPRequestSerializerObserverContext) { if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) { [self.mutableObservedChangedKeyPaths removeObject:keyPath]; } else { [self.mutableObservedChangedKeyPaths addObject:keyPath]; } } }
至此我們知道
self.mutableObservedChangedKeyPaths
其實就是我們自己設置的request屬性值的集合。
接下來調用:[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
用KVC的方式,把屬性值都設置到我們請求的request中去。
3)把需要傳遞的參數進行編碼,並且設置到request中去:
//將傳入的parameters進行編碼,並添加到request中 mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(id)parameters error:(NSError *__autoreleasing *)error { NSParameterAssert(request); NSMutableURLRequest *mutableRequest = [request mutableCopy]; //從自己的head裏去遍歷,如果有值則設置給request的head [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { if (![request valueForHTTPHeaderField:field]) { [mutableRequest setValue:value forHTTPHeaderField:field]; } }]; //來把各種類型的參數,array dic set轉化成字符串,給request NSString *query = nil; if (parameters) { //自定義的解析方式 if (self.queryStringSerialization) { NSError *serializationError; query = self.queryStringSerialization(request, parameters, &serializationError); if (serializationError) { if (error) { *error = serializationError; } return nil; } } else { //默認解析方式 switch (self.queryStringSerializationStyle) { case AFHTTPRequestQueryStringDefaultStyle: query = AFQueryStringFromParameters(parameters); break; } } } //最後判斷該request中是否包含了GET、HEAD、DELETE(都包含在HTTPMethodsEncodingParametersInURI)。因爲這幾個method的quey是拼接到url後面的。而POST、PUT是把query拼接到http body中的。 if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { if (query && query.length > 0) { mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]]; } } else { //post put請求 // #2864: an empty string is a valid x-www-form-urlencoded payload if (!query) { query = @""; } if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; } //設置請求體 [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]]; } return mutableRequest; }
這個方法做了3件事:
1.從self.HTTPRequestHeaders
中拿到設置的參數,賦值要請求的request裏去
2.把請求網絡的參數,從array dic set這些容器類型轉換爲字符串,具體轉碼方式,我們可以使用自定義的方式,也可以用AF默認的轉碼方式。自定義的方式沒什麼好說的,想怎麼去解析由你自己來決定。我們可以來看看默認的方式:NSString * AFQueryStringFromParameters(NSDictionary *parameters) { NSMutableArray *mutablePairs = [NSMutableArray array]; //把參數給AFQueryStringPairsFromDictionary,拿到AF的一個類型的數據就一個key,value對象,在URLEncodedStringValue拼接keyValue,一個加到數組裏 for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) { [mutablePairs addObject:[pair URLEncodedStringValue]]; } //拆分數組返回參數字符串 return [mutablePairs componentsJoinedByString:@"&"]; } NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) { //往下調用 return AFQueryStringPairsFromKeyAndValue(nil, dictionary); } NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) { NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; // 根據需要排列的對象的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是什麼類型的,然後去遞歸調用自己,直到解析的是除了array dic set以外的元素,然後把得到的參數數組返回。 if ([value isKindOfClass:[NSDictionary class]]) { NSDictionary *dictionary = value; // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries //拿到 for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { id nestedValue = dictionary[nestedKey]; if (nestedValue) { [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; } } } else if ([value isKindOfClass:[NSArray class]]) { NSArray *array = value; for (id nestedValue in array) { [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; } } else if ([value isKindOfClass:[NSSet class]]) { NSSet *set = value; for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)]; } } else { [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]]; } return mutableQueryStringComponents; }
- 轉碼主要是以上三個函數,配合着註釋應該也很好理解:主要是在遞歸調用
AFQueryStringPairsFromKeyAndValue
。判斷vaLue是什麼類型的,然後去遞歸調用自己,直到解析的是除了array dic set以外的元素,然後把得到的參數數組返回。 -
其中有個
AFQueryStringPair
對象,其只有兩個屬性和兩個方法:@property (readwrite, nonatomic, strong) id field; @property (readwrite, nonatomic, strong) id value; - (instancetype)initWithField:(id)field value:(id)value { self = [super init]; if (!self) { return nil; } self.field = field; self.value = value; return self; } - (NSString *)URLEncodedStringValue { if (!self.value || [self.value isEqual:[NSNull null]]) { return AFPercentEscapedStringFromString([self.field description]); } else { return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])]; } }
方法很簡單,現在我們也很容易理解這整個轉碼過程了,我們舉個例子梳理下,就是以下這3步:
@{ @"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
至此,我們原來的容器類型的參數,就這樣變成字符串類型了。
緊接着這個方法還根據該request中請求類型,來判斷參數字符串應該如何設置到request中去。如果是GET、HEAD、DELETE,則把參數quey是拼接到url後面的。而POST、PUT是把query拼接到http body中的:
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { if (query && query.length > 0) { mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]]; } } else { //post put請求 // #2864: an empty string is a valid x-www-form-urlencoded payload if (!query) { query = @""; } if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; } //設置請求體 [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]]; }
至此,我們生成了一個request。
- 轉碼主要是以上三個函數,配合着註釋應該也很好理解:主要是在遞歸調用
我們再回到AFHTTPSessionManager類中來,回到這個方法:
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
NSError *serializationError = nil;
//把參數,還有各種東西轉化爲一個request
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//如果解析錯誤,直接返回
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request
uploadProgress:uploadProgress
downloadProgress:downloadProgress
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
繞了一圈我們又回來了。。
-
我們繼續往下看:當解析錯誤,我們直接調用傳進來的fauler的Block失敗返回了,這裏有一個
self.completionQueue
,這個是我們自定義的,這個是一個GCD的Queue如果設置了那麼從這個Queue中回調結果,否則從主隊列回調。 -
實際上這個Queue還是挺有用的,之前還用到過。我們公司有自己的一套數據加解密的解析模式,所以我們回調回來的數據並不想是主線程,我們可以設置這個Queue,在分線程進行解析數據,然後自己再調回到主線程去刷新UI。
言歸正傳,我們接着調用了父類的生成task的方法,並且執行了一個成功和失敗的回調,我們接着去父類AFURLSessionManger裏看(總算到我們的核心類了..):
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler {
__block NSURLSessionDataTask *dataTask = nil;
//第一件事,創建NSURLSessionDataTask,裏面適配了Ios8以下taskIdentifiers,函數創建task對象。
//其實現應該是因爲iOS 8.0以下版本中會併發地創建多個task對象,而同步有沒有做好,導致taskIdentifiers 不唯一…這邊做了一個串行處理
url_session_manager_create_task_safely(^{
dataTask = [self.session dataTaskWithRequest:request];
});
[self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];
return dataTask;
}
- 我們注意到這個方法非常簡單,就調用了一個
url_session_manager_create_task_safely()
函數,傳了一個Block進去,Block裏就是iOS原生生成dataTask的方法。此外,還調用了一個addDelegateForDataTask
的方法。 -
我們到這先到這個函數裏去看看:
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 //理解下,第一爲什麼用sync,因爲是想要主線程等在這,等執行完,在返回,因爲必須執行完dataTask纔有數據,傳值纔有意義。 //第二,爲什麼要用串行隊列,因爲這塊是爲了防止ios8以下內部的dataTaskWithRequest是併發創建的, //這樣會導致taskIdentifiers這個屬性值不唯一,因爲後續要用taskIdentifiers來作爲Key對應delegate。 dispatch_sync(url_session_manager_creation_queue(), block); } else { block(); } } static dispatch_queue_t url_session_manager_creation_queue() { static dispatch_queue_t af_url_session_manager_creation_queue; static dispatch_once_t onceToken; //保證了即使是在多線程的環境下,也不會創建其他隊列 dispatch_once(&onceToken, ^{ af_url_session_manager_creation_queue = dispatch_queue_create("com.alamofire.networking.session.manager.creation", DISPATCH_QUEUE_SERIAL); }); return af_url_session_manager_creation_queue; }
- 方法非常簡單,關鍵是理解這麼做的目的:爲什麼我們不直接去調用
dataTask = [self.session dataTaskWithRequest:request];
非要繞這麼一圈,我們點進去bug日誌裏看看,原來這是爲了適配iOS8的以下,創建session的時候,偶發的情況會出現session的屬性taskIdentifier這個值不唯一,而這個taskIdentifier是我們後面來映射delegate的key,所以它必須是唯一的。 - 具體原因應該是NSURLSession內部去生成task的時候是用多線程併發去執行的。想通了這一點,我們就很好解決了,我們只需要在iOS8以下同步串行的去生成task就可以防止這一問題發生(如果還是不理解同步串行的原因,可以看看註釋)。
- 題外話:很多同學都會抱怨爲什麼sync我從來用不到,看,有用到的地方了吧,很多東西不是沒用,而只是你想不到怎麼用。
- 方法非常簡單,關鍵是理解這麼做的目的:爲什麼我們不直接去調用
我們接着看到:
[self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];
調用到:
- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
// AFURLSessionManagerTaskDelegate與AFURLSessionManager建立相互關係
delegate.manager = self;
delegate.completionHandler = completionHandler;
//這個taskDescriptionForSessionTasks用來發送開始和掛起通知的時候會用到,就是用這個值來Post通知,來兩者對應
dataTask.taskDescription = self.taskDescriptionForSessionTasks;
// ***** 將AF delegate對象與 dataTask建立關係
[self setDelegate:delegate forTask:dataTask];
// 設置AF delegate的上傳進度,下載進度塊。
delegate.uploadProgressBlock = uploadProgressBlock;
delegate.downloadProgressBlock = downloadProgressBlock;
}
- 總結一下:
1)這個方法,生成了一個AFURLSessionManagerTaskDelegate
,這個其實就是AF的自定義代理。我們請求傳來的參數,都賦值給這個AF的代理了。
2)delegate.manager = self;
代理把AFURLSessionManager這個類作爲屬性了,我們可以看到:
這個屬性是弱引用的,所以不會存在循環引用的問題。@property (nonatomic, weak) AFURLSessionManager *manager;
3)我們調用了[self setDelegate:delegate forTask:dataTask];
我們進去看看這個方法做了什麼:
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
forTask:(NSURLSessionTask *)task
{
//斷言,如果沒有這個參數,debug下crash在這
NSParameterAssert(task);
NSParameterAssert(delegate);
//加鎖保證字典線程安全
[self.lock lock];
// 將AF delegate放入以taskIdentifier標記的詞典中(同一個NSURLSession中的taskIdentifier是唯一的)
self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
// 爲AF delegate 設置task 的progress監聽
[delegate setupProgressForTask:task];
//添加task開始和暫停的通知
[self addNotificationObserverForTask:task];
[self.lock unlock];
}
- 這個方法主要就是把AF代理和task建立映射,存在了一個我們事先聲明好的字典裏。
- 而要加鎖的原因是因爲本身我們這個字典屬性是mutable的,是線程不安全的。而我們對這些方法的調用,確實是會在複雜的多線程環境中,後面會仔細提到線程問題。
- 還有個
[delegate setupProgressForTask:task];
我們到方法裏去看看:
- (void)setupProgressForTask:(NSURLSessionTask *)task {
__weak __typeof__(task) weakTask = task;
//拿到上傳下載期望的數據大小
self.uploadProgress.totalUnitCount = task.countOfBytesExpectedToSend;
self.downloadProgress.totalUnitCount = task.countOfBytesExpectedToReceive;
//將上傳與下載進度和 任務綁定在一起,直接cancel suspend resume進度條,可以cancel...任務
[self.uploadProgress setCancellable:YES];
[self.uploadProgress setCancellationHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask cancel];
}];
[self.uploadProgress setPausable:YES];
[self.uploadProgress setPausingHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask suspend];
}];
if ([self.uploadProgress respondsToSelector:@selector(setResumingHandler:)]) {
[self.uploadProgress setResumingHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask resume];
}];
}
[self.downloadProgress setCancellable:YES];
[self.downloadProgress setCancellationHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask cancel];
}];
[self.downloadProgress setPausable:YES];
[self.downloadProgress setPausingHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask suspend];
}];
if ([self.downloadProgress respondsToSelector:@selector(setResumingHandler:)]) {
[self.downloadProgress setResumingHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask resume];
}];
}
//觀察task的這些屬性
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))
options:NSKeyValueObservingOptionNew
context:NULL];
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))
options:NSKeyValueObservingOptionNew
context:NULL];
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))
options:NSKeyValueObservingOptionNew
context:NULL];
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToSend))
options:NSKeyValueObservingOptionNew
context:NULL];
//觀察progress這兩個屬性
[self.downloadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
[self.uploadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
}
-
這個方法也非常簡單,主要做了以下幾件事:
1)設置downloadProgress
與uploadProgress
的一些屬性,並且把兩者和task的任務狀態綁定在了一起。注意這兩者都是NSProgress的實例對象,(這裏可能又一羣小夥伴楞在這了,這是個什麼…)簡單來說,這就是iOS7引進的一個用來管理進度的類,可以開始,暫停,取消,完整的對應了task的各種狀態,當progress進行各種操作的時候,task也會引發對應操作。
2)給task和progress的各個屬及添加KVO監聽,至於監聽了幹什麼用,我們接着往下看:- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { //是task if ([object isKindOfClass:[NSURLSessionTask class]] || [object isKindOfClass:[NSURLSessionDownloadTask class]]) { //給進度條賦新值 if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) { self.downloadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue]; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) { self.downloadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue]; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) { self.uploadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue]; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]) { self.uploadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue]; } } //上面的賦新值會觸發這兩個,調用block回調,用戶拿到進度 else if ([object isEqual:self.downloadProgress]) { if (self.downloadProgressBlock) { self.downloadProgressBlock(object); } } else if ([object isEqual:self.uploadProgress]) { if (self.uploadProgressBlock) { self.uploadProgressBlock(object); } } }
- 方法非常簡單直觀,主要就是如果task觸發KVO,則給progress進度賦值,應爲賦值了,所以會觸發progress的KVO,也會調用到這裏,然後去執行我們傳進來的
downloadProgressBlock
和uploadProgressBlock
。主要的作用就是爲了讓進度實時的傳遞。 -
主要是觀摩一下大神的寫代碼的結構,這個解耦的編程思想,不愧是大神…
-
還有一點需要注意:我們之前的setProgress和這個KVO監聽,都是在我們AF自定義的delegate內的,是有一個task就會有一個delegate的。所以說我們是每個task都會去監聽這些屬性,分別在各自的AF代理內。看到這,可能有些小夥伴會有點亂,沒關係。等整個講完之後我們還會詳細的去講捋一捋manager、task、還有AF自定義代理三者之前的對應關係。
- 方法非常簡單直觀,主要就是如果task觸發KVO,則給progress進度賦值,應爲賦值了,所以會觸發progress的KVO,也會調用到這裏,然後去執行我們傳進來的
到這裏我們整個對task的處理就完成了。
接着task就開始請求網絡了,還記得我們初始化方法中:
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
我們把AFUrlSessionManager作爲了所有的task的delegate。當我們請求網絡的時候,這些代理開始調用了:
-
AFUrlSessionManager一共實現瞭如上圖所示這麼一大堆NSUrlSession相關的代理。(小夥伴們的順序可能不一樣,樓主根據代理隸屬重新排序了一下)
-
而只轉發了其中3條到AF自定義的delegate中:
AF自定義delegate.png這就是我們一開始說的,AFUrlSessionManager對這一大堆代理做了一些公共的處理,而轉發到AF自定義代理的3條,則負責把每個task對應的數據回調出去。
又有小夥伴問了,我們設置的這個代理不是NSURLSessionDelegate
嗎?怎麼能響應NSUrlSession這麼多代理呢?我們點到類的聲明文件中去看看:
@protocol NSURLSessionDelegate <NSObject>
@protocol NSURLSessionTaskDelegate <NSURLSessionDelegate>
@protocol NSURLSessionDataDelegate <NSURLSessionTaskDelegate>
@protocol NSURLSessionDownloadDelegate <NSURLSessionTaskDelegate>
@protocol NSURLSessionStreamDelegate <NSURLSessionTaskDelegate>
- 我們可以看到這些代理都是繼承關係,而在
NSURLSession
實現中,只要設置了這個代理,它會去判斷這些所有的代理,是否respondsToSelector
這些代理中的方法,如果響應了就會去調用。 -
而AF還重寫了
respondsToSelector
方法:- (BOOL)respondsToSelector:(SEL)selector { //複寫了selector的方法,這幾個方法是在本類有實現的,但是如果外面的Block沒賦值的話,則返回NO,相當於沒有實現! if (selector == @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)) { return self.taskWillPerformHTTPRedirection != nil; } else if (selector == @selector(URLSession:dataTask:didReceiveResponse:completionHandler:)) { return self.dataTaskDidReceiveResponse != nil; } else if (selector == @selector(URLSession:dataTask:willCacheResponse:completionHandler:)) { return self.dataTaskWillCacheResponse != nil; } else if (selector == @selector(URLSessionDidFinishEventsForBackgroundURLSession:)) { return self.didFinishEventsForBackgroundURLSession != nil; } return [[self class] instancesRespondToSelector:selector]; }
這樣如果沒實現這些我們自定義的Block也不會去回調這些代理。因爲本身某些代理,只執行了這些自定義的Block,如果Block都沒有賦值,那我們調用代理也沒有任何意義。
講到這,我們順便看看AFUrlSessionManager的一些自定義Block:@property (readwrite, nonatomic, copy) AFURLSessionDidBecomeInvalidBlock sessionDidBecomeInvalid; @property (readwrite, nonatomic, copy) AFURLSessionDidReceiveAuthenticationChallengeBlock sessionDidReceiveAuthenticationChallenge; @property (readwrite, nonatomic, copy) AFURLSessionDidFinishEventsForBackgroundURLSessionBlock didFinishEventsForBackgroundURLSession; @property (readwrite, nonatomic, copy) AFURLSessionTaskWillPerformHTTPRedirectionBlock taskWillPerformHTTPRedirection; @property (readwrite, nonatomic, copy) AFURLSessionTaskDidReceiveAuthenticationChallengeBlock taskDidReceiveAuthenticationChallenge; @property (readwrite, nonatomic, copy) AFURLSessionTaskNeedNewBodyStreamBlock taskNeedNewBodyStream; @property (readwrite, nonatomic, copy) AFURLSessionTaskDidSendBodyDataBlock taskDidSendBodyData; @property (readwrite, nonatomic, copy) AFURLSessionTaskDidCompleteBlock taskDidComplete; @property (readwrite, nonatomic, copy) AFURLSessionDataTaskDidReceiveResponseBlock dataTaskDidReceiveResponse; @property (readwrite, nonatomic, copy) AFURLSessionDataTaskDidBecomeDownloadTaskBlock dataTaskDidBecomeDownloadTask; @property (readwrite, nonatomic, copy) AFURLSessionDataTaskDidReceiveDataBlock dataTaskDidReceiveData; @property (readwrite, nonatomic, copy) AFURLSessionDataTaskWillCacheResponseBlock dataTaskWillCacheResponse; @property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidFinishDownloadingBlock downloadTaskDidFinishDownloading; @property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidWriteDataBlock downloadTaskDidWriteData; @property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidResumeBlock downloadTaskDidResume;
各自對應的還有一堆這樣的set方法:
- (void)setSessionDidBecomeInvalidBlock:(void (^)(NSURLSession *session, NSError *error))block { self.sessionDidBecomeInvalid = block; }
方法都是一樣的,就不重複粘貼佔篇幅了。
主要談談這個設計思路- 作者用@property把這個些Block屬性在.m文件中聲明,然後複寫了set方法。
- 然後在.h中去聲明這些set方法:
爲什麼要繞這麼一大圈呢?原來這是爲了我們這些用戶使用起來方便,調用set方法去設置這些Block,能很清晰的看到Block的各個參數與返回值。大神的精髓的編程思想無處不體現…- (void)setSessionDidBecomeInvalidBlock:(nullable void (^)(NSURLSession *session, NSError *error))block;
接下來我們就講講這些代理方法做了什麼(按照順序來):
NSURLSessionDelegate
代理1:
//當前這個session已經失效時,該代理方法被調用。
/*
如果你使用finishTasksAndInvalidate函數使該session失效,
那麼session首先會先完成最後一個task,然後再調用URLSession:didBecomeInvalidWithError:代理方法,
如果你調用invalidateAndCancel方法來使session失效,那麼該session會立即調用上面的代理方法。
*/
- (void)URLSession:(NSURLSession *)session
didBecomeInvalidWithError:(NSError *)error
{
if (self.sessionDidBecomeInvalid) {
self.sessionDidBecomeInvalid(session, error);
}
[[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDidInvalidateNotification object:session];
}
- 方法調用時機註釋寫的很清楚,就調用了一下我們自定義的Block,還發了一個失效的通知,至於這個通知有什麼用。很抱歉,AF沒用它做任何事,只是發了…目的是用戶自己可以利用這個通知做什麼事吧。
- 其實AF大部分通知都是如此。當然,還有一部分通知AF還是有自己用到的,包括配合對UIKit的一些擴展來使用,後面我們會有單獨篇幅展開講講這些UIKit的擴展類的實現。
代理2:
//2、https認證
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
//挑戰處理類型爲 默認
/*
NSURLSessionAuthChallengePerformDefaultHandling:默認方式處理
NSURLSessionAuthChallengeUseCredential:使用指定的證書
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑戰
*/
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// sessionDidReceiveAuthenticationChallenge是自定義方法,用來如何應對服務器端的認證挑戰
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
// 此處服務器要求客戶端的接收認證挑戰方法是NSURLAuthenticationMethodServerTrust
// 也就是說服務器端需要客戶端返回一個根據認證挑戰的保護空間提供的信任(即challenge.protectionSpace.serverTrust)產生的挑戰證書。
// 而這個證書就需要使用credentialForTrust:來創建一個NSURLCredential對象
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 基於客戶端的安全策略來決定是否信任該服務器,不信任的話,也就沒必要響應挑戰
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 創建挑戰證書(注:挑戰方式爲UseCredential和PerformDefaultHandling都需要新建挑戰證書)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 確定挑戰的方式
if (credential) {
//證書挑戰
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
//默認挑戰 唯一區別,下面少了這一步!
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
//取消挑戰
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
//默認挑戰方式
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
//完成挑戰
if (completionHandler) {
completionHandler(disposition, credential);
}
}
- 函數作用:
web服務器接收到客戶端請求時,有時候需要先驗證客戶端是否爲正常用戶,再決定是夠返回真實數據。這種情況稱之爲服務端要求客戶端接收挑戰(NSURLAuthenticationChallenge challenge)。接收到挑戰後,客戶端要根據服務端傳來的challenge來生成completionHandler所需的NSURLSessionAuthChallengeDisposition disposition和NSURLCredential credential(disposition指定應對這個挑戰的方法,而credential是客戶端生成的挑戰證書,注意只有challenge中認證方法爲NSURLAuthenticationMethodServerTrust的時候,才需要生成挑戰證書)。最後調用completionHandler迴應服務器端的挑戰。- 函數討論:
該代理方法會在下面兩種情況調用:
- 當服務器端要求客戶端提供證書時或者進行NTLM認證(Windows NT LAN Manager,微軟提出的WindowsNT挑戰/響應驗證機制)時,此方法允許你的app提供正確的挑戰證書。
- 當某個session使用SSL/TLS協議,第一次和服務器端建立連接的時候,服務器會發送給iOS客戶端一個證書,此方法允許你的app驗證服務期端的證書鏈(certificate keychain)
注:如果你沒有實現該方法,該session會調用其NSURLSessionTaskDelegate的代理方法URLSession:task:didReceiveChallenge:completionHandler: 。
這裏,我把官方文檔對這個方法的描述翻譯了一下。
總結一下,這個方法其實就是做https認證的。看看上面的註釋,大概能看明白這個方法做認證的步驟,我們還是如果有自定義的做認證的Block,則調用我們自定義的,否則去執行默認的認證步驟,最後調用完成認證:
//完成挑戰
if (completionHandler) {
completionHandler(disposition, credential);
}
代理3:
//3、 當session中所有已經入隊的消息被髮送出去後,會調用該代理方法。
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
if (self.didFinishEventsForBackgroundURLSession) {
dispatch_async(dispatch_get_main_queue(), ^{
self.didFinishEventsForBackgroundURLSession(session);
});
}
}
官方文檔翻譯:
函數討論:
- 在iOS中,當一個後臺傳輸任務完成或者後臺傳輸時需要證書,而此時你的app正在後臺掛起,那麼你的app在後臺會自動重新啓動運行,並且這個app的UIApplicationDelegate會發送一個application:handleEventsForBackgroundURLSession:completionHandler:消息。該消息包含了對應後臺的session的identifier,而且這個消息會導致你的app啓動。你的app隨後應該先存儲completion handler,然後再使用相同的identifier創建一個background configuration,並根據這個background configuration創建一個新的session。這個新創建的session會自動與後臺任務重新關聯在一起。
- 當你的app獲取了一個URLSessionDidFinishEventsForBackgroundURLSession:消息,這就意味着之前這個session中已經入隊的所有消息都轉發出去了,這時候再調用先前存取的completion handler是安全的,或者因爲內部更新而導致調用completion handler也是安全的。
NSURLSessionTaskDelegate
代理4:
//被服務器重定向的時候調用
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
{
NSURLRequest *redirectRequest = request;
// step1. 看是否有對應的user block 有的話轉發出去,通過這4個參數,返回一個NSURLRequest類型參數,request轉發、網絡重定向.
if (self.taskWillPerformHTTPRedirection) {
//用自己自定義的一個重定向的block實現,返回一個新的request。
redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);
}
if (completionHandler) {
// step2. 用request重新請求
completionHandler(redirectRequest);
}
}
- 一開始我以爲這個方法是類似
NSURLProtocol
,可以在請求時自己主動的去重定向request,後來發現不是,這個方法是在服務器去重定向的時候,纔會被調用。爲此我寫了段簡單的PHP測了測:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Welcome extends CI_Controller {
public function index()
{
header("location: http://www.huixionghome.cn/");
}
}
證實確實如此,當我們服務器重定向的時候,代理就被調用了,我們可以去重新定義這個重定向的request。
- 關於這個代理還有一些需要注意的地方:
此方法只會在default session或者ephemeral session中調用,而在background session中,session task會自動重定向。
這裏指的模式是我們一開始Init的模式:
if (!configuration) {
configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
self.sessionConfiguration = configuration;
這個模式總共分爲3種:
對於NSURLSession對象的初始化需要使用NSURLSessionConfiguration,而NSURLSessionConfiguration有三個類工廠方法:
+defaultSessionConfiguration 返回一個標準的 configuration,這個配置實際上與 NSURLConnection 的網絡堆棧(networking stack)是一樣的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享NSURLCredentialStorage。
+ephemeralSessionConfiguration 返回一個預設配置,這個配置中不會對緩存,Cookie 和證書進行持久性的存儲。這對於實現像祕密瀏覽這種功能來說是很理想的。
+backgroundSessionConfiguration:(NSString *)identifier 的獨特之處在於,它會創建一個後臺 session。後臺 session 不同於常規的,普通的 session,它甚至可以在應用程序掛起,退出或者崩潰的情況下運行上傳和下載任務。初始化時指定的標識符,被用於向任何可能在進程外恢復後臺傳輸的守護進程(daemon)提供上下文。
代理5:
//https認證
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.taskDidReceiveAuthenticationChallenge) {
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
- 鑑於篇幅,就不去貼官方文檔的翻譯了,大概總結一下:
之前我們也有一個https認證,功能一樣,執行的內容也完全一樣。 - 區別在於這個是non-session-level級別的認證,而之前的是session-level級別的。
- 相對於它,多了一個參數task,然後調用我們自定義的Block會多回傳這個task作爲參數,這樣我們就可以根據每個task去自定義我們需要的https認證方式。
代理6:
//當一個session task需要發送一個新的request body stream到服務器端的時候,調用該代理方法。
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler
{
NSInputStream *inputStream = nil;
//有自定義的taskNeedNewBodyStream,用自定義的,不然用task裏原始的stream
if (self.taskNeedNewBodyStream) {
inputStream = self.taskNeedNewBodyStream(session, task);
} else if (task.originalRequest.HTTPBodyStream && [task.originalRequest.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]) {
inputStream = [task.originalRequest.HTTPBodyStream copy];
}
if (completionHandler) {
completionHandler(inputStream);
}
}
- 該代理方法會在下面兩種情況被調用:
- 如果task是由uploadTaskWithStreamedRequest:創建的,那麼提供初始的request body stream時候會調用該代理方法。
- 因爲認證挑戰或者其他可恢復的服務器錯誤,而導致需要客戶端重新發送一個含有body stream的request,這時候會調用該代理。
代理7:
/*
//週期性地通知代理髮送到服務器端數據的進度。
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
// 如果totalUnitCount獲取失敗,就使用HTTP header中的Content-Length作爲totalUnitCount
int64_t totalUnitCount = totalBytesExpectedToSend;
if(totalUnitCount == NSURLSessionTransferSizeUnknown) {
NSString *contentLength = [task.originalRequest valueForHTTPHeaderField:@"Content-Length"];
if(contentLength) {
totalUnitCount = (int64_t) [contentLength longLongValue];
}
}
if (self.taskDidSendBodyData) {
self.taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalUnitCount);
}
}
- 就是每次發送數據給服務器,會回調這個方法,通知已經發送了多少,總共要發送多少。
- 代理方法裏也就是僅僅調用了我們自定義的Block而已。
未完總結:
- 其實寫了這麼多,還沒有講到真正重要的地方,但是因爲已經接近簡書最大篇幅,所以只能先在這裏結個尾了。
- 如果能看到這裏,說明你是個非常有耐心,非常好學,非常nice的iOS開發。樓主爲你點個贊。那麼相信你也不吝嗇手指動一動,給本文點個喜歡…順便關注一下樓主…畢竟寫了這麼多…也很辛苦…咳咳,我不小心說出心聲了麼?