這一部分主要研究AFN的上傳和下載功能,中間涉及到各種NSURLSessionTask的一些創建的解析和HTTPSessionManager對RESTful風格的web應用的支持,同時會穿插一點NSURLSession代理方法被調用的時機和對上傳的數據的序列化的步驟。 本文主要講解的是上傳和下載的代
這一部分主要研究AFN的上傳和下載功能,中間涉及到各種NSURLSessionTask的一些創建的解析和HTTPSessionManager對RESTful風格的web應用的支持,同時會穿插一點NSURLSession代理方法被調用的時機和對上傳的數據的序列化的步驟。 本文主要講解的是上傳和下載的代碼實現細節,不會考慮上傳過程中的安全性問題。 文件的上傳和下載同時也包括普通的數據請求說說到底都是使用了系統的NSURLSession類創建對應的Task,然後執行,為了更好得理解,我們先理清一下NSURLSessionTask類以及它的子類、NSURLSessionTaskDelegate協議和它的子協議之間的關係,以及各種代理方法調用的時機。 先看一張圖: 其中的調用是指,task在resume之後會調用的Session對應的代理方法聲明在的協議,例如: 當執行一個NSURLSessionDataTask類型的任務resume之後,負責創建它的session將會調用在NSURLSessionDataDelegate中定義的幾個方法: ```objectivec - URLSession: dataTask: didReceiveResponse: completionHandler: - URLSession: dataTask: didBecomeDownloadTask: - URLSession: dataTask: didBecomeStreamTask: - URLSession: dataTask: didReceiveData: - URLSession: dataTask: willCacheResponse: completionHandler: ``` 由於NSURLSessionDataDelegate協議遵守了NSURLSessionTaskDelegate和NSURLSessionDelegate,所以也會調用這樣幾個方法: ```objectivec // 在NSURLSessionDelegate中聲明的 - URLSession: didBecomeInvalidWithError: - URLSession: didReceiveChallenge: completionHandler: - URLSessionDidFinishEventsForBackgroundURLSession: // 在NSURLSessionTaskDelegate中聲明的 - URLSession: task: willPerformHTTPRedirection: newRequest: completionHandler: - URLSession: task: didReceiveChallenge: completionHandler: - URLSession: task: needNewBodyStream: - URLSession: task: didSendBodyData: totalBytesSent: totalBytesExpectedToSend: - URLSession: task: didCompleteWithError: ``` 實際上你無法通過session來創建NSURLSessionTask,只能創建它的子類來使用,iOS並沒有提供可以直接創建它的方法: 1.不可能通過alloc init創建 ,因為創建之後 無法給request屬性(readonly)賦值,網路請求無法進行。 2.NSURLSession、NSURLSession(NSURLSessionAsynchronousConvenience) 沒有提供直接創建的方法。 或許apple本來就打算將這個類設計為抽象類,而只能使用繼承它的類。 而只要是使用了session類進行創建任何一個dataTask、uploadTask或者是downloadTask就會調用在NSURLSessionDelegate中和NSURLSessionTaskDelegate中聲明的代理方法,這些代理方法大都是進行網路請求的配置,少部分涉及到數據處理,而子協議NSURLSessionDataDelegate、NSURLSessionDownloadDelegate和NSURLSessionSteamDelegate都是具體的數據處理方法。 經過一些簡單的測試:看看一些方法的調用順序,使用dataTask進行一個普通的網路請求: 如果使用的是GET請求,或者使用的是POST請求、但是HTTPBody沒有數據,主要調用兩個代理方法: ```objectivec - URLSession: dataTask: didReceiveData: // 當服務端有數據返回時調用,沒有數據返回則不調用 - URLSession: task: didCompleteWithError: // 在請求完成之後必調用 ``` 如果是POST請求,並且HTTPBody中帶有數據,那麼主要調用以下幾個方法(實際上不管創建的任務是dataTask或是uploadTask都是這樣,畢竟uploadTask是繼承自dataTask的): ```objectivec - URLSession: task: didSendBodyData: totalBytesSent: totalBytesExpectedToSend: // 當HTTPBody中有數據時調用 - URLSession: dataTask: didReceiveData: // 同上 - URLSession: task: didCompleteWithError: // 同上 ``` 當進行一些下載操作,使用downloadTask的時候: ```objectivec - URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite: // 間歇性調用 - URLSession: downloadTask: didFinishDownloadingToURL: // 下載完成時調用 - URLSession: task: didCompleteWithError: // 本次網路訪問完成時調用 在上面的方法調用之後調用 ``` 可以發現,但凡是session進行的網路請求都會最終調用`- URLSession: task: didCompleteWithError:`,而在在之前調用的代理方法,會因request是否攜帶數據,訪問完成的時候服務端是否有response的數據,還有使用的task的類型會有一些差別。下麵會針對uploadTask的使用和downloadTask的使用細說這些差別,以及介紹一些實現上傳和下載的具體方案。 ### 第二部分 上傳 使用上傳歸根結底都會使用apple的uploadTask,翻看AFN的源碼(僅僅是session部分)也都是使用了蘋果的三個創建uploadTask的方法完成的。 apple的三個方法都是一個思路:將要上傳的文件的二進位寫入到HTTPBody中, 按照有沒有使用Form可以分為兩類: 1.沒有使用form ```objectivec - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL; - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData; ``` 2.使用了form ```objectivec - (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request; ``` 下麵分別介紹一下: #### 不使用表單的情況 AFN沒有使用html表單直接上傳的方式比較簡單,實現上是直接調用了apple的`- uploadTaskWithRequest: fromFile:`方法或者`- uploadTaskWithRequest: fromData:`,關於蘋果的這兩個方法,蘋果給出這樣的文檔 創建一個任務,這個任務能對指定的URLRquest對象執行HTTP請求和上傳提供的數據。 對於request的參數有一點需要註意的是:它的body stream和body data會被忽略,只使用fromData參數提供的數據。對於request對象還有一個要求,必須是包含了request body,因此HTTP方法可以是POST或者PUT,另外可以使用HTTP的RequestHeader提供一些上傳的元數據,如文件名字等。其實這兩個方法內部的實現中,是將要上傳的數據覆蓋寫入到了HTTPBody中。 AFNURLSessionManager對上面兩個蘋果的方法進行了再一次的封裝,這個封裝就是將代理方法的處理交給了AFURLSessionManagerTaskDelegate類,同時將傳入的進度NSProgress對象的指針指向了AFURLSessionManagerTaskDelegate對象的屬性progress,將task處理完成的回調賦給它的屬性AFURLSessionTaskCompletionHandler。 如果按照這種方式進行文件上傳,可以按照如下方式使用AFN: ```objectivec // 文件上傳,不使用表單(只能上傳單個文件),需要服務端的配合: // 1.服務端從HTTPBody得到文件內容的二進位 // 2.將二進位存入文件中,並命名 // 這個方法內部直接使用apple的uploadTaskWithRequest創建任務, uploadTask具體的實現是: // 將文件的二進位寫入到HTTPBody中 - (void)uploadFileNoFormWithURLString:(NSString *)urlString fromFile:(NSURL *)fileURL orFromData:(NSData *)bodyData progress:(NSProgress * __autoreleasing *)progress success:(void(^)(id responseObject))success failure:(void(^)(NSError *error))failure { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; NSURL *url = [NSURL URLWithString:urlString]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; // 必須要使用POST 否則會使用預設的GET, 這樣伺服器得到的input和HTTPBody內容不同,這是因為使用這種方式上傳文件實際上是將文件的二進位寫入到HTTPBody中。 void (^completionBlock)(id responseObject, NSError *error) = ^(id responseObject, NSError *error) { if (error) { if (failure) { failure(error); } } else { if (success) { success(responseObject); } } }; // 這裡實際調用的URLSessionManager的方法,而不是HTTPSessionManager的方法 if (fileURL) { [[manager uploadTaskWithRequest:request fromFile:fileURL progress:progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { completionBlock(responseObject, error); }] resume]; return; } if (bodyData) { [[manager uploadTaskWithRequest:request fromData:bodyData progress:progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { completionBlock(responseObject, error); }] resume]; } return; } ``` #### 使用表單的情況 使用表單其實是對HTTPBody數據格式進行改造,類似於html中使用表單控制項上傳,同樣的在底層上也是模擬html表單上傳數據的格式。經過這樣的模擬之後,服務端接收到的每個文件對應到一個表單域(field)的值,這樣`方便了服務端的處理和前臺的html頁面的統一`。 AFN使用這種方式上傳的實現依靠的是apple的`- uploadTaskWithStreamedRequest:`這個方法,對於這方法,文檔中有這樣的說明: 用一個指定的request創建upload task。之前的request的body stream數據會被忽略,如何需要上傳數據調用URLSession:task:needNewBodyStream:方法。也就是說在這個方法中設置的request的HTTPBody和HTTPBodyStream會被忽略,而真正上傳的數據是從代理方法`URLSession:task:needNewBodyStream:`中取得的。 AFN的做法是:在AFHTTPRequestSerializer對象的`multipartFormRequestWithMethod: URLString: parameters: constructingBodyWithBlock: error:`方法中將要上傳的數據組裝為NSInputStream對象(其實是NSInputStream的子類AFMultipartBodyStream)並設置為request的HTTPBodyStream屬性,然後返回這個request,當真正執行到`URLSession:task:needNewBodyStream:`方法時,會從request中將這個InputStream取出,然後複製,最終傳遞給用來接收它的回調completionHandler。 ```objectivec - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler { NSInputStream *inputStream = nil; 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); } } ``` 這裡AFN拼接表單域的方式和html在瀏覽器中的行為一致,AFN的拼接方式完全按照瀏覽器的方式模擬了這個過程,主要通過兩個類來實現,用於拼接的AFStreamingMultipartFormData類和用於Strea轉換的AFMultipartBodyStream類。 1.首先是將parameter參數轉為AFQueryStringPair數組,並將每個AFQueryStringPair對象的元素轉為filed和value的二進位形式,然後使用AFStreamingMultipartFormData對象的`- appendPartWithFormData: name:`方法將它們拼接為下麵格式(boundary生成之後的boundary) ```sh --boundary Content-Disposition: form-data; name="xx"; 二進位data ``` 2.將parameter的傳遞的參數拼接完成後拼接文件: 利用request中傳遞過來的block繼續給上面的AFStreamingMultipartFormData兌現追加內容:使用`-appendPartWithFileURL: name: error:`方法拼接文件,拼接為如下格式: ```sh --boundary Content-Disposition: form-data; name="xxx"; filename="xxx" Content-Type: xxx/xxx 二進位data ``` 最後還得加上頭部 ```sh Content-Type:multipart/form-data; boundary=生成後的boundary ``` 還有尾部 ```sh --生成後的boundary-- ``` 這是最終拼接的結果,實際上AFN的拼接過程比這個要複雜,它並沒有將最終形式的'串'直接拼接出來,而是將每一個部分轉為一個AFHTTPBodyPart對象,存儲到AFStreamingMultipartFormData對象的屬性bodyStream中,bodyStream是一個AFMultipartBodyStream對象,使用的是的`- appendHTTPBodyPart:`方法將AFHTTPBodyPart存儲到了它自己的可變數組屬性HTTPBodyParts中,最後在AFStreamingMultipartFormData對象的以下方法完成拼接: ```objectivec - (NSMutableURLRequest *)requestByFinalizingMultipartFormData { if ([self.bodyStream isEmpty]) { return self.request; } // Reset the initial and final boundaries to ensure correct Content-Length [self.bodyStream setInitialAndFinalBoundaries]; [self.request setHTTPBodyStream:self.bodyStream]; [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"]; [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"]; return self.request; } ``` 可以看到bodyStream在加了頭部和尾部之後賦值給了request,這裡最關鍵的就是AFMultipartBodyStream(bodyStream的類型)已經重寫了InputStream的`read:maxLength:`和`getBuffer:length:`連個方法,這樣當bodyStream被讀取的時候會按照這兩個方法的實現,按照剛纔介紹的那種形式將數據拼接起來。 介紹完了這些,我們看一下使用這種方案進行上傳文件的常用代碼: ```objectivec // 多文件上傳,使用POST方法,使用的是表單的方式,需要服務端的腳本支持 // 使用表單上傳,將文件作為表單的中的一個field - (void)uploadFileUseFormWithURLString:(NSString *)urlString parameter:(id)parameter constructingBodyWithBlock:(void (^)(id