通讀AFN②--AFN的上傳和下載功能分析以政、SessionTask及相應(yīng)的session代理方法的使用細(xì)節(jié)

這一部分主要研究AFN的上傳和下載功能囱桨,中間涉及到各種NSURLSessionTask的一些創(chuàng)建的解析和HTTPSessionManager對(duì)RESTful風(fēng)格的web應(yīng)用的支持万哪,同時(shí)會(huì)穿插一點(diǎn)NSURLSession代理方法被調(diào)用的時(shí)機(jī)和對(duì)上傳的數(shù)據(jù)的序列化的步驟。

本文主要講解的是上傳和下載的代碼實(shí)現(xiàn)細(xì)節(jié)脐恩,不會(huì)考慮上傳過程中的安全性問題镐侯。

文件的上傳和下載同時(shí)也包括普通的數(shù)據(jù)請(qǐng)求說說到底都是使用了系統(tǒng)的NSURLSession類創(chuàng)建對(duì)應(yīng)的Task,然后執(zhí)行驶冒,為了更好得理解苟翻,我們先理清一下NSURLSessionTask類以及它的子類、NSURLSessionTaskDelegate協(xié)議和它的子協(xié)議之間的關(guān)系只怎,以及各種代理方法調(diào)用的時(shí)機(jī)袜瞬。

先看一張圖:

其中的調(diào)用是指,task在resume之后會(huì)調(diào)用的Session對(duì)應(yīng)的代理方法聲明在的協(xié)議身堡,例如:

當(dāng)執(zhí)行一個(gè)NSURLSessionDataTask類型的任務(wù)resume之后邓尤,負(fù)責(zé)創(chuàng)建它的session將會(huì)調(diào)用在NSURLSessionDataDelegate中定義的幾個(gè)方法:

- URLSession: dataTask: didReceiveResponse: completionHandler:

- URLSession: dataTask: didBecomeDownloadTask:

- URLSession: dataTask: didBecomeStreamTask:

- URLSession: dataTask: didReceiveData:

- URLSession: dataTask: willCacheResponse: completionHandler:

由于NSURLSessionDataDelegate協(xié)議遵守了NSURLSessionTaskDelegate和NSURLSessionDelegate,所以也會(huì)調(diào)用這樣幾個(gè)方法:

// 在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:

實(shí)際上你無法通過session來創(chuàng)建NSURLSessionTask贴谎,只能創(chuàng)建它的子類來使用汞扎,iOS并沒有提供可以直接創(chuàng)建它的方法:

1.不可能通過alloc init創(chuàng)建 ,因?yàn)閯?chuàng)建之后 無法給request屬性(readonly)賦值擅这,網(wǎng)絡(luò)請(qǐng)求無法進(jìn)行澈魄。

2.NSURLSession、NSURLSession(NSURLSessionAsynchronousConvenience) 沒有提供直接創(chuàng)建的方法仲翎。

或許apple本來就打算將這個(gè)類設(shè)計(jì)為抽象類痹扇,而只能使用繼承它的類。

而只要是使用了session類進(jìn)行創(chuàng)建任何一個(gè)dataTask溯香、uploadTask或者是downloadTask就會(huì)調(diào)用在NSURLSessionDelegate中和NSURLSessionTaskDelegate中聲明的代理方法,這些代理方法大都是進(jìn)行網(wǎng)絡(luò)請(qǐng)求的配置鲫构,少部分涉及到數(shù)據(jù)處理,而子協(xié)議NSURLSessionDataDelegate玫坛、NSURLSessionDownloadDelegate和NSURLSessionSteamDelegate都是具體的數(shù)據(jù)處理方法结笨。

經(jīng)過一些簡(jiǎn)單的測(cè)試:看看一些方法的調(diào)用順序,使用dataTask進(jìn)行一個(gè)普通的網(wǎng)絡(luò)請(qǐng)求:

如果使用的是GET請(qǐng)求,或者使用的是POST請(qǐng)求炕吸、但是HTTPBody沒有數(shù)據(jù)伐憾,主要調(diào)用兩個(gè)代理方法:

- URLSession: dataTask: didReceiveData:// 當(dāng)服務(wù)端有數(shù)據(jù)返回時(shí)調(diào)用,沒有數(shù)據(jù)返回則不調(diào)用- URLSession: task: didCompleteWithError:// 在請(qǐng)求完成之后必調(diào)用

如果是POST請(qǐng)求赫模,并且HTTPBody中帶有數(shù)據(jù)树肃,那么主要調(diào)用以下幾個(gè)方法(實(shí)際上不管創(chuàng)建的任務(wù)是dataTask或是uploadTask都是這樣,畢竟uploadTask是繼承自dataTask的):

- URLSession: task: didSendBodyData: totalBytesSent: totalBytesExpectedToSend:// 當(dāng)HTTPBody中有數(shù)據(jù)時(shí)調(diào)用- URLSession: dataTask: didReceiveData:// 同上- URLSession: task: didCompleteWithError:// 同上

當(dāng)進(jìn)行一些下載操作瀑罗,使用downloadTask的時(shí)候:

- URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:// 間歇性調(diào)用- URLSession: downloadTask: didFinishDownloadingToURL:// 下載完成時(shí)調(diào)用- URLSession: task: didCompleteWithError:// 本次網(wǎng)絡(luò)訪問完成時(shí)調(diào)用 在上面的方法調(diào)用之后調(diào)用

可以發(fā)現(xiàn)扫外,但凡是session進(jìn)行的網(wǎng)絡(luò)請(qǐng)求都會(huì)最終調(diào)用- URLSession: task: didCompleteWithError:,而在在之前調(diào)用的代理方法,會(huì)因request是否攜帶數(shù)據(jù)廓脆,訪問完成的時(shí)候服務(wù)端是否有response的數(shù)據(jù),還有使用的task的類型會(huì)有一些差別磁玉。下面會(huì)針對(duì)uploadTask的使用和downloadTask的使用細(xì)說這些差別停忿,以及介紹一些實(shí)現(xiàn)上傳和下載的具體方案。

第二部分 上傳

使用上傳歸根結(jié)底都會(huì)使用apple的uploadTask蚊伞,翻看AFN的源碼(僅僅是session部分)也都是使用了蘋果的三個(gè)創(chuàng)建uploadTask的方法完成的席赂。

apple的三個(gè)方法都是一個(gè)思路:將要上傳的文件的二進(jìn)制寫入到HTTPBody中,

按照有沒有使用Form可以分為兩類:

1.沒有使用form

- (NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request fromFile:(NSURL*)fileURL;- (NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request fromData:(NSData*)bodyData;

2.使用了form

- (NSURLSessionUploadTask*)uploadTaskWithStreamedRequest:(NSURLRequest*)request;

下面分別介紹一下:

不使用表單的情況

AFN沒有使用html表單直接上傳的方式比較簡(jiǎn)單时迫,實(shí)現(xiàn)上是直接調(diào)用了apple的- uploadTaskWithRequest: fromFile:方法或者- uploadTaskWithRequest: fromData:颅停,關(guān)于蘋果的這兩個(gè)方法,蘋果給出這樣的文檔

創(chuàng)建一個(gè)任務(wù)掠拳,這個(gè)任務(wù)能對(duì)指定的URLRquest對(duì)象執(zhí)行HTTP請(qǐng)求和上傳提供的數(shù)據(jù)癞揉。

對(duì)于request的參數(shù)有一點(diǎn)需要注意的是:它的body stream和body data會(huì)被忽略,只使用fromData參數(shù)提供的數(shù)據(jù)溺欧。對(duì)于request對(duì)象還有一個(gè)要求喊熟,必須是包含了request body,因此HTTP方法可以是POST或者PUT姐刁,另外可以使用HTTP的RequestHeader提供一些上傳的元數(shù)據(jù)芥牌,如文件名字等。其實(shí)這兩個(gè)方法內(nèi)部的實(shí)現(xiàn)中聂使,是將要上傳的數(shù)據(jù)覆蓋寫入到了HTTPBody中壁拉。

AFNURLSessionManager對(duì)上面兩個(gè)蘋果的方法進(jìn)行了再一次的封裝,這個(gè)封裝就是將代理方法的處理交給了AFURLSessionManagerTaskDelegate類柏靶,同時(shí)將傳入的進(jìn)度NSProgress對(duì)象的指針指向了AFURLSessionManagerTaskDelegate對(duì)象的屬性progress弃理,將task處理完成的回調(diào)賦給它的屬性AFURLSessionTaskCompletionHandler。

如果按照這種方式進(jìn)行文件上傳宿礁,可以按照如下方式使用AFN:

// 文件上傳,不使用表單(只能上傳單個(gè)文件),需要服務(wù)端的配合:// 1.服務(wù)端從HTTPBody得到文件內(nèi)容的二進(jìn)制// 2.將二進(jìn)制存入文件中案铺,并命名// 這個(gè)方法內(nèi)部直接使用apple的uploadTaskWithRequest創(chuàng)建任務(wù), uploadTask具體的實(shí)現(xiàn)是:// 將文件的二進(jìn)制寫入到HTTPBody中- (void)uploadFileNoFormWithURLString:(NSString*)urlString fromFile:(NSURL*)fileURL orFromData:(NSData*)bodyData progress:(NSProgress* __autoreleasing *)progress success:(void(^)(idresponseObject))success failure:(void(^)(NSError*error))failure {? ? ? ? AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];NSURL*url = [NSURLURLWithString:urlString];NSMutableURLRequest*request = [NSMutableURLRequestrequestWithURL:url];? ? request.HTTPMethod =@"POST";// 必須要使用POST? 否則會(huì)使用默認(rèn)的GET, 這樣服務(wù)器得到的input和HTTPBody內(nèi)容不同控汉,這是因?yàn)槭褂眠@種方式上傳文件實(shí)際上是將文件的二進(jìn)制寫入到HTTPBody中笔诵。void(^completionBlock)(idresponseObject,NSError*error) = ^(idresponseObject,NSError*error) {if(error) {if(failure) {? ? ? ? ? ? ? ? failure(error);? ? ? ? ? ? }? ? ? ? }else{if(success) {? ? ? ? ? ? ? ? success(responseObject);? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? };// 這里實(shí)際調(diào)用的URLSessionManager的方法,而不是HTTPSessionManager的方法if(fileURL) {? ? ? ? [[manager uploadTaskWithRequest:request fromFile:fileURL progress:progress completionHandler:^(NSURLResponse*response,idresponseObject,NSError*error) {? ? ? ? ? ? completionBlock(responseObject, error);? ? ? ? }] resume];return;? ? }if(bodyData) {? ? ? ? [[manager uploadTaskWithRequest:request fromData:bodyData progress:progress completionHandler:^(NSURLResponse*response,idresponseObject,NSError*error) {? ? ? ? ? ? completionBlock(responseObject, error);? ? ? ? }] resume];? ? }return;}

使用表單的情況

使用表單其實(shí)是對(duì)HTTPBody數(shù)據(jù)格式進(jìn)行改造姑子,類似于html中使用表單控件上傳乎婿,同樣的在底層上也是模擬html表單上傳數(shù)據(jù)的格式。經(jīng)過這樣的模擬之后街佑,服務(wù)端接收到的每個(gè)文件對(duì)應(yīng)到一個(gè)表單域(field)的值谢翎,這樣方便了服務(wù)端的處理和前臺(tái)的html頁(yè)面的統(tǒng)一。

AFN使用這種方式上傳的實(shí)現(xiàn)依靠的是apple的- uploadTaskWithStreamedRequest:這個(gè)方法,對(duì)于這方法沐旨,文檔中有這樣的說明:

用一個(gè)指定的request創(chuàng)建upload task森逮。之前的request的body stream數(shù)據(jù)會(huì)被忽略,如何需要上傳數(shù)據(jù)調(diào)用URLSession:task:needNewBodyStream:方法磁携。也就是說在這個(gè)方法中設(shè)置的request的HTTPBody和HTTPBodyStream會(huì)被忽略褒侧,而真正上傳的數(shù)據(jù)是從代理方法URLSession:task:needNewBodyStream:中取得的。

AFN的做法是:在AFHTTPRequestSerializer對(duì)象的multipartFormRequestWithMethod: URLString: parameters: constructingBodyWithBlock: error:方法中將要上傳的數(shù)據(jù)組裝為NSInputStream對(duì)象(其實(shí)是NSInputStream的子類AFMultipartBodyStream)并設(shè)置為request的HTTPBodyStream屬性谊迄,然后返回這個(gè)request闷供,當(dāng)真正執(zhí)行到URLSession:task:needNewBodyStream:方法時(shí),會(huì)從request中將這個(gè)InputStream取出统诺,然后復(fù)制歪脏,最終傳遞給用來接收它的回調(diào)completionHandler。

- (void)URLSession:(NSURLSession*)session? ? ? ? ? ? ? task:(NSURLSessionTask*)task needNewBodyStream:(void(^)(NSInputStream*bodyStream))completionHandler{NSInputStream*inputStream =nil;if(self.taskNeedNewBodyStream) {? ? ? ? inputStream =self.taskNeedNewBodyStream(session, task);? ? }elseif(task.originalRequest.HTTPBodyStream && [task.originalRequest.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]){? ? ? ? inputStream = [task.originalRequest.HTTPBodyStreamcopy];? ? }if(completionHandler) {? ? ? ? completionHandler(inputStream);? ? }}

這里AFN拼接表單域的方式和html在瀏覽器中的行為一致粮呢,AFN的拼接方式完全按照瀏覽器的方式模擬了這個(gè)過程婿失,主要通過兩個(gè)類來實(shí)現(xiàn),用于拼接的AFStreamingMultipartFormData類和用于Strea轉(zhuǎn)換的AFMultipartBodyStream類啄寡。

1.首先是將parameter參數(shù)轉(zhuǎn)為AFQueryStringPair數(shù)組移怯,并將每個(gè)AFQueryStringPair對(duì)象的元素轉(zhuǎn)為filed和value的二進(jìn)制形式,然后使用AFStreamingMultipartFormData對(duì)象的- appendPartWithFormData: name:方法將它們拼接為下面格式(boundary生成之后的boundary)

--boundaryContent-Disposition: form-data;name="xx";二進(jìn)制data

2.將parameter的傳遞的參數(shù)拼接完成后拼接文件:

利用request中傳遞過來的block繼續(xù)給上面的AFStreamingMultipartFormData兌現(xiàn)追加內(nèi)容:使用-appendPartWithFileURL: name: error:方法拼接文件这难,拼接為如下格式:

--boundaryContent-Disposition: form-data;name="xxx";filename="xxx"Content-Type: xxx/xxx二進(jìn)制data

最后還得加上頭部

Content-Type:multipart/form-data;boundary=生成后的boundary

還有尾部

--生成后的boundary--

這是最終拼接的結(jié)果舟误,實(shí)際上AFN的拼接過程比這個(gè)要復(fù)雜,它并沒有將最終形式的'串'直接拼接出來姻乓,而是將每一個(gè)部分轉(zhuǎn)為一個(gè)AFHTTPBodyPart對(duì)象嵌溢,存儲(chǔ)到AFStreamingMultipartFormData對(duì)象的屬性bodyStream中,bodyStream是一個(gè)AFMultipartBodyStream對(duì)象蹋岩,使用的是的- appendHTTPBodyPart:方法將AFHTTPBodyPart存儲(chǔ)到了它自己的可變數(shù)組屬性HTTPBodyParts中赖草,最后在AFStreamingMultipartFormData對(duì)象的以下方法完成拼接:

- (NSMutableURLRequest*)requestByFinalizingMultipartFormData {if([self.bodyStream isEmpty]) {returnself.request;? ? }// Reset the initial and final boundaries to ensure correct Content-Length[self.bodyStream setInitialAndFinalBoundaries];? ? [self.request setHTTPBodyStream:self.bodyStream];? ? [self.request setValue:[NSStringstringWithFormat:@"multipart/form-data; boundary=%@",self.boundary] forHTTPHeaderField:@"Content-Type"];? ? [self.request setValue:[NSStringstringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];returnself.request;}

可以看到bodyStream在加了頭部和尾部之后賦值給了request,這里最關(guān)鍵的就是AFMultipartBodyStream(bodyStream的類型)已經(jīng)重寫了InputStream的read:maxLength:和getBuffer:length:連個(gè)方法剪个,這樣當(dāng)bodyStream被讀取的時(shí)候會(huì)按照這兩個(gè)方法的實(shí)現(xiàn)秧骑,按照剛才介紹的那種形式將數(shù)據(jù)拼接起來。

介紹完了這些,我們看一下使用這種方案進(jìn)行上傳文件的常用代碼:

// 多文件上傳乎折,使用POST方法绒疗,使用的是表單的方式,需要服務(wù)端的腳本支持// 使用表單上傳骂澄,將文件作為表單的中的一個(gè)field- (void)uploadFileUseFormWithURLString:(NSString*)urlString parameter:(id)parameter constructingBodyWithBlock:(void(^)(id formData))block progress:(NSProgress* __autoreleasing *)progress success:(void(^)(idresponseObject))success failure:(void(^)(NSError*error))failure {? ? ? ? AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];? ? [mgr POST:urlString parameters:parameter constructingBodyWithBlock:block success:^(NSURLSessionDataTask*task,idresponseObject) {if(success) {? ? ? ? ? ? success(responseObject);? ? ? ? }? ? } failure:^(NSURLSessionDataTask*task,NSError*error) {if(failure) {? ? ? ? ? ? failure(error);? ? ? ? }? ? }];}// 也可以使用提前組裝好request的方法- (void)uploadFileUseFormWithStreamedRequest:(NSURLRequest*)urlRequest progress:(NSProgress* __autoreleasing *)progress success:(void(^)(idresponseObject))success failure:(void(^)(NSError*error))failure {? ? AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];? ? [[mgr uploadTaskWithStreamedRequest:urlRequest progress:progress completionHandler:^(NSURLResponse*response,idresponseObject,NSError*error) {if(error) {if(failure) {? ? ? ? ? ? ? ? failure(error);? ? ? ? ? ? }? ? ? ? }else{if(success) {? ? ? ? ? ? ? ? success(responseObject);? ? ? ? ? ? }? ? ? ? }? ? }] resume];}

這里提供了兩種方案吓蘑,底層代碼完全一樣,第一種是在使用時(shí)拼接表單坟冲,第二種是將表單和parameter組裝到request之后直接調(diào)用磨镶,調(diào)用方法如下:

// 對(duì)第一種方式的調(diào)用NSString*uploadURLString =@"http://127.0.0.1/post/upload-multipart.php";NSDictionary*parameter = @{@"username":@"Mike"};NSProgress*progress1 =nil;[selfuploadFileUseFormWithURLString:uploadURLString parameter:parameter constructingBodyWithBlock:^(id formData) {NSString*documentFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject];NSURL*fileURL = [NSURLfileURLWithPath:[documentFolder stringByAppendingPathComponent:@"1.txt"]];? ? [formData appendPartWithFileURL:fileURL name:@"userfile[]"error:NULL];NSURL*fileURL1 = [NSURLfileURLWithPath:[documentFolder stringByAppendingPathComponent:@"2.jpg"]];? ? [formData appendPartWithFileURL:fileURL1 name:@"userfile[]"fileName:@"aaa.jpg"mimeType:@"image/jpeg"error:NULL];} progress:&progress1 success:^(idresponseObject) {NSLog(@"%@", responseObject);} failure:nil];// 對(duì)第二種方式(預(yù)先組裝request)的調(diào)用NSString*documentFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject];NSURL*fileURL1 = [NSURLfileURLWithPath:[documentFolder stringByAppendingPathComponent:@"1.txt"]];NSURL*fileURL2 = [NSURLfileURLWithPath:[documentFolder stringByAppendingPathComponent:@"2.jpg"]];NSMutableURLRequest*request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST"URLString:uploadURLString parameters:parameter constructingBodyWithBlock:^(id formData) {? ? [formData appendPartWithFileURL:fileURL1 name:@"userfile[]"fileName:@"1.txt"mimeType:@"text/plain"error:nil];? ? [formData appendPartWithFileURL:fileURL2 name:@"userfile[]"fileName:@"aaa.jpg"mimeType:@"image/jpeg"error:nil];} error:nil];NSProgress*progress2 =nil;[selfuploadFileUseFormWithStreamedRequest:request progress:&progress2 success:^(idresponseObject) {NSLog(@"%@", responseObject);} failure:nil];

兩種方法的底層實(shí)現(xiàn)和結(jié)果是完全一致的。

針對(duì)RESTful Web應(yīng)用的文件上傳

使用RESTful風(fēng)格的應(yīng)用健提,默認(rèn)對(duì)網(wǎng)絡(luò)資源增琳猫、刪、改私痹、查對(duì)應(yīng)于HTTP的PUT沸移、DELETE、POST侄榴、GET方法。RESTful更多的是強(qiáng)調(diào)服務(wù)端的技術(shù)网沾,而對(duì)于iOS客戶端而言癞蚕,網(wǎng)絡(luò)訪問的代碼變動(dòng)不是特別大,例如針對(duì)文件上傳辉哥,服務(wù)器要具有處理文件上傳的能力桦山,而且不用像html表單那樣針對(duì)特定的頁(yè)面定制具有專一功能的處理腳本,對(duì)于這種需求醋旦,也許webDav是一個(gè)很好的工具恒水,當(dāng)然web服務(wù)端也可能會(huì)針對(duì)各自的語言和平臺(tái)使用各自的文件處理中間件,但服務(wù)端的處理不是本文的重點(diǎn)饲齐,不再多說钉凌。另外,針對(duì)文件上傳服務(wù)端可能要做一些用戶驗(yàn)證捂人,iOS客戶端就要做一些配合了御雕。

以'我要在http://127.0.0.1/uploads這個(gè)url映射的目錄下放置一個(gè)文件名為12345.png的圖片文件'為例,當(dāng)我要進(jìn)行的操作已經(jīng)能用自然語言表達(dá)時(shí)滥搭,便可以以REST的思路來寫代碼了酸纲,那么:

我要使用的HTTP方法是PUT

我要操作的url為http://127.0.0.1/uploads

我會(huì)寫一個(gè)方法將文件名傳入,將文件的URL或者二進(jìn)制傳入,如下:

- (void)uploadFileUseRESTWithURLString:(NSString*)urlString rename:(NSString*)rename fromFile:(NSURL*)fileURL orFromData:(NSData*)bodyData progress:(NSProgress* __autoreleasing *)progress success:(void(^)(idresponseObject))success failure:(void(^)(NSError*error))failure {? ? ? ? AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];NSString*urlStringPath = [urlString stringByAppendingPathComponent:rename];NSURL*url = [NSURLURLWithString:urlStringPath];NSMutableURLRequest*request = [NSMutableURLRequestrequestWithURL:url];? ? ? ? request.HTTPMethod =@"PUT";// 身份驗(yàn)證 BASIC 方式NSString*usernameAndPassword =@"admin:123456";NSData*data = [usernameAndPassword dataUsingEncoding:NSUTF8StringEncoding];NSString*authString = [@"BASIC "stringByAppendingString:[data base64EncodedStringWithOptions:0]];? ? [request setValue:authString forHTTPHeaderField:@"Authorization"];void(^completionBlock)(idresponseObject,NSError*error) = ^(idresponseObject,NSError*error) {if(error) {if(failure) {? ? ? ? ? ? ? ? failure(error);? ? ? ? ? ? }? ? ? ? }else{if(success) {? ? ? ? ? ? ? ? success(responseObject);? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? };if(fileURL) {? ? ? ? [manager uploadTaskWithRequest:request fromFile:fileURL progress:progress completionHandler:^(NSURLResponse*response,idresponseObject,NSError*error) {? ? ? ? ? ? completionBlock(responseObject, error);? ? ? ? }];return;? ? }if(bodyData) {? ? ? ? [manager uploadTaskWithRequest:request fromData:bodyData progress:progress completionHandler:^(NSURLResponse*response,idresponseObject,NSError*error) {? ? ? ? ? ? completionBlock(responseObject, error);? ? ? ? }];? ? }return;}

調(diào)用也是非常的簡(jiǎn)單:

NSString*filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject] stringByAppendingPathComponent:@"1.png"];NSURL*fileURL = [NSURLfileURLWithPath:filePath];NSProgress*progress =nil;[selfuploadFileUseRESTWithURLString:@"http://127.0.0.1/uploads"rename:@"12345.png"fromFile:fileURL orFromData:nilprogress:&progress success:^(idresponseObject) {NSLog(@"%@", responseObject);} failure:nil];

第三部分 下載

AFN使用的下載同樣是調(diào)用了apple提供的方法:

- downloadTaskWithRequest:

- downloadTaskWithURL:

- downloadTaskWithResumeData:

毫無疑問的是使用url最簡(jiǎn)單了瑟匆,畢竟大部分的下載是無需構(gòu)建request的闽坡。AFN使用名字相似的方法對(duì)系統(tǒng)方法進(jìn)行的一層包裝,通過sessionManager統(tǒng)一管理task。

例如完成一個(gè)簡(jiǎn)單的下載任務(wù):

- (void)downloadWithURLString:(NSString*)urlString progress:(NSProgress* __autoreleasing *)progress completionHandler:(void(^)(NSURLResponse*response,NSURL*filePath,NSError*error))completionHandler {? ? AFURLSessionManager *manager = [[AFURLSessionManager alloc] init];NSMutableCharacterSet*mutableCharacterSet = [[NSMutableCharacterSetalloc] init];? ? [mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSetURLHostAllowedCharacterSet]];? ? [mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSetURLPathAllowedCharacterSet]];NSString*escapedURLString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:mutableCharacterSet];NSURL*url = [NSURLURLWithString:escapedURLString];NSURLRequest*request = [NSURLRequestrequestWithURL:url];? ? [[manager downloadTaskWithRequest:request progress:progress destination:^NSURL*(NSURL*targetPath,NSURLResponse*response) {NSHTTPURLResponse*httpResponse = (NSHTTPURLResponse*)response;NSString*fileName = httpResponse.suggestedFilename ?: urlString;NSString*filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject] stringByAppendingPathComponent:fileName];NSURL*destinationURL = [NSURLfileURLWithPath:filePath];returndestinationURL;? ? } completionHandler:^(NSURLResponse*response,NSURL*filePath,NSError*error) {? ? ? ? completionHandler(response, filePath, error);? ? }] resume];}

好吧疾嗅,那段進(jìn)行urlEncode的代碼確實(shí)礙眼外厂。看一下下載的功能非常簡(jiǎn)單宪迟,只要使用幾行代碼就完成了酣衷,這里有一個(gè)調(diào)用并獲取進(jìn)度的示例:

NSString*urlString =@"http://127.0.0.1/static/功夫熊貓.mp4";NSProgress*progress =nil;[selfdownloadWithURLString:urlString progress:&progress completionHandler:^(NSURLResponse*response,NSURL*filePath,NSError*error) {NSLog(@"%@", error);}];[progress addObserver:selfforKeyPath:@"completedUnitCount"options:NSKeyValueObservingOptionNewcontext:nil];}- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {if([object isKindOfClass:[NSProgressclass]]) {NSProgress*p = object;NSLog(@"已完成大小:%lld? 總大小:%lld", p.completedUnitCount, p.totalUnitCount);NSLog(@"進(jìn)度:%0.2f%%", p.fractionCompleted *100);? ? }}

可以利用這種方式來觀察進(jìn)度,它的原理是將外部傳來的NSProgress指針指向AFURLSessionManagerTaskDelegate對(duì)象的progress屬性,AFURLSessionManagerTaskDelegate對(duì)象會(huì)在- URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:方法中改變自身的progress屬性值次泽,這是外部的Progress的值也就改變了穿仪。

也可以使用URLSessionManager的downloadProgressForTask:方法獲取指定task的完成進(jìn)度,不過這要進(jìn)行對(duì)task的統(tǒng)一管理意荤。

在開發(fā)中經(jīng)常會(huì)遇到下載管理啊片,UI要獲取到下載的進(jìn)度,通常有兩種方法:

1.下載器管理進(jìn)度玖像,進(jìn)度改變發(fā)送通知紫谷,監(jiān)聽進(jìn)度改變的UI控件更新(非常消耗性能)

2.UI控件主動(dòng)獲取任務(wù)管理器中的任務(wù),間隔性地獲取進(jìn)度捐寥。

說到NSURLSessionDownloadDelegate中聲明的三個(gè)方法:

- URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:// 在下載完成的時(shí)候調(diào)用- URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:// 在下載的過程中間歇性調(diào)用- URLSession: downloadTask: didResumeAtOffset: expectedTotalBytes:

對(duì)于第三個(gè)方法到底什么時(shí)候調(diào)用笤昨,就不得不說一下使用NSURLSession的- downloadTaskWithResumeData:方法創(chuàng)建downloadTask了。

這個(gè)方法用resumeData創(chuàng)建一個(gè)downloadTask握恳,如果downloadTask不能被成功地恢復(fù), URLSession:task:didCompleteWithError: 會(huì)被調(diào)用瞒窒。

說白了它被用來恢復(fù)已經(jīng)暫停的downloadTask,那么downloadTask如何暫停呢乡洼,畢竟都沒有被暫停何來恢復(fù)崇裁。可以主動(dòng)調(diào)用downloadTask的一個(gè)對(duì)象方法:

- (void)cancelByProducingResumeData:(void(^)(NSData* __nullable resumeData))completionHandler;

這個(gè)方法會(huì)讓downloadTask暫停束昵,它接收一個(gè)回調(diào)拔稳,在任務(wù)暫停之后調(diào)用,一般在這個(gè)回調(diào)內(nèi)部記錄一下恢復(fù)點(diǎn)的數(shù)據(jù)resumeData锹雏,resumeData參數(shù)是將來用來繼續(xù)的參數(shù)巴比。resumeData只是一個(gè)chunk,而不是已經(jīng)下載的全部數(shù)據(jù)礁遵,因此無法通過它實(shí)現(xiàn)斷點(diǎn)續(xù)傳匿辩,只能實(shí)現(xiàn)簡(jiǎn)單的暫停和繼續(xù),并且要保證通過resume創(chuàng)建downloadTask時(shí)使用的session和創(chuàng)建被取消的downloadTask時(shí)使用的session是同一個(gè)榛丢,也就是所謂的session沒有離線 铲球。

這是一些實(shí)現(xiàn)暫停和繼續(xù)的示例代碼:

// 暫停下載- (IBAction)pauseButtonDidClicked:(UIButton*)sender {? ? [self.downloadTask cancelByProducingResumeData:^(NSData*resumeData) {self.resumeData = resumeData;self.downloadTask =nil;? ? }];// 這是一個(gè)異步的方法}// 繼續(xù)- (IBAction)resumeButtonDidClicked:(UIButton*)sender {if(self.resumeData ==nil) {return;? ? }self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];self.resumeData =nil;? ? ? ? [self.downloadTask resume];}

那么以AFN的方式,就得保存manager晰赞,因?yàn)橹灰猰anager沒有改變session就沒有改變稼病,同時(shí)應(yīng)該將下載的任務(wù)添加到一個(gè)任務(wù)管理器中选侨,對(duì)任務(wù)進(jìn)行統(tǒng)一的調(diào)配,這樣就可以使用使用下面的步驟進(jìn)行任務(wù)的暫停和繼續(xù):

1.先取得downloadTask調(diào)用它的cancelByProducingResumeData:將resumeData保存起來

2.需要繼續(xù)的時(shí)候使用創(chuàng)建了上面的dataTask的manager和保存的resumeData進(jìn)行恢復(fù),manager調(diào)用downloadTaskWithResumeData:就可以然走。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末援制,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子芍瑞,更是在濱河造成了極大的恐慌晨仑,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拆檬,死亡現(xiàn)場(chǎng)離奇詭異洪己,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)竟贯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門答捕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屑那,你說我怎么就攤上這事拱镐。” “怎么了持际?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵沃琅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蜘欲,道長(zhǎng)益眉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任芒填,我火速辦了婚禮,結(jié)果婚禮上空繁,老公的妹妹穿的比我還像新娘殿衰。我一直安慰自己,他們只是感情好盛泡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布闷祥。 她就那樣靜靜地躺著,像睡著了一般傲诵。 火紅的嫁衣襯著肌膚如雪凯砍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天拴竹,我揣著相機(jī)與錄音悟衩,去河邊找鬼。 笑死栓拜,一個(gè)胖子當(dāng)著我的面吹牛座泳,可吹牛的內(nèi)容都是我干的惠昔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼挑势,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼镇防!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起潮饱,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤来氧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后香拉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啦扬,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年缕溉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了考传。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡证鸥,死狀恐怖僚楞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情枉层,我是刑警寧澤泉褐,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站鸟蜡,受9級(jí)特大地震影響膜赃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜揉忘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一跳座、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泣矛,春花似錦疲眷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至哗总,卻和暖如春几颜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背讯屈。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工蛋哭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涮母。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓具壮,卻偏偏與公主長(zhǎng)得像准颓,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子棺妓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容