【原】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為key,delegate為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之間的代碼)也是分為三個部分:
1. self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate; 2. [delegate setupProgressForTask:task]; 3. [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對應的cancel、pause和resume這三個狀態,正好對應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. 當服務器端要求客戶端提供證書時或者進行NTLM認證(Windows NT LAN Manager,微軟提出的WindowsNT挑戰/響應驗證機制)時,此方法允許你的app提供正確的挑戰證書。
- 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源碼之路還很長。
