NSURLSession
NSURLSession在iOS7中推出,NSURLSession的推出旨在替換之前的NSURLConnection,NSURLSession的使用相對(duì)于之前的NSURLConnection更簡(jiǎn)單,而且不用處理Runloop相關(guān)的東西。
2015年RFC 7540標(biāo)準(zhǔn)發(fā)布了http 2.0版本,http 2.0版本中包含很多新的特性,在傳輸速度上也有很明顯的提升。NSURLSession從iOS9.0開始,對(duì)http 2.0提供了支持。
NSURLSession部分構(gòu)成:
NSURLSession:請(qǐng)求會(huì)話對(duì)象,可以用系統(tǒng)提供的單例對(duì)象,也可以自己創(chuàng)建。
NSURLSessionConfiguration:對(duì)session會(huì)話進(jìn)行配置,一般都采用default。
NSURLSessionTask:負(fù)責(zé)執(zhí)行具體請(qǐng)求的task,由session創(chuàng)建。
NSURLSession有三種方式創(chuàng)建:
sharedSession
系統(tǒng)維護(hù)的一個(gè)單例對(duì)象,可以和其他使用這個(gè)session的task共享連接和請(qǐng)求信息。
sessionWithConfiguration:
在NSURLSession初始化時(shí)傳入一個(gè)NSURLSessionConfiguration,這樣可以自定義請(qǐng)求頭、cookie等信息。
sessionWithConfiguration:delegate:delegateQueue:
如果想更好的控制請(qǐng)求過(guò)程以及回調(diào)線程,需要上面的方法進(jìn)行初始化操作,并傳入delegate來(lái)設(shè)置回調(diào)對(duì)象和回調(diào)的線程。
通過(guò)NSURLSession發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求也比較簡(jiǎn)單。
創(chuàng)建一個(gè)NSURLSessionConfiguration配置請(qǐng)求。
通過(guò)Configuration創(chuàng)建NSURLSession對(duì)象。
通過(guò)session對(duì)象發(fā)起網(wǎng)絡(luò)請(qǐng)求,并獲取task對(duì)象。
調(diào)用[task resume]方法發(fā)起網(wǎng)絡(luò)請(qǐng)求。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession*session?=?[NSURLSessionsessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueuemainQueue]];
NSURLSessionDataTask*task?=?[session?dataTaskWithURL:[NSURLURLWithString:@"http://www.baidu.com"]];
[task?resume];
NSURLSessionTask
通過(guò)NSURLSession發(fā)起的每個(gè)請(qǐng)求,都會(huì)被封裝為一個(gè)NSURLSessionTask任務(wù),但一般不會(huì)直接是NSURLSessionTask類,而是基于不同任務(wù)類型,被封裝為其對(duì)應(yīng)的子類。
NSURLSessionDataTask:處理普通的Get、Post請(qǐng)求。
NSURLSessionUploadTask:處理上傳請(qǐng)求,可以傳入對(duì)應(yīng)的上傳文件或路徑。
NSURLSessionDownloadTask:處理下載地址,提供斷點(diǎn)續(xù)傳功能的cancel方法。
主要方法都定義在父類NSURLSessionTask中,下面是一些關(guān)鍵方法或?qū)傩浴?/p>
currentRequest
當(dāng)前正在執(zhí)行的任務(wù),一般和originalRequest是一樣的,除非發(fā)生重定向才會(huì)有所區(qū)別。
originalRequest
主要用于重定向操作,用來(lái)記錄重定向前的請(qǐng)求。
taskIdentifier
當(dāng)前session下,task的唯一標(biāo)示,多個(gè)session之間可能存在相同的標(biāo)識(shí)。
priority
task中可以設(shè)置優(yōu)先級(jí),但這個(gè)屬性并不代表請(qǐng)求的優(yōu)先級(jí),而是一個(gè)標(biāo)示。官方已經(jīng)說(shuō)明,NSURLSession并沒(méi)有提供API可以改變請(qǐng)求的優(yōu)先級(jí)。
state
當(dāng)前任務(wù)的狀態(tài),可以通過(guò)KVO的方式監(jiān)聽狀態(tài)的改變。
- resume
開始或繼續(xù)請(qǐng)求,創(chuàng)建后的task默認(rèn)是掛起的,需要手動(dòng)調(diào)用resume才可以開始請(qǐng)求。
- suspend
掛起當(dāng)前請(qǐng)求。主要是下載請(qǐng)求用的多一些,普通請(qǐng)求掛起后都會(huì)重新開始請(qǐng)求。下載請(qǐng)求掛起后,只要不超過(guò)NSURLRequest設(shè)置的timeout時(shí)間,調(diào)用resume就是繼續(xù)請(qǐng)求。
- cancel
取消當(dāng)前請(qǐng)求。任務(wù)會(huì)被標(biāo)記為取消,并在未來(lái)某個(gè)時(shí)間調(diào)用URLSession:task:didCompleteWithError:方法。
NSURLSession提供有普通創(chuàng)建task的方式,創(chuàng)建后可以通過(guò)重寫代理方法,獲取對(duì)應(yīng)的回調(diào)和參數(shù)。這種方式對(duì)于請(qǐng)求過(guò)程比較好控制。
-?(NSURLSessionDataTask*)dataTaskWithRequest:(NSURLRequest*)request;
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromFile:(NSURL*)fileURL;
-?(NSURLSessionDownloadTask*)downloadTaskWithRequest:(NSURLRequest*)request;
除此之外,NSURLSession也提供了block的方式創(chuàng)建task,創(chuàng)建方式簡(jiǎn)單如AFN,直接傳入U(xiǎn)RL或NSURLRequest,即可直接在block中接收返回?cái)?shù)據(jù)。和普通創(chuàng)建方式一樣,block的創(chuàng)建方式創(chuàng)建后默認(rèn)也是suspend的狀態(tài),需要調(diào)用resume開始任務(wù)。
completionHandler和delegate是互斥的,completionHandler的優(yōu)先級(jí)大于delegate。相對(duì)于普通創(chuàng)建方法,block方式更偏向于面向結(jié)果的創(chuàng)建,可以直接在completionHandler中獲取返回結(jié)果,但不能控制請(qǐng)求過(guò)程。
-?(NSURLSessionDataTask*)dataTaskWithURL:(NSURL*)url?completionHandler:(void(^)(NSData*?_Nullable?data,NSURLResponse*?_Nullable?response,NSError*?_Nullable?error))completionHandler;
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromData:(nullableNSData*)bodyData?completionHandler:(void(^)(NSData*?_Nullable?data,NSURLResponse*?_Nullable?response,NSError*?_Nullable?error))completionHandler;
-?(NSURLSessionDownloadTask*)downloadTaskWithURL:(NSURL*)url?completionHandler:(void(^)(NSURL*?_Nullable?location,NSURLResponse*?_Nullable?response,NSError*?_Nullable?error))completionHandler;
可以通過(guò)下面的兩個(gè)方法,獲取當(dāng)前session對(duì)應(yīng)的所有task,方法區(qū)別在于回調(diào)的參數(shù)不同。以getTasksWithCompletionHandler為例,在AFN中的應(yīng)用是用來(lái)獲取當(dāng)前session的task,并將AFURLSessionManagerTaskDelegate的回調(diào)都置為nil,以防止崩潰。
-?(void)getTasksWithCompletionHandler:(void(^)(NSArray?*dataTasks,NSArray?*uploadTasks,NSArray?*downloadTasks))completionHandler;
-?(void)getAllTasksWithCompletionHandler:(void(^)(NSArray<__kindofNSURLSessionTask*>?*tasks))completionHandler);
delegateQueue
在初始化NSURLSession時(shí)可以指定線程,如果不指定線程,則completionHandler和delegate的回調(diào)方法,都會(huì)在子線程中執(zhí)行。
如果初始化NSURLSession時(shí)指定了delegateQueue,則回調(diào)會(huì)在指定的隊(duì)列中執(zhí)行,如果指定的是mainQueue,則回調(diào)在主線程中執(zhí)行,這樣就避免了切換線程的問(wèn)題。
[NSURLSessionsessionWithConfiguration:config?delegate:selfdelegateQueue:nil];
delegate
對(duì)于NSURLSession的代理方法這里就不詳細(xì)列舉了,方法命名遵循蘋果一貫見名知意的原則,用起來(lái)很簡(jiǎn)單。這里介紹一下NSURLSession的代理繼承結(jié)構(gòu)。
NSURLSession中定義了一系列代理,并遵循上面的繼承關(guān)系。根據(jù)繼承關(guān)系和代理方法的聲明,如果執(zhí)行某項(xiàng)任務(wù),只需要遵守其中的某個(gè)代理即可。
例如執(zhí)行上傳或普通Post請(qǐng)求,則遵守NSURLSessionDataDelegate,執(zhí)行下載任務(wù)則遵循NSURLSessionDownloadDelegate,父級(jí)代理定義的都是公共方法。
請(qǐng)求重定向
HTTP協(xié)議中定義了例如301等重定向狀態(tài)碼,通過(guò)下面的代理方法,可以處理重定向任務(wù)。發(fā)生重定向時(shí)可以根據(jù)response創(chuàng)建一個(gè)新的request,也可以直接用系統(tǒng)生成的request,并在completionHandler回調(diào)中傳入,如果想終止這次重定向,在completionHandler傳入nil即可。
-?(void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
willPerformHTTPRedirection:(NSHTTPURLResponse*)response
newRequest:(NSURLRequest*)request
completionHandler:(void(^)(NSURLRequest*))completionHandler
{
NSURLRequest*redirectRequest?=?request;
if(self.taskWillPerformHTTPRedirection)?{
redirectRequest?=self.taskWillPerformHTTPRedirection(session,?task,?response,?request);
}
if(completionHandler)?{
completionHandler(redirectRequest);
}
}
NSURLSessionConfiguration
創(chuàng)建方式
NSURLSessionConfiguration負(fù)責(zé)對(duì)NSURLSession初始化時(shí)進(jìn)行配置,通過(guò)NSURLSessionConfiguration可以設(shè)置請(qǐng)求的Cookie、密鑰、緩存、請(qǐng)求頭等參數(shù),將網(wǎng)絡(luò)請(qǐng)求的一些配置參數(shù)從NSURLSession中分離出來(lái)。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession*session?=?[NSURLSessionsessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueuemainQueue]];
NSURLSessionConfiguration提供三種初始化方法,下面是請(qǐng)求的方法的一些解釋。
@property(class,readonly,strong)NSURLSessionConfiguration*defaultSessionConfiguration;
NSURLSessionConfiguration提供defaultSessionConfiguration的方式創(chuàng)建,但這并不是單例方法,而是類方法,創(chuàng)建的是不同對(duì)象。通過(guò)這種方式創(chuàng)建的configuration,并不會(huì)共享cookie、cache、密鑰等,而是不同configuration都需要單獨(dú)設(shè)置。
這塊網(wǎng)上很多人理解都是錯(cuò)的,并沒(méi)有真的在項(xiàng)目里使用或者沒(méi)有留意過(guò),如和其他人有出入,以我為準(zhǔn)。
@property(class,readonly,strong)NSURLSessionConfiguration*ephemeralSessionConfiguration;
創(chuàng)建臨時(shí)的configuration,通過(guò)這種方式創(chuàng)建的對(duì)象,和普通的對(duì)象主要區(qū)別在于URLCache、URLCredentialStorage、HTTPCookieStorage上面。同樣的,Ephemeral也不是單例方法,而只是類方法。
URLCredentialStorage
Ephemeral<__NSCFMemoryURLCredentialStorage:0x600001bc8320>
HTTPCookieStorage
Ephemeral
如果對(duì)Ephemeral方式創(chuàng)建的config進(jìn)行打印的話,可以看到變量類型明顯區(qū)別于其他類型,并且在打印信息前面會(huì)有Ephemeral的標(biāo)示。通過(guò)Ephemeral的方式創(chuàng)建的config,不會(huì)產(chǎn)生持久化信息,可以很好保護(hù)請(qǐng)求的數(shù)據(jù)安全性。
+?(NSURLSessionConfiguration*)backgroundSessionConfigurationWithIdentifier:(NSString*)identifier;
identifier方式一般用于恢復(fù)之前的任務(wù),主要用于下載。如果一個(gè)下載任務(wù)正在進(jìn)行中,程序被kill調(diào),可以在程序退出之前保存identifier。下次進(jìn)入程序后通過(guò)identifier恢復(fù)之前的任務(wù),系統(tǒng)會(huì)將NSURLSession及NSURLSessionConfiguration和之前的下載任務(wù)進(jìn)行關(guān)聯(lián),并繼續(xù)之前的任務(wù)。
timeout
timeoutIntervalForRequest
設(shè)置session請(qǐng)求間的超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間并不是請(qǐng)求從開始到結(jié)束的時(shí)間,而是兩個(gè)數(shù)據(jù)包之間的時(shí)間間隔。當(dāng)任意請(qǐng)求返回后這個(gè)值將會(huì)被重置,如果在超時(shí)時(shí)間內(nèi)未返回則超時(shí)。單位為秒,默認(rèn)為60秒。
timeoutIntervalForResource
資源超時(shí)時(shí)間,一般用于上傳或下載任務(wù),在上傳或下載任務(wù)開始后計(jì)時(shí),如果到達(dá)時(shí)間任務(wù)未結(jié)束,則刪除資源文件。單位為秒,默認(rèn)時(shí)間是七天。
資源共享
如果是相同的NSURLSessionConfiguration對(duì)象,會(huì)共享請(qǐng)求頭、緩存、cookie、Credential,通過(guò)Configuration創(chuàng)建的NSURLSession,也會(huì)擁有對(duì)應(yīng)的請(qǐng)求信息。
@property(nullable,copy)NSDictionary*HTTPAdditionalHeaders;
公共請(qǐng)求頭,默認(rèn)是空的,設(shè)置后所有經(jīng)Confuguration配置的NSURLSession,請(qǐng)求頭都會(huì)帶有設(shè)置的信息。
@property(nullable,retain)NSHTTPCookieStorage*HTTPCookieStorage;
HTTP請(qǐng)求的Cookie管理器。如果是通過(guò)sharedSession或backgroundConfiguration創(chuàng)建的NSURLSession,默認(rèn)使用sharedHTTPCookieStorage的Cookie數(shù)據(jù)。如果不想使用Cookie,則直接設(shè)置為nil即可,也可以手動(dòng)設(shè)置為自己的CookieStorage。
@property(nullable,retain)NSURLCredentialStorage*URLCredentialStorage;
證書管理器。如果是通過(guò)sharedSession或backgroundConfiguration創(chuàng)建的NSURLSession,默認(rèn)使用sharedCredentialStorage的證書。如果不想使用證書,可以直接設(shè)置為nil,也可以自己創(chuàng)建證書管理器。
@property(nullable,retain)NSURLCache*URLCache;
請(qǐng)求緩存,如果不手動(dòng)設(shè)置的話為nil,對(duì)于NSURLCache這個(gè)類我沒(méi)有研究過(guò),不太了解。
緩存處理
在NSURLRequest中可以設(shè)置cachePolicy請(qǐng)求緩存策略,這里不對(duì)具體值做詳細(xì)描述,默認(rèn)值為NSURLRequestUseProtocolCachePolicy使用緩存。
NSURLSessionConfiguration可以設(shè)置處理緩存的對(duì)象,我們可以手動(dòng)設(shè)置自定義的緩存對(duì)象,如果不設(shè)置的話,默認(rèn)使用系統(tǒng)的sharedURLCache單例緩存對(duì)象。經(jīng)過(guò)configuration創(chuàng)建的NSURLSession發(fā)出的請(qǐng)求,NSURLRequest都會(huì)使用這個(gè)NSURLCache來(lái)處理緩存。
@property(nullable,retain)NSURLCache*URLCache;
NSURLCache提供了Memory和Disk的緩存,在創(chuàng)建時(shí)需要為其分別指定Memory和Disk的大小,以及存儲(chǔ)的文件位置。使用NSURLCache不用考慮磁盤空間不夠,或手動(dòng)管理內(nèi)存空間的問(wèn)題,如果發(fā)生內(nèi)存警告系統(tǒng)會(huì)自動(dòng)清理內(nèi)存空間。但是NSURLCache提供的功能非常有限,項(xiàng)目中一般很少直接使用它來(lái)處理緩存數(shù)據(jù),還是用數(shù)據(jù)庫(kù)比較多。
[[NSURLCache?alloc]?initWithMemoryCapacity:30?*?1024?*?1024
diskCapacity:30?*?1024?*?1024
directoryURL:[NSURL?URLWithString:filePath]]
;
使用NSURLCache還有一個(gè)好處,就是可以由服務(wù)端來(lái)設(shè)置資源過(guò)期時(shí)間,在請(qǐng)求服務(wù)端后,服務(wù)端會(huì)返回Cache-Control來(lái)說(shuō)明文件的過(guò)期時(shí)間。NSURLCache會(huì)根據(jù)NSURLResponse來(lái)自動(dòng)完成過(guò)期時(shí)間的設(shè)置。
最大連接數(shù)
限制NSURLSession的最大連接數(shù),通過(guò)此方法創(chuàng)建的NSURLSession和服務(wù)端的最大連接數(shù)量不會(huì)超出這里設(shè)置的數(shù)量。蘋果為我們?cè)O(shè)置的iOS端默認(rèn)為4,Mac端默認(rèn)為6。
@propertyNSInteger?HTTPMaximumConnectionsPerHost;
連接復(fù)用
HTTP是基于傳輸層協(xié)議TCP的,通過(guò)TCP發(fā)送網(wǎng)絡(luò)請(qǐng)求都需要先進(jìn)行三次握手,建立網(wǎng)絡(luò)請(qǐng)求后再發(fā)送數(shù)據(jù),請(qǐng)求結(jié)束時(shí)再經(jīng)歷四次揮手。HTTP1.0開始支持keep-alive,keep-alive可以保持已經(jīng)建立的鏈接,如果是相同的域名,在請(qǐng)求連接建立后,后面的請(qǐng)求不會(huì)立刻斷開,而是復(fù)用現(xiàn)有的連接。從HTTP1.1開始默認(rèn)開啟keep-alive。
請(qǐng)求是在請(qǐng)求頭中設(shè)置下面的參數(shù),服務(wù)器如果支持keep-alive的話,響應(yīng)客戶端請(qǐng)求時(shí),也會(huì)在響應(yīng)頭中加上相同的字段。
Connection:?Keep-Alive
如果想斷開keep-alive,可以在請(qǐng)求頭中加上下面的字段,但一般不推薦這么做。
Connection:?Close
如果通過(guò)NSURLSession來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求的話,需要使用同一個(gè)NSURLSession對(duì)象,如果創(chuàng)建新的session對(duì)象則不能復(fù)用之前的鏈接。keep-alive可以保持請(qǐng)求的連接,蘋果允許在iOS上最大保持有4個(gè)連接,Mac則是6個(gè)連接。
pipeline
在HTTP1.1中,基于keep-alive,還可以將請(qǐng)求進(jìn)行管線化。和相同后端服務(wù),TCP層建立的鏈接,一般都需要前一個(gè)請(qǐng)求返回后,后面的請(qǐng)求再發(fā)出。但pipeline就可以不依賴之前請(qǐng)求的響應(yīng),而發(fā)出后面的請(qǐng)求。
pipeline依賴客戶端和服務(wù)器都有實(shí)現(xiàn),服務(wù)端收到客戶端的請(qǐng)求后,要按照先進(jìn)先出的順序進(jìn)行任務(wù)處理和響應(yīng)。pipeline依然存在之前非pipeline的問(wèn)題,就是前面的請(qǐng)求如果出現(xiàn)問(wèn)題,會(huì)阻塞當(dāng)前連接影響后面的請(qǐng)求。
pipeline對(duì)于請(qǐng)求大文件并沒(méi)有提升作用,只是對(duì)于普通請(qǐng)求速度有提升。在NSURLSessionConfiguration中可以設(shè)置HTTPShouldUsePipelining為YES,開啟管線化,此屬性默認(rèn)為NO。
NSURLSessionTaskMetrics
在日常開發(fā)過(guò)程中,經(jīng)常遇到頁(yè)面加載太慢的問(wèn)題,這很大一部分原因都是因?yàn)榫W(wǎng)絡(luò)導(dǎo)致的。所以,查找網(wǎng)絡(luò)耗時(shí)的原因并解決,就是一個(gè)很重要的任務(wù)了。蘋果對(duì)于網(wǎng)絡(luò)檢查提供了NSURLSessionTaskMetrics類來(lái)進(jìn)行檢查,NSURLSessionTaskMetrics是對(duì)應(yīng)NSURLSessionTaskDelegate的,每個(gè)task結(jié)束時(shí)都會(huì)回調(diào)下面的方法,并且可以獲得一個(gè)metrics對(duì)象。
-?(void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics;
NSURLSessionTaskMetrics可以很好的幫助我們分析網(wǎng)絡(luò)請(qǐng)求的過(guò)程,以找到耗時(shí)原因。除了這個(gè)類之外,NSURLSessionTaskTransactionMetrics類中承載了更詳細(xì)的數(shù)據(jù)。
@property(copy,readonly)NSArray?*transactionMetrics;
transactionMetrics數(shù)組中每一個(gè)元素都對(duì)應(yīng)著當(dāng)前task的一個(gè)請(qǐng)求,一般數(shù)組中只會(huì)有一個(gè)元素,如果發(fā)生重定向等情況,可能會(huì)存在多個(gè)元素。
@property(copy,readonly)NSDateInterval*taskInterval;
taskInterval記錄了當(dāng)前task從開始請(qǐng)求到最后完成的總耗時(shí),NSDateInterval中包含了startDate、endDate和duration耗時(shí)時(shí)間。
@property(assign,readonly)NSUIntegerredirectCount;
redirectCount記錄了重定向次數(shù),在進(jìn)行下載請(qǐng)求時(shí)一般都會(huì)進(jìn)行重定向,來(lái)保證下載任務(wù)能由后端最合適的節(jié)點(diǎn)來(lái)處理。
NSURLSessionTaskTransactionMetrics
NSURLSessionTaskTransactionMetrics中的屬性都是用來(lái)做統(tǒng)計(jì)的,功能都是記錄某個(gè)值,并沒(méi)有邏輯上的意義。所以這里就對(duì)一些主要的屬性做一下解釋,基本涵蓋了大部分屬性,其他就不管了。
這張圖是我從網(wǎng)上扒下來(lái)的,標(biāo)示了NSURLSessionTaskTransactionMetrics的屬性在請(qǐng)求過(guò)程中處于什么位置。
//?請(qǐng)求對(duì)象
@property(copy,readonly)NSURLRequest*request;
//?響應(yīng)對(duì)象,請(qǐng)求失敗可能會(huì)為nil
@property(nullable,copy,readonly)NSURLResponse*response;
//?請(qǐng)求開始時(shí)間
@property(nullable,copy,readonly)NSDate*fetchStartDate;
//?DNS解析開始時(shí)間
@property(nullable,copy,readonly)NSDate*domainLookupStartDate;
//?DNS解析結(jié)束時(shí)間,如果解析失敗可能為nil
@property(nullable,copy,readonly)NSDate*domainLookupEndDate;
//?開始建立TCP連接時(shí)間
@property(nullable,copy,readonly)NSDate*connectStartDate;
//?結(jié)束建立TCP連接時(shí)間
@property(nullable,copy,readonly)NSDate*connectEndDate;
//?開始TLS握手時(shí)間
@property(nullable,copy,readonly)NSDate*secureConnectionStartDate;
//?結(jié)束TLS握手時(shí)間
@property(nullable,copy,readonly)NSDate*secureConnectionEndDate;
//?開始傳輸請(qǐng)求數(shù)據(jù)時(shí)間
@property(nullable,copy,readonly)NSDate*requestStartDate;
//?結(jié)束傳輸請(qǐng)求數(shù)據(jù)時(shí)間
@property(nullable,copy,readonly)NSDate*requestEndDate;
//?接收到服務(wù)端響應(yīng)數(shù)據(jù)時(shí)間
@property(nullable,copy,readonly)NSDate*responseStartDate;
//?服務(wù)端響應(yīng)數(shù)據(jù)傳輸完成時(shí)間
@property(nullable,copy,readonly)NSDate*responseEndDate;
//?網(wǎng)絡(luò)協(xié)議,例如http/1.1
@property(nullable,copy,readonly)NSString*networkProtocolName;
//?請(qǐng)求是否使用代理
@property(assign,readonly,getter=isProxyConnection)BOOLproxyConnection;
//?是否復(fù)用已有連接
@property(assign,readonly,getter=isReusedConnection)BOOLreusedConnection;
//?資源標(biāo)識(shí)符,表示請(qǐng)求是從Cache、Push、Network哪種類型加載的
@property(assign,readonly)NSURLSessionTaskMetricsResourceFetchTyperesourceFetchType;
//?本地IP
@property(nullable,copy,readonly)NSString*localAddress;
//?本地端口號(hào)
@property(nullable,copy,readonly)NSNumber*localPort;
//?遠(yuǎn)端IP
@property(nullable,copy,readonly)NSString*remoteAddress;
//?遠(yuǎn)端端口號(hào)
@property(nullable,copy,readonly)NSNumber*remotePort;
//?TLS協(xié)議版本,如果是http則是0x0000
@property(nullable,copy,readonly)NSNumber*negotiatedTLSProtocolVersion;
//?是否使用蜂窩數(shù)據(jù)
@property(readonly,getter=isCellular)BOOLcellular;
下面是我發(fā)起一個(gè)http的下載請(qǐng)求,統(tǒng)計(jì)得到的數(shù)據(jù)。設(shè)備是Xcode模擬器,網(wǎng)絡(luò)環(huán)境是WiFi。
(Request)??{?URL:?http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4?}
(Response)??{?URL:?http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4?}?{?Status?Code:?200,?Headers?{
"Accept-Ranges"=?????(
bytes
);
Age?=?????(
1063663
);
"Ali-Swift-Global-Savetime"=?????(
1575358696
);
Connection?=?????(
"keep-alive"
);
"Content-Length"=?????(
20472584
);
"Content-Md5"=?????(
"YM+JxIH9oLH6l1+jHN9pmQ=="
);
"Content-Type"=?????(
"video/mp4"
);
Date?=?????(
"Tue,?03?Dec?2019?07:38:16?GMT"
);
EagleId?=?????(
dbee142415764223598843838e
);
Etag?=?????(
"\"60CF89C481FDA0B1FA975FA31CDF6999\""
);
"Last-Modified"=?????(
"Fri,?31?Mar?2017?01:41:36?GMT"
);
Server?=?????(
Tengine
);
"Timing-Allow-Origin"=?????(
"*"
);
Via?=?????(
"cache39.l2et2[0,200-0,H],?cache6.l2et2[3,0],?cache16.cn548[0,200-0,H],?cache16.cn548[1,0]"
);
"X-Cache"=?????(
"HIT?TCP_MEM_HIT?dirn:-2:-2"
);
"X-M-Log"=?????(
"QNM:xs451;QNM3:71"
);
"X-M-Reqid"=?????(
"m0AAAP__UChjzNwV"
);
"X-Oss-Hash-Crc64ecma"=?????(
12355898484621380721
);
"X-Oss-Object-Type"=?????(
Normal
);
"X-Oss-Request-Id"=?????(
5DE20106F3150D38305CE159
);
"X-Oss-Server-Time"=?????(
130
);
"X-Oss-Storage-Class"=?????(
Standard
);
"X-Qnm-Cache"=?????(
Hit
);
"X-Swift-CacheTime"=?????(
2592000
);
"X-Swift-SaveTime"=?????(
"Sun,?15?Dec?2019?15:05:37?GMT"
);
}?}
(Fetch?Start)2019-12-1515:05:59+0000
(Domain?Lookup?Start)2019-12-1515:05:59+0000
(Domain?Lookup?End)2019-12-1515:05:59+0000
(Connect?Start)2019-12-1515:05:59+0000
(Secure?Connection?Start)?(null)
(Secure?Connection?End)?(null)
(Connect?End)2019-12-1515:05:59+0000
(Request?Start)2019-12-1515:05:59+0000
(Request?End)2019-12-1515:05:59+0000
(Response?Start)2019-12-1515:05:59+0000
(Response?End)2019-12-1515:06:04+0000
(Protocol?Name)?http/1.1
(Proxy?Connection)NO
(Reused?Connection)NO
(Fetch?Type)?Network?Load
(Request?Header?Bytes)235
(Request?Body?Transfer?Bytes)0
(Request?Body?Bytes)0
(Response?Header?Bytes)866
(Response?Body?Transfer?Bytes)20472584
(Response?Body?Bytes)20472584
(Local?Address)192.168.1.105
(Local?Port)63379
(Remote?Address)219.238.20.101
(Remote?Port)80
(TLS?Protocol?Version)0x0000
(TLS?Cipher?Suite)0x0000
(Cellular)NO
(Expensive)NO
(Constrained)NO
(Multipath)NO
FAQ
NSURLSession的delegate為什么是強(qiáng)引用?
在初始化NSURLSession對(duì)象并設(shè)置代理后,代理對(duì)象將會(huì)被強(qiáng)引用。根據(jù)蘋果官方的注釋來(lái)看,這個(gè)強(qiáng)持有并不會(huì)一直存在,而是在調(diào)用URLSession:didBecomeInvalidWithError:方法后,會(huì)將delegate釋放。
通過(guò)調(diào)用NSURLSession的invalidateAndCancel或finishTasksAndInvalidate方法,即可將強(qiáng)引用斷開并執(zhí)行didBecomeInvalidWithError:代理方法,執(zhí)行完成后session就會(huì)無(wú)效不可以使用。也就是只有在session無(wú)效時(shí),才可以解除強(qiáng)引用的關(guān)系。
有時(shí)候?yàn)榱吮WC連接復(fù)用等問(wèn)題,一般不會(huì)輕易將session會(huì)話invalid,所以最好不要直接使用NSURLSession,而是要對(duì)其進(jìn)行一次二次封裝,使用AFN3.0的原因之一也在于此。
文件上傳
表單上傳
客戶端有時(shí)候需要給服務(wù)端上傳大文件,進(jìn)行大文件肯定不能全都加載到內(nèi)存里,一口氣都傳給服務(wù)器。進(jìn)行大文件上傳時(shí),一般都會(huì)對(duì)需要上傳的文件進(jìn)行分片,分片后逐個(gè)文件進(jìn)行上傳。需要注意的是,分片上傳和斷點(diǎn)續(xù)傳并不是同一個(gè)概念,上傳并不支持?jǐn)帱c(diǎn)續(xù)傳。
進(jìn)行分片上傳時(shí),需要對(duì)本地文件進(jìn)行讀取,我們使用NSFileHandle來(lái)進(jìn)行文件讀取。NSFileHandle提供了一個(gè)偏移量的功能,我們可以將handle的當(dāng)前讀取位置seek到上次讀取的位置,并設(shè)置本次讀取長(zhǎng)度,讀取的文件就是我們指定文件的字節(jié)。
-?(NSData*)readNextBuffer?{
if(self.maxSegment?<=self.currentIndex)?{
returnnil;
}
if(!self.fileHandler){
NSString*filePath?=?[selfuploadFile];
NSFileHandle*fileHandle?=?[NSFileHandlefileHandleForReadingAtPath:filePath];
self.fileHandler?=?fileHandle;
}
[self.fileHandler?seekToFileOffset:(self.currentIndex)?*self.segmentSize];
NSData*data?=?[self.fileHandler?readDataOfLength:self.segmentSize];
returndata;
}
上傳文件現(xiàn)在主流的方式,都是采取表單上傳的方式,也就是multipart/from-data,AFNetworking對(duì)表單上傳也有很有的支持。表單上傳需要遵循下面的格式進(jìn)行上傳,boundary是一個(gè)16進(jìn)制字符串,可以是任何且唯一的。boundary的功能用來(lái)進(jìn)行字段分割,區(qū)分開不同的參數(shù)部分。
multipart/from-data規(guī)范定義在rfc2388,詳細(xì)字段可以看一下規(guī)范。
--boundary
Content-Disposition:?form-data;?name="參數(shù)名"
參數(shù)值
--boundary
Content-Disposition:form-data;name=”表單控件名”;filename=”上傳文件名”
Content-Type:mime?type
要上傳文件二進(jìn)制數(shù)據(jù)
--boundary--
拼接上傳文件基本上可以分為下面三部分,上傳參數(shù)、上傳信息、上傳文件。并且通過(guò)UTF-8格式進(jìn)行編碼,服務(wù)端也采用相同的解碼方式,則可以獲得上傳文件和信息。需要注意的是,換行符數(shù)量是固定的,這都是固定的協(xié)議格式,不要多或者少,會(huì)導(dǎo)致服務(wù)端解析失敗。
-?(NSData*)writeMultipartFormData:(NSData*)data
parameters:(NSDictionary*)parameters?{
if(data.length?==0)?{
returnnil;
}
NSMutableData*formData?=?[NSMutableDatadata];
NSData*lineData?=?[@"\r\n"dataUsingEncoding:NSUTF8StringEncoding];
NSData*boundary?=?[kBoundary?dataUsingEncoding:NSUTF8StringEncoding];
//?拼接上傳參數(shù)
[parameters?enumerateKeysAndObjectsUsingBlock:^(idkey,idobj,BOOL*stop)?{
[formData?appendData:boundary];
[formData?appendData:lineData];
NSString*thisFieldString?=?[NSStringstringWithFormat:@"Content-Disposition:?form-data;?name=\"%@\"\r\n\r\n%@",?key,?obj];
[formData?appendData:[thisFieldString?dataUsingEncoding:NSUTF8StringEncoding]];
[formData?appendData:lineData];
}];
//?拼接上傳信息
[formData?appendData:boundary];
[formData?appendData:lineData];
NSString*thisFieldString?=?[NSStringstringWithFormat:@"Content-Disposition:?form-data;?name=\"%@\";?filename=\"%@\"\r\nContent-Type:?%@",@"name",@"filename",@"mimetype"];
[formData?appendData:[thisFieldString?dataUsingEncoding:NSUTF8StringEncoding]];
[formData?appendData:lineData];
[formData?appendData:lineData];
//?拼接上傳文件
[formData?appendData:data];
[formData?appendData:lineData];
[formData?appendData:?[[NSStringstringWithFormat:@"--%@--\r\n",?kBoundary]?dataUsingEncoding:NSUTF8StringEncoding]];
returnformData;
}
除此之外,表單提交還需要設(shè)置請(qǐng)求頭的Content-Type和Content-Length,否則會(huì)導(dǎo)致請(qǐng)求失敗。其中Content-Length并不是強(qiáng)制要求的,要看后端的具體支持情況。
設(shè)置請(qǐng)求頭時(shí),一定要加上boundary,這個(gè)boundary和拼接上傳文件的boundary需要是同一個(gè)。服務(wù)端從請(qǐng)求頭拿到boundary,來(lái)解析上傳文件。
NSString*headerField?=?[NSStringstringWithFormat:@"multipart/form-data;?charset=utf-8;?boundary=%@",?kBoundary];
[request?setValue:headerField?forHTTPHeaderField:@"Content-Type"];
NSUIntegersize?=?[[[NSFileManagerdefaultManager]?attributesOfItemAtPath:uploadPath?error:nil]?fileSize];
headerField?=?[NSStringstringWithFormat:@"%lu",?size];
[request?setValue:headerField?forHTTPHeaderField:@"Content-Length"];
隨后我們通過(guò)下面的代碼創(chuàng)建NSURLSessionUploadTask,并調(diào)用resume發(fā)起請(qǐng)求,實(shí)現(xiàn)對(duì)應(yīng)的代理回調(diào)即可。
//?發(fā)起網(wǎng)絡(luò)請(qǐng)求
NSURLSessionUploadTask*uploadTask?=?[self.backgroundSession?uploadTaskWithRequest:request?fromData:fromData];
[uploadTask?resume];
//?請(qǐng)求完成后調(diào)用,無(wú)論成功還是失敗
-?(void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error?{
}
//?更新上傳進(jìn)度,會(huì)回調(diào)多次
-?(void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend?{
}
//?數(shù)據(jù)接收完成回調(diào)
-?(void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveData:(NSData*)data?{
}
//?處理后臺(tái)上傳任務(wù),當(dāng)前session的上傳任務(wù)結(jié)束后會(huì)回調(diào)此方法。
-?(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession*)session?{
}
但是,如果你認(rèn)為這就完成一個(gè)上傳功能了,too young too simple~
后臺(tái)上傳
如果通過(guò)fromData的方式進(jìn)行上傳,并不支持后臺(tái)上傳。如果想實(shí)現(xiàn)后臺(tái)上傳,需要通過(guò)fromFile的方式上傳文件。不止如此,fromData還有其他坑。
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromData:(NSData*)bodyData;
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromFile:(NSURL*)fileURL;
內(nèi)存占用
我們發(fā)現(xiàn)通過(guò)fromData:的方式上傳文件,內(nèi)存漲上去之后一直不能降下來(lái),無(wú)論是直接使用NSURLSession還是AFNetworking,都是這樣的。小文件還好,不是很明顯,如果是幾百M(fèi)B的大文件很明顯就會(huì)有一個(gè)內(nèi)存峰值,而且漲上去就不會(huì)降下來(lái)。WTF?
上傳有兩種方式上傳,如果我們把fromData:的上傳改為fromFile:,就可以解決內(nèi)存不下降的問(wèn)題。所以,我們可以把fromData:的上傳方式,理解為UIImage的imageNamed的方法,上傳后NSData文件會(huì)保存在內(nèi)存中,不會(huì)被回收。而fromFile:的方式是從本地加載文件,并且上傳完成后可以被回收。而且如果想支持后臺(tái)上傳,就必須用fromFile:的方式進(jìn)行上傳。
OK,那找到問(wèn)題我們就開干,改變之前的上傳邏輯,改為fromFile:的方式上傳。
//?將分片寫入到本地
NSString*filePath?=?[NSStringstringWithFormat:@"%@/%ld",?[selfsegmentDocumentPath],?currentIndex];
BOOLwrite?=?[formData?writeToFile:filePath?atomically:YES];
//?創(chuàng)建分片文件夾
-?(NSString*)segmentDocumentPath?{
NSString*documentName?=?[fileName?md5String];
NSString*filePath?=?[[SVPUploadCompressor?compressorPath]?stringByAppendingPathComponent:documentName];
BOOLneedCreateDirectory?=YES;
BOOLisDirectory?=NO;
if([[NSFileManagerdefaultManager]?fileExistsAtPath:filePath?isDirectory:&isDirectory])?{
if(isDirectory)?{
needCreateDirectory?=NO;
}else{
[[NSFileManagerdefaultManager]?removeItemAtPath:filePath?error:nil];
}
}
if(needCreateDirectory)?{
[[NSFileManagerdefaultManager]?createDirectoryAtPath:filePath
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
returnfilePath;
}
因?yàn)橐ㄟ^(guò)fromFile:方法傳一個(gè)本地分片的路徑進(jìn)去,所以需要預(yù)先對(duì)文件進(jìn)行分片,并保存在本地。在分片的同時(shí),還需要拼接boundary信息。
所以我們?cè)谏蟼魅蝿?wù)開始前,先對(duì)文件進(jìn)行分片并拼接信息,然后將分片文件寫入到本地。為了方便管理,我們基于具有唯一性的文件名進(jìn)行MD5來(lái)創(chuàng)建分片文件夾,分片文件命名通過(guò)下標(biāo)來(lái)命名,并寫入到本地。文件上傳完成后,直接刪除整個(gè)文件夾即可。當(dāng)然,這些文件操作都是在異步線程中完成的,防止影響UI線程。
我們用一個(gè)400MB的視頻測(cè)試上傳,我們可以從上圖看出,圈紅部分是我們上傳文件的時(shí)間。將上傳方式改為fromFile:后,上傳文件的峰值最高也就是在10MB左右徘徊,這對(duì)于iPhone6這樣的低內(nèi)存老年機(jī)來(lái)說(shuō),是相當(dāng)友好的,不會(huì)導(dǎo)致低端設(shè)備崩潰或者卡頓。
動(dòng)態(tài)分片
用戶在上傳時(shí)網(wǎng)絡(luò)環(huán)境會(huì)有很多情況,WiFi、4G、弱網(wǎng)等很多情況。如果上傳分片太大可能會(huì)導(dǎo)致失敗率上升,分片文件太小會(huì)導(dǎo)致網(wǎng)絡(luò)請(qǐng)求太多,產(chǎn)生太多無(wú)用的boundary、header、數(shù)據(jù)鏈路等資源的浪費(fèi)。
為了解決這個(gè)問(wèn)題,我們采取的是動(dòng)態(tài)分片大小的策略。根據(jù)特定的計(jì)算策略,預(yù)先使用第一個(gè)分片的上傳速度當(dāng)做測(cè)速分片,測(cè)速分片的大小是固定的。根據(jù)測(cè)速的結(jié)果,對(duì)其他分片大小進(jìn)行動(dòng)態(tài)分片,這樣可以保證分片大小可以最大限度的利用當(dāng)前網(wǎng)速。
if([Reachability?reachableViaWiFi])?{
self.segmentSize?=500*1024;
}elseif([Reachability?reachableViaWWAN])?{
self.segmentSize?=300*1024;
}
當(dāng)然,如果覺得這種分片方式太過(guò)復(fù)雜,也可以采取一種閹割版的動(dòng)態(tài)分片策略。即根據(jù)網(wǎng)絡(luò)情況做判斷,如果是WiFi就固定某個(gè)分片大小,如果是流量就固定某個(gè)分片大小。然而這種策略并不穩(wěn)定,因?yàn)楝F(xiàn)在很多手機(jī)的網(wǎng)速比WiFi還快,我們也不能保證WiFi都是百兆光纖。
并行上傳
上傳的所有任務(wù)如果使用的都是同一個(gè)NSURLSession的話,是可以保持連接的,省去建立和斷開連接的消耗。在iOS平臺(tái)上,NSURLSession支持對(duì)一個(gè)Host保持4個(gè)連接,所以,如果我們采取并行上傳,可以更好的利用當(dāng)前的網(wǎng)絡(luò)。
并行上傳的數(shù)量在iOS平臺(tái)上不要超過(guò)4個(gè),最大連接數(shù)是可以通過(guò)NSURLSessionConfiguration設(shè)置的,而且數(shù)量最好不要寫死。同樣的,應(yīng)該基于當(dāng)前網(wǎng)絡(luò)環(huán)境,在上傳任務(wù)開始的時(shí)候就計(jì)算好最大連接數(shù),并設(shè)置給Configuration。
經(jīng)過(guò)我們的線上用戶數(shù)據(jù)分析,在線上環(huán)境使用并行任務(wù)的方式上傳,上傳速度相較于串行上傳提升四倍左右。計(jì)算方式是每秒文件上傳的大小。
iPhone串行上傳:715 kb/s
iPhone并行上傳:2909 kb/s
隊(duì)列管理
分片上傳過(guò)程中可能會(huì)因?yàn)榫W(wǎng)速等原因,導(dǎo)致上傳失敗。失敗的任務(wù)應(yīng)該由單獨(dú)的隊(duì)列進(jìn)行管理,并且在合適的時(shí)機(jī)進(jìn)行失敗重傳。
例如對(duì)一個(gè)500MB的文件進(jìn)行分片,每片是300KB,就會(huì)產(chǎn)生1700多個(gè)分片文件,每一個(gè)分片文件就對(duì)應(yīng)一個(gè)上傳任務(wù)。如果在進(jìn)行上傳時(shí),一口氣創(chuàng)建1700多個(gè)uploadTask,盡管NSURLSession是可以承受的,也不會(huì)造成一個(gè)很大的內(nèi)存峰值。但是我覺得這樣并不太好,實(shí)際上并不會(huì)同時(shí)有這么多請(qǐng)求發(fā)出。
///?已上傳成功片段數(shù)組
@property(nonatomic,strong)NSMutableArray*successSegments;
///?待上傳隊(duì)列的數(shù)組
@property(nonatomic,strong)NSMutableArray*uploadSegments;
所以在創(chuàng)建上傳任務(wù)時(shí),我設(shè)置了一個(gè)最大任務(wù)數(shù),就是同時(shí)向NSURLSession發(fā)起的請(qǐng)求不會(huì)超過(guò)這個(gè)數(shù)量。需要注意的是,這個(gè)最大任務(wù)數(shù)是我創(chuàng)建uploadTask的任務(wù)數(shù),并不是最大并發(fā)數(shù),最大并發(fā)數(shù)由NSURLSession來(lái)控制,我不做干預(yù)。
我將待上傳任務(wù)都放在uploadSegments中,上傳成功后我會(huì)從待上傳任務(wù)數(shù)組中取出一條或多條,并保證同時(shí)進(jìn)行的任務(wù)始終不超過(guò)最大任務(wù)數(shù)。失敗的任務(wù)理論上來(lái)說(shuō)也是需要等待上傳的,所以我把失敗任務(wù)也放在uploadSegments中,插入到隊(duì)列最下面,這樣就保證了待上傳任務(wù)完成后,繼續(xù)重試失敗任務(wù)。
成功的任務(wù)我放在successSegments中,并且始終保持和uploadSegments沒(méi)有交集。兩個(gè)隊(duì)列中保存的并不是uploadTask,而是分片的索引,這也就是為什么我給分片命名的時(shí)候用索引當(dāng)做名字的原因。當(dāng)successSegments等于分片數(shù)量時(shí),就表示所有任務(wù)上傳完成。
文件下載
NSURLSession是在單獨(dú)的進(jìn)程中運(yùn)行,所以通過(guò)此類發(fā)起的網(wǎng)絡(luò)請(qǐng)求,是獨(dú)立于應(yīng)用程序運(yùn)行的,即使App掛起、kill也不會(huì)停止請(qǐng)求。在下載任務(wù)時(shí)會(huì)比較明顯,即便App被kill下載任務(wù)仍然會(huì)繼續(xù),并且允許下次啟動(dòng)App使用這次的下載結(jié)果或繼續(xù)下載。
和上傳代碼一樣,創(chuàng)建下載任務(wù)很簡(jiǎn)單,通過(guò)NSURLSession創(chuàng)建一個(gè)downloadTask,并調(diào)用resume即可開啟一個(gè)下載任務(wù)。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession*session?=?[NSURLSessionsessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueuemainQueue]];
NSURL*url?=?[NSURLURLWithString:@"http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4"];
NSURLRequest*request?=?[[NSURLRequestalloc]?initWithURL:url];
NSURLSessionDownloadTask*downloadTask?=?[session?downloadTaskWithRequest:request];
[downloadTask?resume];
我們可以調(diào)用suspend將下載任務(wù)掛起,隨后調(diào)用resume方法繼續(xù)下載任務(wù),suspend和resume需要是成對(duì)的。但是suspend掛起任務(wù)是有超時(shí)的,默認(rèn)為60s,如果超時(shí)系統(tǒng)會(huì)將TCP連接斷開,我們?cè)僬{(diào)用resume是失效的??梢酝ㄟ^(guò)NSURLSessionConfiguration的timeoutIntervalForResource來(lái)設(shè)置上傳和下載的資源耗時(shí)。suspend只針對(duì)于下載任務(wù),其他任務(wù)掛起后將會(huì)重新開始。
下面兩個(gè)方法是下載比較基礎(chǔ)的方法,分別用來(lái)接收下載進(jìn)度和下載完的臨時(shí)文件地址。didFinishDownloadingToURL:方法是required,當(dāng)下載結(jié)束后下載文件被寫入在Library/Caches下的一個(gè)臨時(shí)文件,我們需要將此文件移動(dòng)到自己的目錄,臨時(shí)目錄在未來(lái)的一個(gè)時(shí)間會(huì)被刪掉。
//?從服務(wù)器接收數(shù)據(jù),下載進(jìn)度回調(diào)
-?(void)URLSession:(NSURLSession*)session?downloadTask:(NSURLSessionDownloadTask*)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite?{
CGFloatprogress?=?(CGFloat)totalBytesWritten?/?(CGFloat)totalBytesExpectedToWrite;
self.progressView.progress?=?progress;
}
//?下載完成后回調(diào)
-?(void)URLSession:(NSURLSession*)session?downloadTask:(NSURLSessionDownloadTask*)downloadTask
didFinishDownloadingToURL:(NSURL*)location?{
}
斷點(diǎn)續(xù)傳
HTTP協(xié)議支持?jǐn)帱c(diǎn)續(xù)傳操作,在開始下載請(qǐng)求時(shí)通過(guò)請(qǐng)求頭設(shè)置Range字段,標(biāo)示從什么位置開始下載。
Range:bytes=512000-
服務(wù)端收到客戶端請(qǐng)求后,開始從512kb的位置開始傳輸數(shù)據(jù),并通過(guò)Content-Range字段告知客戶端傳輸數(shù)據(jù)的起始位置。
Content-Range:bytes?512000-/1024000
downloadTask任務(wù)開始請(qǐng)求后,可以調(diào)用cancelByProducingResumeData:方法可以取消下載,并且可以獲得一個(gè)resumeData,resumeData中存放一些斷點(diǎn)下載的信息。可以將resumeData寫到本地,后面通過(guò)這個(gè)文件可以進(jìn)行斷點(diǎn)續(xù)傳。
NSString*library?=NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES).firstObject;
NSString*resumePath?=?[library?stringByAppendingPathComponent:[self.downloadURL?md5String]];
[self.downloadTask?cancelByProducingResumeData:^(NSData*?_Nullable?resumeData)?{
[resumeData?writeToFile:resumePath?atomically:YES];
}];
在創(chuàng)建下載任務(wù)前,可以判斷當(dāng)前任務(wù)有沒(méi)有之前待恢復(fù)的任務(wù),如果有的話調(diào)用downloadTaskWithResumeData:方法并傳入一個(gè)resumeData,可以恢復(fù)之前的下載,并重新創(chuàng)建一個(gè)downloadTask任務(wù)。
NSString*library?=NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES).firstObject;
NSString*resumePath?=?[library?stringByAppendingPathComponent:[self.downloadURL?md5String]];
NSData*resumeData?=?[[NSDataalloc]?initWithContentsOfFile:resumePath];
self.downloadTask?=?[self.session?downloadTaskWithResumeData:resumeData];
[self.downloadTask?resume];
通過(guò)suspend和resume這種方式掛起的任務(wù),downloadTask是同一個(gè)對(duì)象,而通過(guò)cancel然后resumeData恢復(fù)的任務(wù),會(huì)創(chuàng)建一個(gè)新的downloadTask任務(wù)。
當(dāng)調(diào)用downloadTaskWithResumeData:方法恢復(fù)下載后,會(huì)回調(diào)下面的方法?;卣{(diào)參數(shù)fileOffset是上次文件的下載大小,expectedTotalBytes是預(yù)估的文件總大小。
-(void)URLSession:(NSURLSession*)sessiondownloadTask:(NSURLSessionDownloadTask*)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
后臺(tái)下載
通過(guò)backgroundSessionConfigurationWithIdentifier方法創(chuàng)建后臺(tái)上傳或后臺(tái)下載類型的NSURLSessionConfiguration,并且設(shè)置一個(gè)唯一標(biāo)識(shí),需要保證這個(gè)標(biāo)識(shí)在不同的session之間的唯一性。后臺(tái)任務(wù)只支持http和https的任務(wù),其他協(xié)議的任務(wù)并不支持。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSessionsessionWithConfiguration:config?delegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
通過(guò)backgroundSessionConfigurationWithIdentifier方法創(chuàng)建的NSURLSession,請(qǐng)求任務(wù)將會(huì)在系統(tǒng)的單獨(dú)進(jìn)程中進(jìn)行,因此即使App進(jìn)程被kill也不受影響,依然可以繼續(xù)執(zhí)行請(qǐng)求任務(wù)。如果程序被系統(tǒng)kill調(diào),下次啟動(dòng)并執(zhí)行didFinishLaunchingWithOptions可以通過(guò)相同的identifier創(chuàng)建NSURLSession和NSURLSessionConfiguration,系統(tǒng)會(huì)將新創(chuàng)建的NSURLSession和單獨(dú)進(jìn)程中正在運(yùn)行的NSURLSession進(jìn)行關(guān)聯(lián)。
在程序啟動(dòng)并執(zhí)行didFinishLaunchingWithOptions方法時(shí),按照下面方法創(chuàng)建NSURLSession即可將新創(chuàng)建的Session和之前的Session綁定,并自動(dòng)開始執(zhí)行之前的下載任務(wù)?;謴?fù)之前的任務(wù)后會(huì)繼續(xù)執(zhí)行NSURLSession的代理方法,并執(zhí)行后面的任務(wù)。
-?(BOOL)application:(UIApplication*)application?didFinishLaunchingWithOptions:(NSDictionary*)launchOptions?{
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSessionsessionWithConfiguration:config?delegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
returnYES;
}
當(dāng)應(yīng)用進(jìn)入到后臺(tái)時(shí),可以繼續(xù)下載,如果客戶端沒(méi)有開啟Background Mode,則不會(huì)回調(diào)客戶端進(jìn)度。下次進(jìn)入前臺(tái)時(shí),會(huì)繼續(xù)回調(diào)新的進(jìn)度。
如果在后臺(tái)下載完成,則會(huì)通過(guò)AppDelegate的回調(diào)方法通知應(yīng)用來(lái)刷新UI。由于下載是在一個(gè)單獨(dú)的進(jìn)程中完成的,即便業(yè)務(wù)層代碼會(huì)停止執(zhí)行,但下載的回調(diào)依然會(huì)被調(diào)用。在回調(diào)時(shí),允許用戶處理業(yè)務(wù)邏輯,以及刷新UI。
調(diào)用此方法后可以開始刷新UI,調(diào)用completionHandler表示刷新結(jié)束,所以上層業(yè)務(wù)要做一些控制邏輯。didFinishDownloadingToURL的調(diào)用時(shí)機(jī)會(huì)比此方法要晚,依然在那個(gè)方法里可以判斷下載文件。由于項(xiàng)目中可能會(huì)存在多個(gè)下載任務(wù),所以需要通過(guò)identifier對(duì)下載任務(wù)進(jìn)行區(qū)分。
-?(void)application:(UIApplication*)application?handleEventsForBackgroundURLSession:(NSString*)identifier?completionHandler:(void(^)(void))completionHandler?{
ViewController?*vc?=?(ViewController?*)self.window.rootViewController;
vc.completionHandler?=?completionHandler;
}
需要注意的是,如果存在多個(gè)相同名字的identifier任務(wù),則創(chuàng)建的session會(huì)將同名的任務(wù)都繼續(xù)執(zhí)行。NSURLSessionConfiguration還提供下面的屬性,在session下載任務(wù)完成時(shí)是否啟動(dòng)App,默認(rèn)為YES,如果設(shè)置為NO則后臺(tái)下載會(huì)受到影響。
@propertyBOOL?sessionSendsLaunchEvents;
后臺(tái)下載過(guò)程中會(huì)設(shè)計(jì)到一系列的代理方法調(diào)用,下面是調(diào)用順序。
視頻文件下載
現(xiàn)在很多視頻類App都有視頻下載的功能,視頻下載肯定不會(huì)是單純的把一個(gè)mp4下載下來(lái)就可以,這里就講一下視頻下載相關(guān)的知識(shí)。
視頻地址一般都是從服務(wù)端獲取的,所以需要先請(qǐng)求接口獲取下載地址。這個(gè)地址可以是某個(gè)接口就已經(jīng)請(qǐng)求下來(lái)的,也可以是某個(gè)固定格式拼接的。
現(xiàn)在有很多視頻App都是有免流服務(wù)的,例如騰訊大王卡、螞蟻寶卡之類的,免流服務(wù)的本質(zhì)就是對(duì)m3u8、ts、mp4地址重新包一層,請(qǐng)求數(shù)據(jù)的時(shí)候直接請(qǐng)求運(yùn)營(yíng)商給的地址,運(yùn)營(yíng)商對(duì)數(shù)據(jù)做了一個(gè)中轉(zhuǎn)操作。
以流視頻m3u8為例,有了免流地址,先下載m3u8文件。這個(gè)文件一般都是加密的,下載完成后客戶端會(huì)對(duì)m3u8文件進(jìn)行decode,獲取到真正的m3u8文件。
m3u8文件本質(zhì)上是ts片段的集合,視頻播放播的還是ts片段。隨后對(duì)m3u8文件進(jìn)行解析,獲取到ts片段地址,并將ts下載地址轉(zhuǎn)成免流地址后逐個(gè)下載,也可以并行下載。
m3u8文件下載后會(huì)以固定格式存在文件夾下,文件夾對(duì)應(yīng)被緩存的視頻。ts片命名以數(shù)字命名,例如0.ts,下標(biāo)從0開始。
所有ts片段下載完成后,生成本地m3u8文件。
m3u8文件分為遠(yuǎn)端和本地兩種,遠(yuǎn)端的就是正常下載的地址,本地m3u8文件是在播放本地視頻的時(shí)候傳入。格式和普通m3u8文件差不多,區(qū)別在于ts地址是本地地址,例如下面的地址。
#EXTM3U
#EXT-X-TARGETDURATION:30
#EXT-X-VERSION:3
#EXTINF:9.28,
0.ts
#EXTINF:33.04,
1.ts
#EXTINF:30.159,
2.ts
#EXTINF:23.841,
3.ts
#EXT-X-ENDLIST
m3u8文件
HLS(Http Live Streaming)是蘋果推出的流媒體協(xié)議,其中包含兩部分,m3u8文件和ts文件。使用ts文件的原因是因?yàn)槎鄠€(gè)ts可以無(wú)縫拼接,并且單個(gè)ts可以單獨(dú)播放。而mp4由于格式原因,被分割的mp4文件單獨(dú)播放會(huì)導(dǎo)致畫面撕裂或者音頻缺失的問(wèn)題。如果單獨(dú)下載多個(gè)mp4文件,播放時(shí)會(huì)導(dǎo)致間斷的問(wèn)題。
m3u8是Unicode版本的m3u,是蘋果推出的一種視頻格式,是一個(gè)基于HTTP的流媒體傳輸協(xié)議。m3u8協(xié)議將一個(gè)媒體文件切為多個(gè)小文件,并利用HTTP協(xié)議進(jìn)行數(shù)據(jù)傳輸,小文件所在的資源服務(wù)器路徑存儲(chǔ)在.m3u8文件中??蛻舳四玫絤3u8文件,即可根據(jù)文件中資源文件的路徑,分別下載不同的文件。
m3u8文件必須是utf-8格式編碼的,在文件中以#EXT開頭的是標(biāo)簽,并且大小寫敏感。以#開頭的其他字符串則都會(huì)被認(rèn)為是注釋。m3u8分為點(diǎn)播和直播,點(diǎn)播在第一次請(qǐng)求.m3u8文件后,將下載下來(lái)的ts片段進(jìn)行順序播放即可。直播則需要過(guò)一段時(shí)間對(duì).m3u8文件進(jìn)行一個(gè)增量下載,并繼續(xù)下載后續(xù)的ts文件。
m3u8中有很多標(biāo)簽,下面是項(xiàng)目中用到的一些標(biāo)簽或主要標(biāo)簽。將mp4或者flv文件進(jìn)行切片很簡(jiǎn)單,直接用ffmpeg命令切片即可。
起始標(biāo)簽,此標(biāo)簽必須在整個(gè)文件的開頭。
#EXTM3U
結(jié)束標(biāo)簽,此標(biāo)簽必須在整個(gè)文件的末尾。
#EXT-X-ENDLIST
當(dāng)前文件版本,如果不指定則默認(rèn)為1
#EXT-X-VERSION
所有ts片段最大時(shí)長(zhǎng)。
#EXT-X-TARGETDURATION
當(dāng)前ts片段時(shí)長(zhǎng)。
#EXTINF
如果沒(méi)有#EXT或#開頭的,一般都是ts片段下載地址。路徑可以是絕對(duì)路徑,也可以是相對(duì)路徑,我們項(xiàng)目里使用的是絕對(duì)路徑。但相對(duì)路徑數(shù)據(jù)量會(huì)相對(duì)比較小,只不過(guò)看視頻的人網(wǎng)速不會(huì)太差。
下面是相對(duì)路徑地址,文件中只有segment1.ts,則表示相對(duì)于m3u8的路徑,也就是下面的路徑。
https://data.vod.itc.cn/m3u8
https://data.vod.itc.cn/segment1.ts
常見錯(cuò)誤
A?background?URLSessionwithidentifier?backgroundSession?already?exists
如果重復(fù)后臺(tái)已經(jīng)存在的下載任務(wù),會(huì)提示這個(gè)錯(cuò)誤。需要在頁(yè)面退出或程序退出時(shí),調(diào)用finishTasksAndInvalidate方法將任務(wù)invalidate。
[[NSNotificationCenterdefaultCenter]?addObserver:self
selector:@selector(willTerminateNotification)
name:UIApplicationWillTerminateNotification
object:nil];
-?(void)willTerminateNotification?{
[self.session?getAllTasksWithCompletionHandler:^(NSArray<__kindofNSURLSessionTask*>?*?_Nonnull?tasks)?{
if(tasks.count)?{
[self.session?finishTasksAndInvalidate];
}
}];
}
文章來(lái)源于搜狐技術(shù)產(chǎn)品?,作者劉小壯,轉(zhuǎn)發(fā)至知識(shí)小集。