首页 技术 正文
技术 2022年11月20日
0 收藏 781 点赞 4,156 浏览 26499 个字

【原】AFNetworking源码阅读(三)

本文转载请注明出处 —— polobymulberry-博客园

1. 前言


上一篇的话,主要是讲了如何通过构建一个request来生成一个data task。但是对于NSURLSession部分却没有提及。主要是精力有限,准备在这一部分把NSURLSession的知识好好梳理一遍。一切先从上一篇中的addDelegateForDataTask:函数说起,然后再介绍AFURLSessionManagerTaskDelegate,最后结合AFURLSessionManager中的NSURLSession梳理一遍(可能会将部分内容放到下一篇)。

2. 由addDelegateForDataTask引发


注意addDelegateForDataTask:这个函数并不是AFURLSessionManagerTaskDelegate的函数,而是AFURLSessionManager的一个函数。这也侧面说明了AFURLSessionManagerTaskDelegate和NSURLSessionTask的关系是由AFURLSessionManager管理的

该函数除了对于AFURLSessionManagerTaskDelegate类型的成员变量delegate设置之外,最关键的代码就是

[self setDelegate:delegate forTask:dataTask];

这个setDelegate:forTask:函数字面意思是将一个session task和一个AFURLSessionManagerTaskDelegate类型的delegate变量绑在一起,而这个绑在一起的工作是由我们的AFURLSessionManager所做。至于绑定的过程,就是以该session task的taskIdentifier为keydelegate为value,赋值给mutableTaskDelegatesKeyedByTaskIdentifier这个NSMutableDictionary类型的变量。知道了这两者是关联在一起的话,马上就会产生另外的问题 —— 为什么要关联以及怎么关联在一起?索性我们好好研究下setDelegate:forTask:这个函数:

- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
forTask:(NSURLSessionTask *)task
{
NSParameterAssert(task);
NSParameterAssert(delegate); [self.lock lock];
self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
[delegate setupProgressForTask:task];
[self addNotificationObserverForTask:task];
[self.lock unlock];
}

代码首先是基本的判断,判断session task和delegate是否为空,这里实现方式(NSParameterAssert)每次看到都加深一下印象。接着就是使用NSLock来加锁,这个很简单,和@synchronized作用类似,不过@synchronized多了一个可以使用变量作为互斥信号量的功能,这里就不细说了。临界区的代码(lock和unlock之间的代码)也是分为三个部分:

. self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
. [delegate setupProgressForTask:task];
. [self addNotificationObserverForTask:task];
  • 第一部分不赘述了。
  • 第二部分我扫了下代码,好像是设置两个NSProgress的变量 – uploadProgress和downloadProgress。
  • 第三部分就是给session task添加了两个KVO事件。

具体细节详述如下(包含第二部分和第三部分详述):

2.1 –[AFURLSessionManager setupProgressForTask:]

上面简单提了下该函数是为了设置uploadProgress和downloadProgress这两个NSProgress变量。这种设置也是很合理的,毕竟session task的任务中需要记录进度的,要不是上传任务,要不就是下载任务。

我们来看看setupProgressForTask:的具体实现:

- (void)setupProgressForTask:(NSURLSessionTask *)task {
__weak __typeof__(task) weakTask = task; self.uploadProgress.totalUnitCount = task.countOfBytesExpectedToSend;
self.downloadProgress.totalUnitCount = task.countOfBytesExpectedToReceive;
[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 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]; [self.downloadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
[self.uploadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
}

先是设置两个progress的totalUnitCount:

self.uploadProgress.totalUnitCount = task.countOfBytesExpectedToSend;
self.downloadProgress.totalUnitCount = task.countOfBytesExpectedToReceive;

上传的totalUnitCount就对应期望发送(send)的数据大小,下载任务的就对应期望接收(receive)的数据大小。

接着就是设置这两个NSProgress对应的cancelpauseresume这三个状态,正好对应session task的cancel、suspend和resume三个状态,详见上方源码。

最后一部分代码是关键,给session task和两个progress添加KVO。也就是说该AFURLSessionManager的对象需要观察以下属性:

  • NSURLSessionTask的countOfBytesReceived、countOfBytesExpectedToReceive、countOfBytesSent、countOfBytesExpectedToSend属性
  • NSProgress的fractionCompleted属性(任务已经完成的比例,取值为0~1)

看了KVO,马上跳到observeValueForKeyPath:ofObject:change:context:函数中:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([object isKindOfClass:[NSURLSessionTask class]]) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
self.downloadProgress.completedUnitCount = [change[@"new"] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) {
self.downloadProgress.totalUnitCount = [change[@"new"] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
self.uploadProgress.completedUnitCount = [change[@"new"] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]) {
self.uploadProgress.totalUnitCount = [change[@"new"] longLongValue];
}
}
else if ([object isEqual:self.downloadProgress]) {
if (self.downloadProgressBlock) {
self.downloadProgressBlock(object);
}
}
else if ([object isEqual:self.uploadProgress]) {
if (self.uploadProgressBlock) {
self.uploadProgressBlock(object);
}
}
}

总结一下,也就是说

  • downloadProgress.completedUnitCount 《==      countOfBytesReceived更新
  • downloadProgress.totalUnitCount          《==      countOfBytesExpectedToReceive更新
  • uploadProgress.completedUnitCount      《==      countOfBytesSent更新
  • uploadProgress.totalUnitCount               《==      countOfBytesExpectedToSend更新
  • 调用自定义的downloadProgressBlock        《==      downloadProgress.fractionCompleted更新
  • 调用自定义的uploadProgressBlock             《==      uploadProgress.fractionCompleted更新

最后两个KVO事件中使用的block其实就是根据NSProgress的状态做用户自定义的行为,比如需要更新UI进度条的状态之类的。

2.2 –[AFURLSessionManager addNotificationObserverForTask:]

- (void)addNotificationObserverForTask:(NSURLSessionTask *)task {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidResume:) name:AFNSURLSessionTaskDidResumeNotification object:task];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidSuspend:) name:AFNSURLSessionTaskDidSuspendNotification object:task];
}

此处如果往深的地方研究,会涉及到很多runtime甚至methodSwizzle的代码,后面会专门开一个章节,研究下这段代码。此处我们只需知道,当NSURLSessionTask调用resume函数时,会postNotificationName:AFNSURLSessionTaskDidResumeNotification,从而执行taskDidResume:方法:

- (void)taskDidResume:(NSNotification *)notification {
NSURLSessionTask *task = notification.object;
if ([task respondsToSelector:@selector(taskDescription)]) {
if ([task.taskDescription isEqualToString:self.taskDescriptionForSessionTasks]) {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidResumeNotification object:task];
});
}
}
}

有了AFNetworkingTaskDidResumeNotification就方便了,之前我们UIRefreshControl+AFNEtworking中就使用AFNetworkingTaskDidResumeNotification作为NotificationName。同理,参考源码对AFNSURLSessionTaskDidSuspendNotification的处理,这里就不赘述了。

3. 详解AFURLSessionManager的NSURLSession的相关代理


我们之前在看GET:等这些上层函数时,发现内部实现就是为了生成一个session task。而这个session task与网络具体如何交互,如何处理数据的方法,则是写在NSURLSession的相关代理方法中。虽然GET:这些方法是AFHTTPSessionManager的方法,但是AFURLSessionManager是AFHTTPSessionManager的父类,所以调用的NSURLSession的相关代理的实现其实是在AFURLSessionManager中实现的,我们可以看看AFURLSessionManager实现了哪些NSURLSession相关的代理方法:

 ### `NSURLSessionDelegate` - `URLSession:didBecomeInvalidWithError:`
- `URLSession:didReceiveChallenge:completionHandler:`
- `URLSessionDidFinishEventsForBackgroundURLSession:` ### `NSURLSessionTaskDelegate` - `URLSession:willPerformHTTPRedirection:newRequest:completionHandler:`
- `URLSession:task:didReceiveChallenge:completionHandler:`
- `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:`
- `URLSession:task:didCompleteWithError:` ### `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:`

3.1 NSURLSessionDelegate

3.1.1 – URLSession:didBecomeInvalidWithError:

函数声明:

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error

函数作用:

当前这个session已经失效时,该代理方法被调用。

函数讨论:

如果你使用finishTasksAndInvalidate函数使该session失效,那么session首先会先完成最后一个task,然后再调用URLSession:didBecomeInvalidWithError:代理方法,如果你调用invalidateAndCancel方法来使session失效,那么该session会立即调用上面的代理方法。

函数实现:

- (void)URLSession:(NSURLSession *)session
didBecomeInvalidWithError:(NSError *)error
{
// 自定义的一个block,用来处理session无效的情况。
// 此处插一句,刚才突然灵光一现,体验到了block的好处。具体说不清楚,
// 我只能说好处就是此处并不是让用户自己实现didBecomeInvalidWithError:方法,
// 而是让用户实现sessionDidBecomeInvalid这个block,隐藏细节。
// 确实很妙,以后要学会使用block
if (self.sessionDidBecomeInvalid) {
self.sessionDidBecomeInvalid(session, error);
} // 当一个session无效时,post名为AFURLSessionDidInvalidateNotification的Notification
// 不过源代码中没有举例如何使用这个Notification,所以需要用户自己定义,比如结束进度条的显示啊。
[[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDidInvalidateNotification object:session];
}

3.1.2 – URLSession:didReceiveChallenge:completionHandler:

函数声明:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler;

函数作用:

web服务器接收到客户端请求时,有时候需要先验证客户端是否为正常用户,再决定是够返回真实数据。这种情况称之为服务端要求客户端接收挑战(NSURLAuthenticationChallenge *challenge)。接收到挑战后,客户端要根据服务端传来的challenge来生成completionHandler所需的NSURLSessionAuthChallengeDisposition disposition和NSURLCredential *credential(disposition指定应对这个挑战的方法,而credential是客户端生成的挑战证书,注意只有challenge中认证方法为NSURLAuthenticationMethodServerTrust的时候,才需要生成挑战证书)。最后调用completionHandler回应服务器端的挑战。

函数讨论:

该代理方法会在下面两种情况调用:

  1. 1. 当服务器端要求客户端提供证书时或者进行NTLM认证(Windows NT LAN Manager,微软提出的WindowsNT挑战/响应验证机制)时,此方法允许你的app提供正确的挑战证书。
  2. 2. 当某个session使用SSL/TLS协议,第一次和服务器端建立连接的时候,服务器会发送给iOS客户端一个证书,此方法允许你的app验证服务期端的证书链(certificate keychain)

如果你没有实现该方法,该session会调用其NSURLSessionTaskDelegate的代理方法URLSession:task:didReceiveChallenge:completionHandler: 。

函数实现:

- (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 = NSURLSessionAuthChallengeRejectProtectionSpace;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} // 必须调用此方法,完成认证挑战
if (completionHandler) {
completionHandler(disposition, credential);
}
}

3.1.3 – URLSessionDidFinishEventsForBackgroundURLSession:

函数声明:

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session

函数作用:

当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也是安全的。

函数实现:

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
if (self.didFinishEventsForBackgroundURLSession) {
// 意味着background session中的消息已经全部发送出去了,返回到主进程执行自定义的函数
dispatch_async(dispatch_get_main_queue(), ^{
self.didFinishEventsForBackgroundURLSession(session);
});
}
}

3.2 NSURLSessionTaskDelegate

3.2.1 – URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:

函数声明:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler

函数作用:

客户端告知服务器端需要HTTP重定向。

函数讨论:
此方法只会在default session或者ephemeral session中调用,而在background session中,session task会自动重定向。


知识点

对于NSURLSession对象的初始化需要使用NSURLSessionConfiguration,而NSURLSessionConfiguration有三个类工厂方法:

+defaultSessionConfiguration 返回一个标准的 configuration,这个配置实际上与 NSURLConnection网络堆栈(networking stack)是一样的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享NSURLCredentialStorage

+ephemeralSessionConfiguration 返回一个预设配置,这个配置中不会对缓存,Cookie 和证书进行持久性的存储。这对于实现像秘密浏览这种功能来说是很理想的。

+backgroundSessionConfiguration:(NSString *)identifier 的独特之处在于,它会创建一个后台 session。后台 session 不同于常规的,普通的 session,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文。


函数实现:

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
{
NSURLRequest *redirectRequest = request;
// 自定义如何处理重定向请求,注意会生成一个新的request
if (self.taskWillPerformHTTPRedirection) {
redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);
} if (completionHandler) {
completionHandler(redirectRequest);
}
}

3.2.2 – URLSession:task:didReceiveChallenge:completionHandler:

函数声明:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

函数作用:

同NSURLSessionDelegate中的- URLSession:didReceiveChallenge:completionHandler:

函数讨论:

该方法是处理task-level的认证挑战。在NSURLSessionDelegate中提供了一个session-level的认证挑战代理方法。该方法的调用取决于认证挑战的类型:

  • 对于session-level的认证挑战,挑战类型有 — NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, 或NSURLAuthenticationMethodServerTrust — 此时session会调用其代理方法URLSession:didReceiveChallenge:completionHandler:。如果你的app没有提供对应的NSURLSessionDelegate方法,那么NSURLSession对象就会调用URLSession:task:didReceiveChallenge:completionHandler:来处理认证挑战。
  • 对于non-session-level的认证挑战,NSURLSession对象调用URLSession:task:didReceiveChallenge:completionHandler:来处理认证挑战。如果你在app中使用了session代理方法,而且也确实要处理认证挑战这个问题,那么你必须还是在task level来处理这个问题,或者提供一个task-level的handler来显式调用每个session的handler。而对于non-session-level的认证挑战,session的delegate中的URLSession:didReceiveChallenge:completionHandler:方法不会被调用。

函数实现:

参考URLSession:didReceiveChallenge:completionHandler:实现,除了自定义了taskDidReceiveAuthenticationChallenge这个block处理task-level的认证挑战,其他都一样。

3.2.3 – URLSession:task:needNewBodyStream:

函数声明:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler

函数作用:

当一个session task需要发送一个新的request body stream到服务器端的时候,调用该代理方法。

函数讨论:

该代理方法会在下面两种情况被调用:

  • 如果task是由uploadTaskWithStreamedRequest:创建的,那么提供初始的request body stream时候会调用该代理方法。
  • 因为认证挑战或者其他可恢复的服务器错误,而导致需要客户端重新发送一个含有body stream的request,这时候会调用该代理。

函数实现:

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler
{
NSInputStream *inputStream = nil; if (self.taskNeedNewBodyStream) {
// 自定义的获取到新的bodyStream方法
inputStream = self.taskNeedNewBodyStream(session, task);
} else if (task.originalRequest.HTTPBodyStream && [task.originalRequest.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]) {
// 拷贝一份数据出来到新的bodyStream中(即inputStream)
inputStream = [task.originalRequest.HTTPBodyStream copy];
} if (completionHandler) {
completionHandler(inputStream);
}
}

3.2.4 – URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:

函数声明:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend

函数作用:

周期性地通知代理发送到服务器端数据的进度。

函数实现:

- (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];
}
}
// 每次发送数据后的相关自定义处理,比如根据totalBytesSent来进行UI界面的数据上传显示
if (self.taskDidSendBodyData) {
self.taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalUnitCount);
}
}

3.2.5 – URLSession:task:didCompleteWithError:

函数声明:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error

函数作用:

告知该session task已经完成了数据传输任务。

函数讨论:

注意这里的error不会报告服务期端的error,他表示的是客户端这边的eroor,比如无法解析hostname或者连不上host主机。

函数实现:

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
// 这里第一次展示了AFURLSessionManagerTaskDelegate的作用
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task]; // 如果task是在后台完成的,可能delegate会为nil
if (delegate) {
// 调用了一样的代理方法,不过是AFURLSessionManagerTaskDelegate中实现的
[delegate URLSession:session task:task didCompleteWithError:error];
// 该task结束了,就移除对应的delegate
[self removeDelegateForTask:task];
}
// 自定义处理方法
if (self.taskDidComplete) {
self.taskDidComplete(session, task, error);
}
}

3.3 NSURLSessionDataDelegate

3.3.1 – URLSession:dataTask:didReceiveResponse:completionHandler:

函数声明:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

函数作用:

告诉代理,该data task获取到了服务器端传回的最初始回复(response)。注意其中的completionHandler这个block,通过传入一个类型为NSURLSessionResponseDisposition的变量来决定该传输任务接下来该做什么:

  • NSURLSessionResponseAllow 该task正常进行
  • NSURLSessionResponseCancel 该task会被取消
  • NSURLSessionResponseBecomeDownload 会调用URLSession:dataTask:didBecomeDownloadTask:方法来新建一个download task以代替当前的data task

函数讨论:

该方法是可选的,除非你必须支持“multipart/x-mixed-replace”类型的content-type。因为如果你的request中包含了这种类型的content-type,服务器会将数据分片传回来,而且每次传回来的数据会覆盖之前的数据。每次返回新的数据时,session都会调用该函数,你应该在这个函数中合理地处理先前的数据,否则会被新数据覆盖。如果你没有提供该方法的实现,那么session将会继续任务,也就是说会覆盖之前的数据。

函数实现:

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
// 默认方式为继续执行该task
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
// 自定义
if (self.dataTaskDidReceiveResponse) {
disposition = self.dataTaskDidReceiveResponse(session, dataTask, response);
} if (completionHandler) {
completionHandler(disposition);
}
}

3.3.2 – URLSession:dataTask:didBecomeDownloadTask:

函数声明:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask

函数作用:

如果data task变化成了下载任务(download task),那么就会调用该代理方法

函数讨论:

比如在- URLSession:dataTask:didReceiveResponse:completionHandler:给completionHandler方法传递NSURLSessionResponseBecomeDownload,就会使data task变成download task。而且之前的data task不会再响应代理方法了。

函数实现:

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask];
if (delegate) {
// 将delegate关联的data task移除,换成新产生的download task
[self removeDelegateForTask:dataTask];
[self setDelegate:delegate forTask:downloadTask];
}
// 自定义
if (self.dataTaskDidBecomeDownloadTask) {
self.dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask);
}
}

3.3.3 – URLSession:dataTask:didReceiveData:

函数声明:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

函数作用:

当接收到部分期望得到的数据(expected data)时,会调用该代理方法。

函数讨论:

一个NSData类型的数据通常是由一系列不同的数据整合到一起得到的,不管是不是这样,请使用- enumerateByteRangesUsingBlock:来遍历数据然不是使用bytes方法(因为bytes缺少enumerateByteRangesUsingBlock方法中的range,有了range,enumerateByteRangesUsingBlock就可以对NSData不同的数据块进行遍历,而不像bytes把所有NSData看成一个数据块)。

该代理方法可能会调用多次(比如分片获取数据),你需要自己实现函数将所有数据整合在一起。

函数实现:

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask];
// 调用的是AFURLSessionManagerTaskDelegate的didReceiveData方法
[delegate URLSession:session dataTask:dataTask didReceiveData:data];
// 自定义
if (self.dataTaskDidReceiveData) {
self.dataTaskDidReceiveData(session, dataTask, data);
}
}

3.3.4 – URLSession:dataTask:willCacheResponse:completionHandler:

函数声明:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler

函数作用:

询问data task或上传任务(upload task)是否缓存response。

函数讨论:

当task接收到所有期望的数据后,session会调用此代理方法。如果你没有实现该方法,那么就会使用创建session时使用的configuration对象决定缓存策略。这个代理方法最初的目的是为了阻止缓存特定的URLs或者修改NSCacheURLResponse对象相关的userInfo字典。

该方法只会当request决定缓存response时候调用。作为准则,responses只会当以下条件都成立的时候返回缓存:

  • 该request是HTTP或HTTPS URL的请求(或者你自定义的网络协议,并且确保该协议支持缓存)
  • 确保request请求是成功的(返回的status code为200-299)
  • 返回的response是来自服务器端的,而非缓存中本身就有的
  • 提供的NSURLRequest对象的缓存策略要允许进行缓存
  • 服务器返回的response中与缓存相关的header要允许缓存
  • 该response的大小不能比提供的缓存空间大太多(比如你提供了一个磁盘缓存,那么response大小一定不能比磁盘缓存空间还要大5%)

函数实现:

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
NSCachedURLResponse *cachedResponse = proposedResponse;
// 自定义方法,你可以什么都不做,返回原始的cachedResponse,或者使用修改后的cachedResponse
// 当然,你也可以返回NULL,这就意味着不需要缓存Response
if (self.dataTaskWillCacheResponse) {
cachedResponse = self.dataTaskWillCacheResponse(session, dataTask, proposedResponse);
} if (completionHandler) {
completionHandler(cachedResponse);
}
}

3.4 NSURLSessionDownloadDelegate

3.4.1 – URLSession:downloadTask:didFinishDownloadingToURL:(必须实现)

函数声明:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location

函数作用:

告诉代理,该下载任务已完成。

函数实现:

- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:downloadTask];
if (self.downloadTaskDidFinishDownloading) {
// 自定义函数,根据从服务器端获取到的数据临时地址location等参数构建出你想要将临时文件移动的位置
NSURL *fileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location);
// 如果fileURL存在的话,表示用户希望把临时数据存起来
if (fileURL) {
delegate.downloadFileURL = fileURL;
NSError *error = nil;
// 将位于location位置的文件全部移到fileURL位置
[[NSFileManager defaultManager] moveItemAtURL:location toURL:fileURL error:&error];
if (error) {
// 如果移动文件失败,就发送AFURLSessionDownloadTaskDidFailToMoveFileNotification
[[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:error.userInfo];
} return;
}
}
// 这一步比较诡异,感觉有重复的嫌疑。或许是为了兼容以前代码吧
if (delegate) {
[delegate URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:location];
}
}

3.4.2 – URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:

函数声明:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite

函数作用:

周期性地通知下载进度。

函数实现:

// bytesWritten 表示自上次调用该方法后,接收到的数据字节数
// totalBytesWritten 表示目前已经接收到的数据字节数
// totalBytesExpectedToWrite 表示期望收到的文件总字节数,是由Content-Length header提供。如果没有提供,默认是NSURLSessionTransferSizeUnknown
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
// 自定义
if (self.downloadTaskDidWriteData) {
self.downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}
}

3.4.3 – URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:

函数声明:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes

函数作用:

告诉代理,下载任务重新开始下载了。

函数讨论:

如果一个resumable(不是很会翻译)下载任务被取消或者失败了,你可以请求一个resumeData对象(比如在userInfo字典中通过NSURLSessionDownloadTaskResumeData这个键来获取到resumeData)并使用它来提供足够的信息以重新开始下载任务。随后,你可以使用resumeData作为downloadTaskWithResumeData:或downloadTaskWithResumeData:completionHandler:的参数。

当你调用这些方法时,你将开始一个新的下载任务。一旦你继续下载任务,session会调用它的代理方法URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:其中的downloadTask参数表示的就是新的下载任务,这也意味着下载重新开始了。

函数实现:

// fileOffset如果文件缓存策略或者最后文件更新日期阻止重用已经存在的文件内容,那么该值为0。
// 否则,该值表示已经存在磁盘上的,不需要重新获取的数据——— 这是断点续传啊!
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
if (self.downloadTaskDidResume) {
self.downloadTaskDidResume(session, downloadTask, fileOffset, expectedTotalBytes);
}
}

4. 还没结束


这一篇由于篇幅原因,不想继续写下去了。后续内容,包括AFURLSessionManagerTaskDelegate、uploadTask、downloadTask、测试等零散的东西准备放在下一篇进行学习。不过,一切还没结束,AFNetworking源码之路还很长。

5. 参考文章


相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,029
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,519
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,367
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,146
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,781
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,859