在AFNetworking到底长啥样(上)中简单介绍了AFN涉及的主要类及其结构,接下来以一个简单的POST请求探寻其内部是如何实现的。
一、环境搭建
- 服务器配置
本例中直接使用iMac自带的Apache,并为其开启PHP支持。在服务器目录下编写index.php文件如下:
<?php echo @"This is Layne‘s Response"; ?>
- 编写测试App
创建一个测试App,在主界面上增加一个按钮,在按钮的点击函数中发起网络请求,如下:
- (AFHTTPSessionManager *)manager{//lazy if(!_manager){ _manager = [AFHTTPSessionManager manager]; } return _manager; } - (void)click{ [self.manager POST:@"http://www.layne.com" parameters:@{@"name":@"layne",@"age":@30} headers:@{@"TestName":@"myTest"} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSLog(@"success:%@",responseObject); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"fail:%@",error); }]; }
二、一个POST请求的前世今生
启动测试App,点击按钮。接下里让我们看看AFN是如何优雅的管理网络请求的。
1.初始化
- AFHTTPSessionManager的初始化
self.manager
是AFHTTPSessionManager
的实例,它使用类方法+ manager
初始化。这个类方法最终调用如下方法:- (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration { self = [super initWithSessionConfiguration:configuration];//①configuration=nil if (!self) { return nil; } // Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) { url = [url URLByAppendingPathComponent:@""]; } self.baseURL = url; self.requestSerializer = [AFHTTPRequestSerializer serializer];//② self.responseSerializer = [AFJSONResponseSerializer serializer];//③ return self; }
①中调用父类的方法初始化父类相关属性,这包括:
operationQueue responseSerializer(为AFJSONResponseSerializer实例) securityPolicy(为默认,即不进行SSL验证) reachabilityManager mutableTaskDelegatesKeyedByTaskIdentifier(管理{taskID:delegate}) lock(用于操作mutableTaskDelegatesKeyedByTaskIdentifier时保证线程安全) session(NSURLSession实例,设置其代理为self,queue为operationQueue)
②中设置其requestSerializer为AFHTTPRequestSerializer实例,③重设其responseSerializer为AFJSONResponseSerializer实例。
- AFHTTPRequestSerializer的初始化
requestSerializer被设置成了AFHTTPRequestSerializer的实例,初始化是通过类方法
+serializer
实现的。内容包括:stringEncoding(为NSUTF8StringEncoding) mutableHTTPRequestHeaders(保存用户设置的header,默认会添加"Accept-Language"和"User-Agent",其中"User-Agent"还进行了ICU转换以保证必须为ASCII字符) HTTPMethodsEncodingParametersInURI(为GET、HEAD、DELETE) mutableObservedChangedKeyPaths (监听"allowsCellularAccess"、"cachePolicy"、"HTTPShouldHandleCookies"、"HTTPShouldUsePipelining"、"networkServiceType"、"timeoutInterval",一旦用户设置了这6个属性,则对应属性的名会被存入,最后其值会加到request中去,本质是增量添加:“谁改变添加谁”)
- AFJSONResponseSerializer的初始化
responseSerializer被设置成为了AFJSONResponseSerializer的实例,初始化是通过类方法
+serializer
实现的。内容包括:readingOptions(为0) acceptableStatusCodes(可接受的code,初始化为[200~199]) acceptableContentTypes(可接受的contenttype,设置为application/json、text/json、text/javascript) 注:其父类AFHTTPResponseSerializer的acceptableContentTypes=nil,即接受任何Content。
至此必要的初始化已完成。
2.发起请求
(1) POST
进入到POST函数中:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters headers:headers uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure];
[dataTask resume];
return dataTask;
}
即最终生成一个dataTask,然后resume启动,并将dataTask返回。
(2) 生成dataTask
生成的dataTask是通过如下函数实现的,本例中传入的参数值也已标明:
- dataTaskWithHTTPMethod: //@"POST"
URLString: //@"http://www.layne.com"
parameters: //@{@"name":@"layne",@"age":@30}
headers: //@{@"TestName":@"myTest"}
uploadProgress: //nil
downloadProgress: //nil
success: //successBlock{NSLog(@"success:%@",responseObject);}
failure: //failureBlock{NSLog(@"fail:%@",error);}
其内部执行的步骤如下:
Step1:调用requestSerializer的如下方法创建mutableRequest:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method //@"POST"
URLString:(NSString *)URLString//@"http://www.layne.com"
parameters:(id)parameters//@{@"name":@"layne",@"age":@30}
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(URLString);
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;//设置为POST
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}//根据用户是否设置了allowsCellularAccess、cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining、networkServiceType、timeoutInterval来设置mutableRequest对应字段值。本例中由于未进行任何设置,因此上述逻辑不会有效果。
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];//①
return mutableRequest;
}
①是调用AFURLRequestSerialization协议方法对mutableRequest进行进一步处理,包括:
- 设置默认header,如在requestSerializer初始化时设置默认header:User-Agent和Accept-Language
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { if (![request valueForHTTPHeaderField:field]) { [mutableRequest setValue:value forHTTPHeaderField:field]; } }];
- 处理参数
若用户自定义了处理参数的block(self.queryStringSerialization),则使用该block;否则使用默认的处理方式,会被最终处理成如下格式:name=layne&age=30。
- 根据HTTPMethod决定参数串放到哪里:GET/HEAD/DELETE直接拼接到url;其他(如POST)方法放到HTTPBody中(使用stringEncoding(NSUTF8StringEncoding)编码),并设置Content-Type为application/x-www-form-urlencoded。
至此,生成的mutableRequest结构如下:
mutableRequest{
URL: "http://www.layne.com"
Method: "POST"
Header: "Accept-Language" = "en;q=1";
"User-Agent" = "TestApp/1.0 (iPhone; iOS 13.1; Scale/3.00)";
"Content-Type" = "application/x-www-form-urlencoded";
HttpBody <6167653d 3330266e 616d653d 6c61796e 65> //name=layne&age=30(utf-8)
}
Step2:合并传入的header
for (NSString *headerField in headers.keyEnumerator) {
[request addValue:headers[headerField] forHTTPHeaderField:headerField];
}
执行之后mutableRequest结构如下:
mutableRequest{
URL: "http://www.layne.com"
Method: "POST"
Header: "Accept-Language" = "en;q=1";
"User-Agent" = "TestApp/1.0 (iPhone; iOS 13.1; Scale/3.00)";
"Content-Type" = "application/x-www-form-urlencoded";
"TestName":"myTest";
HttpBody <6167653d 3330266e 616d653d 6c61796e 65> //name=layne&age=30(utf-8)
}
Step3:判断Step1中生成mutableRequest的过程是否出错,若出错,则调用failureBlock并返回。
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
Step4:根据mutableRequest生成dataTask。
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request //request
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock//nil
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock//nil
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler /*block{
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}*/
{
__block NSURLSessionDataTask *dataTask = nil;
url_session_manager_create_task_safely(^{ //①
dataTask = [self.session dataTaskWithRequest:request];
});
[self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];//②
return dataTask;//③
}
说明:
①中不直接生成dataTask,是因为在iOS8以下的系统上,若是在并行队列上创建dataTask会导致completionHandler调用出错。因此为了解决这个问题,针对iOS8以下系统,AFN使用自己维护的一个串行队列来创建dataTask。具体问题描述如下:
Due to this bug: http://openradar.appspot.com/radar?id=5871104061079552 in NSURLSessionTask, creating tasks on a concurrent queue can cause incorrect completionHandlers to get called.
When a duplicate taskIdentifier is returned by the task, the previous completionHandler gets cleared out and replaced with the new one. If the data for the first request comes back before the second request‘s data, the first response is then called against the second completionHandler.
I‘m not sure what AFNetworking should do here — it could enforce creating tasks on a serial queue or it could just advise people to do so. We could also add a test to assert that the taskIdentifier is not a duplicate?
②的作用是为dataTask生成对应的delegate,以调用回调方法。
- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask //dataTask
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock//nil
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock//nil
completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler/*block{
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}*/
{
AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];//a
delegate.manager = self;//b
delegate.completionHandler = completionHandler;//c
dataTask.taskDescription = self.taskDescriptionForSessionTasks;//d
[self setDelegate:delegate forTask:dataTask];//e
delegate.uploadProgressBlock = uploadProgressBlock;//nil
delegate.downloadProgressBlock = downloadProgressBlock;//nil
}
a. 为dataTask生成AFURLSessionManagerTaskDelegate实例。delegate内部维护着:
一个mutableData用来保存收到的response data。
一个uploadProgress和downloadProgress用来标明上传/下载进度。它们的cancel、suspend和resume操作与dataTask进行了绑定,即通过对它们进行cancel、suspend和resume操作就可以操作dataTask的cancel、suspend和resume。此外,还采用KVO监听uploadProgress和downloadProgress的进度变化,从而调用用户自定义的block:uploadProgressBlock和downloadProgressBlock。
b. delegate弱引用当前的manager。
c.delegate保存完成回调。
③返回最终的dataTask,并启动。
(3) 处理Response
AFN是基于NSURLSession的,涉及以下几个协议:
NSURLSessionDelegate
- URLSession:didBecomeInvalidWithError:
- URLSession:didReceiveChallenge:completionHandler:
- URLSessionDidFinishEventsForBackgroundURLSession:
NSURLSessionTaskDelegate
- URLSession:willPerformHTTPRedirection:newRequest:completionHandler:
- URLSession:task:didReceiveChallenge:completionHandler:
- URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
- URLSession:task:needNewBodyStream:
- URLSession:task:didCompleteWithError:
- URLSession:task:didFinishCollectingMetrics:
NSURLSessionDataDelegate
- URLSession:dataTask:didReceiveResponse:completionHandler:
- URLSession:dataTask:didBecomeDownloadTask:
- URLSession:dataTask:didReceiveData:
- URLSession:dataTask:willCacheResponse:completionHandler:
NSURLSessionDownloadDelegate
- URLSession:downloadTask:didFinishDownloadingToURL:
- URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:
- URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
回调调用顺序:
① URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
由于使用的是POST方式,因此该回调是首先调用的。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:来更新delegate维护的uploadProgress。若用户自定义了taskDidSendBodyData block,则调用。
② URLSession:task:didFinishCollectingMetrics:
该方法调用时机不定,用来收集整个请求的信息。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didFinishCollectingMetrics:来为delegate.sessionTaskMetrics赋值。若用户自定义了taskDidFinishCollectingMetrics block,则调用。
③ URLSession:dataTask:didReceiveData:
收到response data时执行。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didCompleteWithError:来更新delegate内部维护的downloadProgress并使用mutableData保存response data。若用户自定义了dataTaskDidReceiveData block,则调用。
④ URLSession:task:didCompleteWithError:
这是最后执行的回调。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didCompleteWithError:,并删除delegate和task的对应关系。若用户自定义了taskDidComplete block,则调用。
数据的处理主要在delegate的URLSession:task:didCompleteWithError:中,主要做了以下几件工作:
- 构造userInfo字典:
userInfo{ AFNetworkingTaskDidCompleteResponseSerializerKey:<AFJSONResponseSerializer: 0x600003db6820>, AFNetworkingTaskDidCompleteSessionTaskMetrics:"...", AFNetworkingTaskDidCompleteResponseDataKey:"<length=24,bytes=0x567283d>"//response data /*若出错则会包含以下key*/ AFNetworkingTaskDidCompleteErrorKey:"error" //error数据 }
- 若出错,则直接调用completeHandler回调,之后将上面的userInfo以通知AFNetworkingTaskDidCompleteNotification的形式广播出去。
- 若未出错,则使用responseSerializer(AFJSONResponseSerializer实例)将data转换为NSDictionary。若转换过程未出错,则在userInfo中增加{AFNetworkingTaskDidCompleteSerializedResponseKey:responseObject};若出错,则增加{AFNetworkingTaskDidCompleteErrorKey:error}。之后调用completeHandler回调,并将userInfo以通知AFNetworkingTaskDidCompleteNotification的形式广播出去。
经过上面的处理,最终要么error要么responseObject会返回到POST的回调中去。整个网络请求就完成了。
三、AFNetworking中的干货
上面我们跟着一个简单的POST请求了解了AFN的整个工作流程,但还有一些细节我觉得还是值得我们学习的。
- 如果response不是标准格式的JSON数据或者我们需要对原始data进行加密解密操作该如何做?
答:将responseSerializer设置为AFHTTPResponseSerializer的实例,这样response data 会以原始data的形式返回给上层。AFHTTPResponseSerializer仅针对code和contenttype进行校验,而默认的AFJSONResponseSerializer除了校验code和contenttype之外还对JSON格式进行校验,并直接解析成NSDictionary。当然,如果想对response data想要做最全面的自定义处理,最直接的方式当然是自定义AFHTTPResponseSerializer的子类,并重写协议方法
- (id)responseObjectForResponse:(NSHTTPURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error
- 如果返回的JSON数据中包含NSNull数据该如何处理?
答:将responseSerializer(AFJSONResponseSerializer实例)的removesKeysWithNullValues属性设置为YES。这样一来在JSON转换为NSDictionary之后会将NSDictionary中的NSNull值去除。
- 如果我想自己更改请求参数的格式(即不用默认的name=layne&age=30这种)该如何设置?
答:调用requestSerializer的-setQueryStringSerializationWithBlock:设置自定义的格式。在requestSerializer处理参数的时候会先去判断queryStringSerialization block是否为空,若不为空,则使用该block处理参数。若为空,则使用默认的格式(如name=layne&age=30)生成参数串。
- 除了使用GET/POST方法的回调,还可以通过什么方式获得网络请求结果?
答:还可以使用notification。AFN中有一个名为AFNetworkingTaskDidCompleteNotification的通知,可以通过监听该通知获取网络请求结果。AFURLSessionManagerTaskDelegate的URLSession:task:didCompleteWithError:中在调用completeHandler之后会发送通知:
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{ if (self.completionHandler) { self.completionHandler(task.response, responseObject, serializationError);//回调 } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];//将task和userInfo一起广播出去 }); });
- 如何更改回调所在的线程?
答:指定manager的completeQueue。这样回调就会在其他线程中执行。
_manager.completionQueue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
- AFURLSessionManager如何获取NSURLSession维护的task?
答:使用getTasksWithCompletionHandler:API并使用信号量保证线程安全。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath { //tasks、uploadTask、downloadTasks __block NSArray *tasks = nil; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) { tasks = dataTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) { tasks = uploadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) { tasks = downloadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) { tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"]; } dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); return tasks; }
- 若我想取消一个刚发起的网络请求该如何做?
答:AFHTTPSessionManager的POST/GET方法返回的是task对象,直接[task cancel]即可。如:
NSURLSessionDataTask *task = [self.manager POST:@"http://www.layne.com" parameters:@{@"name":@"layne",@"age":@30} headers:@{@"TestName":@"myTest"} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSLog(@"success:%@",responseObject); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"fail:%@",error); }]; [task cancel];//直接cancel
- 若我想取消所有task该如何做?
答:使用如下方法。建议resetSession传入YES将session重置,否则下次网络请求会crash,并提示:“Attempted to create a task in a session that has been invalidated”
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks resetSession:(BOOL)resetSession { if (cancelPendingTasks) { [self.session invalidateAndCancel]; } else { [self.session finishTasksAndInvalidate]; } if (resetSession) { self.session = nil; } } 例如: [self.manager invalidateSessionCancelingTasks:YES resetSession:YES];
至此AFNetworking是如何工作的我们就知道了。看了源码之后不得不感叹作者真是神人,不仅优雅的给出了NSURLSession的使用范例,而且还包含了业务层面的巧妙设计。嗯,阅读源码使我快乐^_^。
原文地址:https://blog.51cto.com/laynestone/2455938