NSURLSession
NSURLSession在iOS7中推出达址,NSURLSession的推出旨在替換之前的NSURLConnection贺待,NSURLSession的使用相對于之前的NSURLConnection更簡單,而且不用處理Runloop相關(guān)的東西。
2015年RFC 7540標準發(fā)布了http 2.0版本,http 2.0版本中包含很多新的特性,在傳輸速度上也有很明顯的提升者娱。NSURLSession從iOS9.0開始,對http 2.0提供了支持苏揣。
NSURLSession部分構(gòu)成:
NSURLSession:請求會話對象黄鳍,可以用系統(tǒng)提供的單例對象,也可以自己創(chuàng)建平匈。
NSURLSessionConfiguration:對session會話進行配置框沟,一般都采用default。
NSURLSessionTask:負責執(zhí)行具體請求的task增炭,由session創(chuàng)建忍燥。
NSURLSession有三種方式創(chuàng)建:
sharedSession
系統(tǒng)維護的一個單例對象,可以和其他使用這個session的task共享連接和請求信息隙姿。
sessionWithConfiguration:
在NSURLSession初始化時傳入一個NSURLSessionConfiguration梅垄,這樣可以自定義請求頭、cookie等信息孟辑。
sessionWithConfiguration:delegate:delegateQueue:
如果想更好的控制請求過程以及回調(diào)線程哎甲,需要上面的方法進行初始化操作,并傳入delegate來設(shè)置回調(diào)對象和回調(diào)的線程饲嗽。
通過NSURLSession發(fā)起一個網(wǎng)絡(luò)請求也比較簡單炭玫。
創(chuàng)建一個NSURLSessionConfiguration配置請求。
通過Configuration創(chuàng)建NSURLSession對象貌虾。
通過session對象發(fā)起網(wǎng)絡(luò)請求吞加,并獲取task對象。
調(diào)用[task resume]方法發(fā)起網(wǎng)絡(luò)請求。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession*session?=?[NSURLSessionsessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueuemainQueue]];
NSURLSessionDataTask*task?=?[session?dataTaskWithURL:[NSURLURLWithString:@"http://www.baidu.com"]];
[task?resume];
NSURLSessionTask
通過NSURLSession發(fā)起的每個請求衔憨,都會被封裝為一個NSURLSessionTask任務(wù)叶圃,但一般不會直接是NSURLSessionTask類,而是基于不同任務(wù)類型践图,被封裝為其對應(yīng)的子類掺冠。
NSURLSessionDataTask:處理普通的Get、Post請求码党。
NSURLSessionUploadTask:處理上傳請求德崭,可以傳入對應(yīng)的上傳文件或路徑。
NSURLSessionDownloadTask:處理下載地址揖盘,提供斷點續(xù)傳功能的cancel方法眉厨。
主要方法都定義在父類NSURLSessionTask中,下面是一些關(guān)鍵方法或?qū)傩浴?/p>
currentRequest
當前正在執(zhí)行的任務(wù)兽狭,一般和originalRequest是一樣的憾股,除非發(fā)生重定向才會有所區(qū)別。
originalRequest
主要用于重定向操作箕慧,用來記錄重定向前的請求服球。
taskIdentifier
當前session下,task的唯一標示销钝,多個session之間可能存在相同的標識有咨。
priority
task中可以設(shè)置優(yōu)先級,但這個屬性并不代表請求的優(yōu)先級蒸健,而是一個標示座享。官方已經(jīng)說明,NSURLSession并沒有提供API可以改變請求的優(yōu)先級似忧。
state
當前任務(wù)的狀態(tài)渣叛,可以通過KVO的方式監(jiān)聽狀態(tài)的改變。
- resume
開始或繼續(xù)請求盯捌,創(chuàng)建后的task默認是掛起的淳衙,需要手動調(diào)用resume才可以開始請求。
- suspend
掛起當前請求饺著。主要是下載請求用的多一些箫攀,普通請求掛起后都會重新開始請求。下載請求掛起后幼衰,只要不超過NSURLRequest設(shè)置的timeout時間靴跛,調(diào)用resume就是繼續(xù)請求。
- cancel
取消當前請求渡嚣。任務(wù)會被標記為取消梢睛,并在未來某個時間調(diào)用URLSession:task:didCompleteWithError:方法肥印。
NSURLSession提供有普通創(chuàng)建task的方式,創(chuàng)建后可以通過重寫代理方法绝葡,獲取對應(yīng)的回調(diào)和參數(shù)深碱。這種方式對于請求過程比較好控制。
-?(NSURLSessionDataTask*)dataTaskWithRequest:(NSURLRequest*)request;
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromFile:(NSURL*)fileURL;
-?(NSURLSessionDownloadTask*)downloadTaskWithRequest:(NSURLRequest*)request;
除此之外藏畅,NSURLSession也提供了block的方式創(chuàng)建task敷硅,創(chuàng)建方式簡單如AFN,直接傳入URL或NSURLRequest墓赴,即可直接在block中接收返回數(shù)據(jù)竞膳。和普通創(chuàng)建方式一樣,block的創(chuàng)建方式創(chuàng)建后默認也是suspend的狀態(tài)诫硕,需要調(diào)用resume開始任務(wù)。
completionHandler和delegate是互斥的刊侯,completionHandler的優(yōu)先級大于delegate章办。相對于普通創(chuàng)建方法,block方式更偏向于面向結(jié)果的創(chuàng)建滨彻,可以直接在completionHandler中獲取返回結(jié)果藕届,但不能控制請求過程。
-?(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;
可以通過下面的兩個方法亭饵,獲取當前session對應(yīng)的所有task休偶,方法區(qū)別在于回調(diào)的參數(shù)不同。以getTasksWithCompletionHandler為例辜羊,在AFN中的應(yī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時可以指定線程碱妆,如果不指定線程,則completionHandler和delegate的回調(diào)方法昔驱,都會在子線程中執(zhí)行疹尾。
如果初始化NSURLSession時指定了delegateQueue,則回調(diào)會在指定的隊列中執(zhí)行骤肛,如果指定的是mainQueue纳本,則回調(diào)在主線程中執(zhí)行,這樣就避免了切換線程的問題腋颠。
[NSURLSessionsessionWithConfiguration:config?delegate:selfdelegateQueue:nil];
delegate
對于NSURLSession的代理方法這里就不詳細列舉了繁成,方法命名遵循蘋果一貫見名知意的原則,用起來很簡單秕豫。這里介紹一下NSURLSession的代理繼承結(jié)構(gòu)朴艰。
NSURLSession中定義了一系列代理观蓄,并遵循上面的繼承關(guān)系。根據(jù)繼承關(guān)系和代理方法的聲明祠墅,如果執(zhí)行某項任務(wù)侮穿,只需要遵守其中的某個代理即可。
例如執(zhí)行上傳或普通Post請求毁嗦,則遵守NSURLSessionDataDelegate亲茅,執(zhí)行下載任務(wù)則遵循NSURLSessionDownloadDelegate,父級代理定義的都是公共方法狗准。
請求重定向
HTTP協(xié)議中定義了例如301等重定向狀態(tài)碼克锣,通過下面的代理方法,可以處理重定向任務(wù)腔长。發(fā)生重定向時可以根據(jù)response創(chuàng)建一個新的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負責對NSURLSession初始化時進行配置鸟召,通過NSURLSessionConfiguration可以設(shè)置請求的Cookie胆绊、密鑰、緩存欧募、請求頭等參數(shù)压状,將網(wǎng)絡(luò)請求的一些配置參數(shù)從NSURLSession中分離出來。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession*session?=?[NSURLSessionsessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueuemainQueue]];
NSURLSessionConfiguration提供三種初始化方法跟继,下面是請求的方法的一些解釋种冬。
@property(class,readonly,strong)NSURLSessionConfiguration*defaultSessionConfiguration;
NSURLSessionConfiguration提供defaultSessionConfiguration的方式創(chuàng)建,但這并不是單例方法还栓,而是類方法碌廓,創(chuàng)建的是不同對象。通過這種方式創(chuàng)建的configuration剩盒,并不會共享cookie谷婆、cache、密鑰等辽聊,而是不同configuration都需要單獨設(shè)置纪挎。
這塊網(wǎng)上很多人理解都是錯的,并沒有真的在項目里使用或者沒有留意過跟匆,如和其他人有出入异袄,以我為準。
@property(class,readonly,strong)NSURLSessionConfiguration*ephemeralSessionConfiguration;
創(chuàng)建臨時的configuration玛臂,通過這種方式創(chuàng)建的對象烤蜕,和普通的對象主要區(qū)別在于URLCache封孙、URLCredentialStorage、HTTPCookieStorage上面讽营。同樣的虎忌,Ephemeral也不是單例方法,而只是類方法橱鹏。
URLCredentialStorage
Ephemeral<__NSCFMemoryURLCredentialStorage:0x600001bc8320>
HTTPCookieStorage
Ephemeral
如果對Ephemeral方式創(chuàng)建的config進行打印的話膜蠢,可以看到變量類型明顯區(qū)別于其他類型,并且在打印信息前面會有Ephemeral的標示莉兰。通過Ephemeral的方式創(chuàng)建的config挑围,不會產(chǎn)生持久化信息,可以很好保護請求的數(shù)據(jù)安全性糖荒。
+?(NSURLSessionConfiguration*)backgroundSessionConfigurationWithIdentifier:(NSString*)identifier;
identifier方式一般用于恢復(fù)之前的任務(wù)杉辙,主要用于下載。如果一個下載任務(wù)正在進行中寂嘉,程序被kill調(diào)奏瞬,可以在程序退出之前保存identifier。下次進入程序后通過identifier恢復(fù)之前的任務(wù)泉孩,系統(tǒng)會將NSURLSession及NSURLSessionConfiguration和之前的下載任務(wù)進行關(guān)聯(lián),并繼續(xù)之前的任務(wù)并淋。
timeout
timeoutIntervalForRequest
設(shè)置session請求間的超時時間寓搬,這個超時時間并不是請求從開始到結(jié)束的時間,而是兩個數(shù)據(jù)包之間的時間間隔县耽。當任意請求返回后這個值將會被重置句喷,如果在超時時間內(nèi)未返回則超時。單位為秒兔毙,默認為60秒唾琼。
timeoutIntervalForResource
資源超時時間,一般用于上傳或下載任務(wù)澎剥,在上傳或下載任務(wù)開始后計時锡溯,如果到達時間任務(wù)未結(jié)束,則刪除資源文件哑姚。單位為秒祭饭,默認時間是七天。
資源共享
如果是相同的NSURLSessionConfiguration對象叙量,會共享請求頭倡蝙、緩存、cookie绞佩、Credential寺鸥,通過Configuration創(chuàng)建的NSURLSession猪钮,也會擁有對應(yīng)的請求信息。
@property(nullable,copy)NSDictionary*HTTPAdditionalHeaders;
公共請求頭胆建,默認是空的烤低,設(shè)置后所有經(jīng)Confuguration配置的NSURLSession,請求頭都會帶有設(shè)置的信息眼坏。
@property(nullable,retain)NSHTTPCookieStorage*HTTPCookieStorage;
HTTP請求的Cookie管理器拂玻。如果是通過sharedSession或backgroundConfiguration創(chuàng)建的NSURLSession,默認使用sharedHTTPCookieStorage的Cookie數(shù)據(jù)宰译。如果不想使用Cookie檐蚜,則直接設(shè)置為nil即可,也可以手動設(shè)置為自己的CookieStorage沿侈。
@property(nullable,retain)NSURLCredentialStorage*URLCredentialStorage;
證書管理器闯第。如果是通過sharedSession或backgroundConfiguration創(chuàng)建的NSURLSession,默認使用sharedCredentialStorage的證書缀拭。如果不想使用證書咳短,可以直接設(shè)置為nil,也可以自己創(chuàng)建證書管理器蛛淋。
@property(nullable,retain)NSURLCache*URLCache;
請求緩存咙好,如果不手動設(shè)置的話為nil,對于NSURLCache這個類我沒有研究過褐荷,不太了解勾效。
緩存處理
在NSURLRequest中可以設(shè)置cachePolicy請求緩存策略,這里不對具體值做詳細描述叛甫,默認值為NSURLRequestUseProtocolCachePolicy使用緩存层宫。
NSURLSessionConfiguration可以設(shè)置處理緩存的對象,我們可以手動設(shè)置自定義的緩存對象其监,如果不設(shè)置的話萌腿,默認使用系統(tǒng)的sharedURLCache單例緩存對象。經(jīng)過configuration創(chuàng)建的NSURLSession發(fā)出的請求抖苦,NSURLRequest都會使用這個NSURLCache來處理緩存毁菱。
@property(nullable,retain)NSURLCache*URLCache;
NSURLCache提供了Memory和Disk的緩存,在創(chuàng)建時需要為其分別指定Memory和Disk的大小睛约,以及存儲的文件位置鼎俘。使用NSURLCache不用考慮磁盤空間不夠,或手動管理內(nèi)存空間的問題辩涝,如果發(fā)生內(nèi)存警告系統(tǒng)會自動清理內(nèi)存空間贸伐。但是NSURLCache提供的功能非常有限,項目中一般很少直接使用它來處理緩存數(shù)據(jù)怔揩,還是用數(shù)據(jù)庫比較多捉邢。
[[NSURLCache?alloc]?initWithMemoryCapacity:30?*?1024?*?1024
diskCapacity:30?*?1024?*?1024
directoryURL:[NSURL?URLWithString:filePath]]
;
使用NSURLCache還有一個好處脯丝,就是可以由服務(wù)端來設(shè)置資源過期時間,在請求服務(wù)端后伏伐,服務(wù)端會返回Cache-Control來說明文件的過期時間宠进。NSURLCache會根據(jù)NSURLResponse來自動完成過期時間的設(shè)置。
最大連接數(shù)
限制NSURLSession的最大連接數(shù)藐翎,通過此方法創(chuàng)建的NSURLSession和服務(wù)端的最大連接數(shù)量不會超出這里設(shè)置的數(shù)量材蹬。蘋果為我們設(shè)置的iOS端默認為4,Mac端默認為6吝镣。
@propertyNSInteger?HTTPMaximumConnectionsPerHost;
連接復(fù)用
HTTP是基于傳輸層協(xié)議TCP的堤器,通過TCP發(fā)送網(wǎng)絡(luò)請求都需要先進行三次握手,建立網(wǎng)絡(luò)請求后再發(fā)送數(shù)據(jù)末贾,請求結(jié)束時再經(jīng)歷四次揮手闸溃。HTTP1.0開始支持keep-alive,keep-alive可以保持已經(jīng)建立的鏈接拱撵,如果是相同的域名辉川,在請求連接建立后,后面的請求不會立刻斷開拴测,而是復(fù)用現(xiàn)有的連接乓旗。從HTTP1.1開始默認開啟keep-alive。
請求是在請求頭中設(shè)置下面的參數(shù)集索,服務(wù)器如果支持keep-alive的話寸齐,響應(yīng)客戶端請求時,也會在響應(yīng)頭中加上相同的字段抄谐。
Connection:?Keep-Alive
如果想斷開keep-alive,可以在請求頭中加上下面的字段扰法,但一般不推薦這么做蛹含。
Connection:?Close
如果通過NSURLSession來進行網(wǎng)絡(luò)請求的話,需要使用同一個NSURLSession對象塞颁,如果創(chuàng)建新的session對象則不能復(fù)用之前的鏈接浦箱。keep-alive可以保持請求的連接,蘋果允許在iOS上最大保持有4個連接祠锣,Mac則是6個連接酷窥。
pipeline
在HTTP1.1中,基于keep-alive伴网,還可以將請求進行管線化蓬推。和相同后端服務(wù),TCP層建立的鏈接澡腾,一般都需要前一個請求返回后沸伏,后面的請求再發(fā)出糕珊。但pipeline就可以不依賴之前請求的響應(yīng),而發(fā)出后面的請求毅糟。
pipeline依賴客戶端和服務(wù)器都有實現(xiàn)红选,服務(wù)端收到客戶端的請求后,要按照先進先出的順序進行任務(wù)處理和響應(yīng)姆另。pipeline依然存在之前非pipeline的問題喇肋,就是前面的請求如果出現(xiàn)問題,會阻塞當前連接影響后面的請求迹辐。
pipeline對于請求大文件并沒有提升作用蝶防,只是對于普通請求速度有提升。在NSURLSessionConfiguration中可以設(shè)置HTTPShouldUsePipelining為YES右核,開啟管線化慧脱,此屬性默認為NO。
NSURLSessionTaskMetrics
在日常開發(fā)過程中贺喝,經(jīng)常遇到頁面加載太慢的問題菱鸥,這很大一部分原因都是因為網(wǎng)絡(luò)導(dǎo)致的。所以躏鱼,查找網(wǎng)絡(luò)耗時的原因并解決氮采,就是一個很重要的任務(wù)了。蘋果對于網(wǎng)絡(luò)檢查提供了NSURLSessionTaskMetrics類來進行檢查染苛,NSURLSessionTaskMetrics是對應(yīng)NSURLSessionTaskDelegate的鹊漠,每個task結(jié)束時都會回調(diào)下面的方法,并且可以獲得一個metrics對象茶行。
-?(void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics;
NSURLSessionTaskMetrics可以很好的幫助我們分析網(wǎng)絡(luò)請求的過程躯概,以找到耗時原因。除了這個類之外畔师,NSURLSessionTaskTransactionMetrics類中承載了更詳細的數(shù)據(jù)娶靡。
@property(copy,readonly)NSArray?*transactionMetrics;
transactionMetrics數(shù)組中每一個元素都對應(yīng)著當前task的一個請求,一般數(shù)組中只會有一個元素看锉,如果發(fā)生重定向等情況姿锭,可能會存在多個元素。
@property(copy,readonly)NSDateInterval*taskInterval;
taskInterval記錄了當前task從開始請求到最后完成的總耗時伯铣,NSDateInterval中包含了startDate呻此、endDate和duration耗時時間。
@property(assign,readonly)NSUIntegerredirectCount;
redirectCount記錄了重定向次數(shù)腔寡,在進行下載請求時一般都會進行重定向焚鲜,來保證下載任務(wù)能由后端最合適的節(jié)點來處理。
NSURLSessionTaskTransactionMetrics
NSURLSessionTaskTransactionMetrics中的屬性都是用來做統(tǒng)計的,功能都是記錄某個值恃泪,并沒有邏輯上的意義郑兴。所以這里就對一些主要的屬性做一下解釋,基本涵蓋了大部分屬性贝乎,其他就不管了情连。
這張圖是我從網(wǎng)上扒下來的,標示了NSURLSessionTaskTransactionMetrics的屬性在請求過程中處于什么位置览效。
//?請求對象
@property(copy,readonly)NSURLRequest*request;
//?響應(yīng)對象却舀,請求失敗可能會為nil
@property(nullable,copy,readonly)NSURLResponse*response;
//?請求開始時間
@property(nullable,copy,readonly)NSDate*fetchStartDate;
//?DNS解析開始時間
@property(nullable,copy,readonly)NSDate*domainLookupStartDate;
//?DNS解析結(jié)束時間,如果解析失敗可能為nil
@property(nullable,copy,readonly)NSDate*domainLookupEndDate;
//?開始建立TCP連接時間
@property(nullable,copy,readonly)NSDate*connectStartDate;
//?結(jié)束建立TCP連接時間
@property(nullable,copy,readonly)NSDate*connectEndDate;
//?開始TLS握手時間
@property(nullable,copy,readonly)NSDate*secureConnectionStartDate;
//?結(jié)束TLS握手時間
@property(nullable,copy,readonly)NSDate*secureConnectionEndDate;
//?開始傳輸請求數(shù)據(jù)時間
@property(nullable,copy,readonly)NSDate*requestStartDate;
//?結(jié)束傳輸請求數(shù)據(jù)時間
@property(nullable,copy,readonly)NSDate*requestEndDate;
//?接收到服務(wù)端響應(yīng)數(shù)據(jù)時間
@property(nullable,copy,readonly)NSDate*responseStartDate;
//?服務(wù)端響應(yīng)數(shù)據(jù)傳輸完成時間
@property(nullable,copy,readonly)NSDate*responseEndDate;
//?網(wǎng)絡(luò)協(xié)議锤灿,例如http/1.1
@property(nullable,copy,readonly)NSString*networkProtocolName;
//?請求是否使用代理
@property(assign,readonly,getter=isProxyConnection)BOOLproxyConnection;
//?是否復(fù)用已有連接
@property(assign,readonly,getter=isReusedConnection)BOOLreusedConnection;
//?資源標識符挽拔,表示請求是從Cache、Push但校、Network哪種類型加載的
@property(assign,readonly)NSURLSessionTaskMetricsResourceFetchTyperesourceFetchType;
//?本地IP
@property(nullable,copy,readonly)NSString*localAddress;
//?本地端口號
@property(nullable,copy,readonly)NSNumber*localPort;
//?遠端IP
@property(nullable,copy,readonly)NSString*remoteAddress;
//?遠端端口號
@property(nullable,copy,readonly)NSNumber*remotePort;
//?TLS協(xié)議版本螃诅,如果是http則是0x0000
@property(nullable,copy,readonly)NSNumber*negotiatedTLSProtocolVersion;
//?是否使用蜂窩數(shù)據(jù)
@property(readonly,getter=isCellular)BOOLcellular;
下面是我發(fā)起一個http的下載請求,統(tǒng)計得到的數(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為什么是強引用亭枷?
在初始化NSURLSession對象并設(shè)置代理后袭艺,代理對象將會被強引用。根據(jù)蘋果官方的注釋來看叨粘,這個強持有并不會一直存在猾编,而是在調(diào)用URLSession:didBecomeInvalidWithError:方法后,會將delegate釋放升敲。
通過調(diào)用NSURLSession的invalidateAndCancel或finishTasksAndInvalidate方法答倡,即可將強引用斷開并執(zhí)行didBecomeInvalidWithError:代理方法,執(zhí)行完成后session就會無效不可以使用驴党。也就是只有在session無效時苇羡,才可以解除強引用的關(guān)系。
有時候為了保證連接復(fù)用等問題鼻弧,一般不會輕易將session會話invalid,所以最好不要直接使用NSURLSession锦茁,而是要對其進行一次二次封裝攘轩,使用AFN3.0的原因之一也在于此。
文件上傳
表單上傳
客戶端有時候需要給服務(wù)端上傳大文件码俩,進行大文件肯定不能全都加載到內(nèi)存里度帮,一口氣都傳給服務(wù)器。進行大文件上傳時,一般都會對需要上傳的文件進行分片笨篷,分片后逐個文件進行上傳瞳秽。需要注意的是,分片上傳和斷點續(xù)傳并不是同一個概念率翅,上傳并不支持斷點續(xù)傳练俐。
進行分片上傳時,需要對本地文件進行讀取冕臭,我們使用NSFileHandle來進行文件讀取腺晾。NSFileHandle提供了一個偏移量的功能,我們可以將handle的當前讀取位置seek到上次讀取的位置辜贵,并設(shè)置本次讀取長度悯蝉,讀取的文件就是我們指定文件的字節(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對表單上傳也有很有的支持厚棵。表單上傳需要遵循下面的格式進行上傳蕉世,boundary是一個16進制字符串,可以是任何且唯一的窟感。boundary的功能用來進行字段分割讨彼,區(qū)分開不同的參數(shù)部分。
multipart/from-data規(guī)范定義在rfc2388柿祈,詳細字段可以看一下規(guī)范哈误。
--boundary
Content-Disposition:?form-data;?name="參數(shù)名"
參數(shù)值
--boundary
Content-Disposition:form-data;name=”表單控件名”;filename=”上傳文件名”
Content-Type:mime?type
要上傳文件二進制數(shù)據(jù)
--boundary--
拼接上傳文件基本上可以分為下面三部分,上傳參數(shù)、上傳信息站蝠、上傳文件奥务。并且通過UTF-8格式進行編碼,服務(wù)端也采用相同的解碼方式重荠,則可以獲得上傳文件和信息。需要注意的是虚茶,換行符數(shù)量是固定的戈鲁,這都是固定的協(xié)議格式,不要多或者少嘹叫,會導(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è)置請求頭的Content-Type和Content-Length罩扇,否則會導(dǎo)致請求失敗婆芦。其中Content-Length并不是強制要求的怕磨,要看后端的具體支持情況。
設(shè)置請求頭時消约,一定要加上boundary肠鲫,這個boundary和拼接上傳文件的boundary需要是同一個。服務(wù)端從請求頭拿到boundary或粮,來解析上傳文件导饲。
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"];
隨后我們通過下面的代碼創(chuàng)建NSURLSessionUploadTask,并調(diào)用resume發(fā)起請求被啼,實現(xiàn)對應(yīng)的代理回調(diào)即可帜消。
//?發(fā)起網(wǎng)絡(luò)請求
NSURLSessionUploadTask*uploadTask?=?[self.backgroundSession?uploadTaskWithRequest:request?fromData:fromData];
[uploadTask?resume];
//?請求完成后調(diào)用,無論成功還是失敗
-?(void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error?{
}
//?更新上傳進度浓体,會回調(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?{
}
//?處理后臺上傳任務(wù)泡挺,當前session的上傳任務(wù)結(jié)束后會回調(diào)此方法。
-?(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession*)session?{
}
但是命浴,如果你認為這就完成一個上傳功能了娄猫,too young too simple~
后臺上傳
如果通過fromData的方式進行上傳,并不支持后臺上傳生闲。如果想實現(xiàn)后臺上傳媳溺,需要通過fromFile的方式上傳文件。不止如此碍讯,fromData還有其他坑悬蔽。
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromData:(NSData*)bodyData;
-?(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request?fromFile:(NSURL*)fileURL;
內(nèi)存占用
我們發(fā)現(xiàn)通過fromData:的方式上傳文件,內(nèi)存漲上去之后一直不能降下來捉兴,無論是直接使用NSURLSession還是AFNetworking蝎困,都是這樣的。小文件還好倍啥,不是很明顯禾乘,如果是幾百MB的大文件很明顯就會有一個內(nèi)存峰值,而且漲上去就不會降下來虽缕。WTF始藕?
上傳有兩種方式上傳,如果我們把fromData:的上傳改為fromFile:氮趋,就可以解決內(nèi)存不下降的問題伍派。所以,我們可以把fromData:的上傳方式剩胁,理解為UIImage的imageNamed的方法拙已,上傳后NSData文件會保存在內(nèi)存中,不會被回收摧冀。而fromFile:的方式是從本地加載文件,并且上傳完成后可以被回收。而且如果想支持后臺上傳索昂,就必須用fromFile:的方式進行上傳建车。
OK,那找到問題我們就開干椒惨,改變之前的上傳邏輯缤至,改為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;
}
因為要通過fromFile:方法傳一個本地分片的路徑進去康谆,所以需要預(yù)先對文件進行分片领斥,并保存在本地。在分片的同時沃暗,還需要拼接boundary信息月洛。
所以我們在上傳任務(wù)開始前,先對文件進行分片并拼接信息孽锥,然后將分片文件寫入到本地嚼黔。為了方便管理,我們基于具有唯一性的文件名進行MD5來創(chuàng)建分片文件夾惜辑,分片文件命名通過下標來命名唬涧,并寫入到本地。文件上傳完成后盛撑,直接刪除整個文件夾即可碎节。當然,這些文件操作都是在異步線程中完成的抵卫,防止影響UI線程狮荔。
我們用一個400MB的視頻測試上傳,我們可以從上圖看出陌僵,圈紅部分是我們上傳文件的時間轴合。將上傳方式改為fromFile:后,上傳文件的峰值最高也就是在10MB左右徘徊碗短,這對于iPhone6這樣的低內(nèi)存老年機來說受葛,是相當友好的,不會導(dǎo)致低端設(shè)備崩潰或者卡頓偎谁。
動態(tài)分片
用戶在上傳時網(wǎng)絡(luò)環(huán)境會有很多情況总滩,WiFi、4G巡雨、弱網(wǎng)等很多情況闰渔。如果上傳分片太大可能會導(dǎo)致失敗率上升,分片文件太小會導(dǎo)致網(wǎng)絡(luò)請求太多铐望,產(chǎn)生太多無用的boundary冈涧、header茂附、數(shù)據(jù)鏈路等資源的浪費。
為了解決這個問題督弓,我們采取的是動態(tài)分片大小的策略营曼。根據(jù)特定的計算策略,預(yù)先使用第一個分片的上傳速度當做測速分片愚隧,測速分片的大小是固定的蒂阱。根據(jù)測速的結(jié)果,對其他分片大小進行動態(tài)分片狂塘,這樣可以保證分片大小可以最大限度的利用當前網(wǎng)速录煤。
if([Reachability?reachableViaWiFi])?{
self.segmentSize?=500*1024;
}elseif([Reachability?reachableViaWWAN])?{
self.segmentSize?=300*1024;
}
當然,如果覺得這種分片方式太過復(fù)雜荞胡,也可以采取一種閹割版的動態(tài)分片策略妈踊。即根據(jù)網(wǎng)絡(luò)情況做判斷,如果是WiFi就固定某個分片大小硝训,如果是流量就固定某個分片大小响委。然而這種策略并不穩(wěn)定,因為現(xiàn)在很多手機的網(wǎng)速比WiFi還快窖梁,我們也不能保證WiFi都是百兆光纖赘风。
并行上傳
上傳的所有任務(wù)如果使用的都是同一個NSURLSession的話,是可以保持連接的纵刘,省去建立和斷開連接的消耗邀窃。在iOS平臺上,NSURLSession支持對一個Host保持4個連接假哎,所以瞬捕,如果我們采取并行上傳,可以更好的利用當前的網(wǎng)絡(luò)舵抹。
并行上傳的數(shù)量在iOS平臺上不要超過4個肪虎,最大連接數(shù)是可以通過NSURLSessionConfiguration設(shè)置的,而且數(shù)量最好不要寫死惧蛹。同樣的扇救,應(yīng)該基于當前網(wǎng)絡(luò)環(huán)境,在上傳任務(wù)開始的時候就計算好最大連接數(shù)香嗓,并設(shè)置給Configuration迅腔。
經(jīng)過我們的線上用戶數(shù)據(jù)分析,在線上環(huán)境使用并行任務(wù)的方式上傳靠娱,上傳速度相較于串行上傳提升四倍左右沧烈。計算方式是每秒文件上傳的大小。
iPhone串行上傳:715 kb/s
iPhone并行上傳:2909 kb/s
隊列管理
分片上傳過程中可能會因為網(wǎng)速等原因像云,導(dǎo)致上傳失敗锌雀。失敗的任務(wù)應(yīng)該由單獨的隊列進行管理蚂夕,并且在合適的時機進行失敗重傳。
例如對一個500MB的文件進行分片腋逆,每片是300KB双抽,就會產(chǎn)生1700多個分片文件,每一個分片文件就對應(yīng)一個上傳任務(wù)闲礼。如果在進行上傳時,一口氣創(chuàng)建1700多個uploadTask铐维,盡管NSURLSession是可以承受的柬泽,也不會造成一個很大的內(nèi)存峰值。但是我覺得這樣并不太好嫁蛇,實際上并不會同時有這么多請求發(fā)出锨并。
///?已上傳成功片段數(shù)組
@property(nonatomic,strong)NSMutableArray*successSegments;
///?待上傳隊列的數(shù)組
@property(nonatomic,strong)NSMutableArray*uploadSegments;
所以在創(chuàng)建上傳任務(wù)時,我設(shè)置了一個最大任務(wù)數(shù)睬棚,就是同時向NSURLSession發(fā)起的請求不會超過這個數(shù)量第煮。需要注意的是,這個最大任務(wù)數(shù)是我創(chuàng)建uploadTask的任務(wù)數(shù)抑党,并不是最大并發(fā)數(shù)包警,最大并發(fā)數(shù)由NSURLSession來控制,我不做干預(yù)底靠。
我將待上傳任務(wù)都放在uploadSegments中害晦,上傳成功后我會從待上傳任務(wù)數(shù)組中取出一條或多條,并保證同時進行的任務(wù)始終不超過最大任務(wù)數(shù)暑中。失敗的任務(wù)理論上來說也是需要等待上傳的壹瘟,所以我把失敗任務(wù)也放在uploadSegments中,插入到隊列最下面鳄逾,這樣就保證了待上傳任務(wù)完成后稻轨,繼續(xù)重試失敗任務(wù)。
成功的任務(wù)我放在successSegments中雕凹,并且始終保持和uploadSegments沒有交集殴俱。兩個隊列中保存的并不是uploadTask,而是分片的索引请琳,這也就是為什么我給分片命名的時候用索引當做名字的原因粱挡。當successSegments等于分片數(shù)量時,就表示所有任務(wù)上傳完成俄精。
文件下載
NSURLSession是在單獨的進程中運行询筏,所以通過此類發(fā)起的網(wǎng)絡(luò)請求,是獨立于應(yīng)用程序運行的竖慧,即使App掛起嫌套、kill也不會停止請求逆屡。在下載任務(wù)時會比較明顯,即便App被kill下載任務(wù)仍然會繼續(xù)踱讨,并且允許下次啟動App使用這次的下載結(jié)果或繼續(xù)下載魏蔗。
和上傳代碼一樣,創(chuàng)建下載任務(wù)很簡單痹筛,通過NSURLSession創(chuàng)建一個downloadTask莺治,并調(diào)用resume即可開啟一個下載任務(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需要是成對的。但是suspend掛起任務(wù)是有超時的滋早,默認為60s榄审,如果超時系統(tǒng)會將TCP連接斷開,我們再調(diào)用resume是失效的杆麸「榻可以通過NSURLSessionConfiguration的timeoutIntervalForResource來設(shè)置上傳和下載的資源耗時。suspend只針對于下載任務(wù)昔头,其他任務(wù)掛起后將會重新開始饼问。
下面兩個方法是下載比較基礎(chǔ)的方法,分別用來接收下載進度和下載完的臨時文件地址减细。didFinishDownloadingToURL:方法是required匆瓜,當下載結(jié)束后下載文件被寫入在Library/Caches下的一個臨時文件,我們需要將此文件移動到自己的目錄未蝌,臨時目錄在未來的一個時間會被刪掉驮吱。
//?從服務(wù)器接收數(shù)據(jù),下載進度回調(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?{
}
斷點續(xù)傳
HTTP協(xié)議支持斷點續(xù)傳操作萧吠,在開始下載請求時通過請求頭設(shè)置Range字段左冬,標示從什么位置開始下載。
Range:bytes=512000-
服務(wù)端收到客戶端請求后纸型,開始從512kb的位置開始傳輸數(shù)據(jù)拇砰,并通過Content-Range字段告知客戶端傳輸數(shù)據(jù)的起始位置。
Content-Range:bytes?512000-/1024000
downloadTask任務(wù)開始請求后狰腌,可以調(diào)用cancelByProducingResumeData:方法可以取消下載除破,并且可以獲得一個resumeData,resumeData中存放一些斷點下載的信息琼腔」宸悖可以將resumeData寫到本地,后面通過這個文件可以進行斷點續(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ù)前光坝,可以判斷當前任務(wù)有沒有之前待恢復(fù)的任務(wù)尸诽,如果有的話調(diào)用downloadTaskWithResumeData:方法并傳入一個resumeData,可以恢復(fù)之前的下載盯另,并重新創(chuàng)建一個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];
通過suspend和resume這種方式掛起的任務(wù),downloadTask是同一個對象鸳惯,而通過cancel然后resumeData恢復(fù)的任務(wù)商蕴,會創(chuàng)建一個新的downloadTask任務(wù)。
當調(diào)用downloadTaskWithResumeData:方法恢復(fù)下載后芝发,會回調(diào)下面的方法究恤。回調(diào)參數(shù)fileOffset是上次文件的下載大小后德,expectedTotalBytes是預(yù)估的文件總大小。
-(void)URLSession:(NSURLSession*)sessiondownloadTask:(NSURLSessionDownloadTask*)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
后臺下載
通過backgroundSessionConfigurationWithIdentifier方法創(chuàng)建后臺上傳或后臺下載類型的NSURLSessionConfiguration抄腔,并且設(shè)置一個唯一標識瓢湃,需要保證這個標識在不同的session之間的唯一性。后臺任務(wù)只支持http和https的任務(wù)赫蛇,其他協(xié)議的任務(wù)并不支持绵患。
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSessionsessionWithConfiguration:config?delegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
通過backgroundSessionConfigurationWithIdentifier方法創(chuàng)建的NSURLSession,請求任務(wù)將會在系統(tǒng)的單獨進程中進行悟耘,因此即使App進程被kill也不受影響落蝙,依然可以繼續(xù)執(zhí)行請求任務(wù)。如果程序被系統(tǒng)kill調(diào)暂幼,下次啟動并執(zhí)行didFinishLaunchingWithOptions可以通過相同的identifier創(chuàng)建NSURLSession和NSURLSessionConfiguration筏勒,系統(tǒng)會將新創(chuàng)建的NSURLSession和單獨進程中正在運行的NSURLSession進行關(guān)聯(lián)。
在程序啟動并執(zhí)行didFinishLaunchingWithOptions方法時旺嬉,按照下面方法創(chuàng)建NSURLSession即可將新創(chuàng)建的Session和之前的Session綁定管行,并自動開始執(zhí)行之前的下載任務(wù)⌒跋保恢復(fù)之前的任務(wù)后會繼續(xù)執(zhí)行NSURLSession的代理方法捐顷,并執(zhí)行后面的任務(wù)。
-?(BOOL)application:(UIApplication*)application?didFinishLaunchingWithOptions:(NSDictionary*)launchOptions?{
NSURLSessionConfiguration*config?=?[NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSessionsessionWithConfiguration:config?delegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
returnYES;
}
當應(yīng)用進入到后臺時雨效,可以繼續(xù)下載迅涮,如果客戶端沒有開啟Background Mode,則不會回調(diào)客戶端進度徽龟。下次進入前臺時叮姑,會繼續(xù)回調(diào)新的進度。
如果在后臺下載完成顿肺,則會通過AppDelegate的回調(diào)方法通知應(yīng)用來刷新UI戏溺。由于下載是在一個單獨的進程中完成的渣蜗,即便業(yè)務(wù)層代碼會停止執(zhí)行,但下載的回調(diào)依然會被調(diào)用旷祸。在回調(diào)時耕拷,允許用戶處理業(yè)務(wù)邏輯,以及刷新UI托享。
調(diào)用此方法后可以開始刷新UI骚烧,調(diào)用completionHandler表示刷新結(jié)束,所以上層業(yè)務(wù)要做一些控制邏輯闰围。didFinishDownloadingToURL的調(diào)用時機會比此方法要晚赃绊,依然在那個方法里可以判斷下載文件。由于項目中可能會存在多個下載任務(wù)羡榴,所以需要通過identifier對下載任務(wù)進行區(qū)分碧查。
-?(void)application:(UIApplication*)application?handleEventsForBackgroundURLSession:(NSString*)identifier?completionHandler:(void(^)(void))completionHandler?{
ViewController?*vc?=?(ViewController?*)self.window.rootViewController;
vc.completionHandler?=?completionHandler;
}
需要注意的是,如果存在多個相同名字的identifier任務(wù)校仑,則創(chuàng)建的session會將同名的任務(wù)都繼續(xù)執(zhí)行忠售。NSURLSessionConfiguration還提供下面的屬性,在session下載任務(wù)完成時是否啟動App迄沫,默認為YES稻扬,如果設(shè)置為NO則后臺下載會受到影響。
@propertyBOOL?sessionSendsLaunchEvents;
后臺下載過程中會設(shè)計到一系列的代理方法調(diào)用羊瘩,下面是調(diào)用順序泰佳。
視頻文件下載
現(xiàn)在很多視頻類App都有視頻下載的功能,視頻下載肯定不會是單純的把一個mp4下載下來就可以尘吗,這里就講一下視頻下載相關(guān)的知識逝她。
視頻地址一般都是從服務(wù)端獲取的,所以需要先請求接口獲取下載地址睬捶。這個地址可以是某個接口就已經(jīng)請求下來的汽绢,也可以是某個固定格式拼接的。
現(xiàn)在有很多視頻App都是有免流服務(wù)的侧戴,例如騰訊大王卡宁昭、螞蟻寶卡之類的,免流服務(wù)的本質(zhì)就是對m3u8酗宋、ts积仗、mp4地址重新包一層,請求數(shù)據(jù)的時候直接請求運營商給的地址蜕猫,運營商對數(shù)據(jù)做了一個中轉(zhuǎn)操作寂曹。
以流視頻m3u8為例,有了免流地址,先下載m3u8文件隆圆。這個文件一般都是加密的漱挚,下載完成后客戶端會對m3u8文件進行decode,獲取到真正的m3u8文件渺氧。
m3u8文件本質(zhì)上是ts片段的集合旨涝,視頻播放播的還是ts片段。隨后對m3u8文件進行解析侣背,獲取到ts片段地址白华,并將ts下載地址轉(zhuǎn)成免流地址后逐個下載,也可以并行下載贩耐。
m3u8文件下載后會以固定格式存在文件夾下弧腥,文件夾對應(yīng)被緩存的視頻。ts片命名以數(shù)字命名潮太,例如0.ts管搪,下標從0開始。
所有ts片段下載完成后铡买,生成本地m3u8文件抛蚤。
m3u8文件分為遠端和本地兩種,遠端的就是正常下載的地址寻狂,本地m3u8文件是在播放本地視頻的時候傳入。格式和普通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文件的原因是因為多個ts可以無縫拼接筋夏,并且單個ts可以單獨播放蒂胞。而mp4由于格式原因,被分割的mp4文件單獨播放會導(dǎo)致畫面撕裂或者音頻缺失的問題条篷。如果單獨下載多個mp4文件骗随,播放時會導(dǎo)致間斷的問題。
m3u8是Unicode版本的m3u赴叹,是蘋果推出的一種視頻格式鸿染,是一個基于HTTP的流媒體傳輸協(xié)議。m3u8協(xié)議將一個媒體文件切為多個小文件乞巧,并利用HTTP協(xié)議進行數(shù)據(jù)傳輸涨椒,小文件所在的資源服務(wù)器路徑存儲在.m3u8文件中。客戶端拿到m3u8文件蚕冬,即可根據(jù)文件中資源文件的路徑免猾,分別下載不同的文件。
m3u8文件必須是utf-8格式編碼的囤热,在文件中以#EXT開頭的是標簽猎提,并且大小寫敏感。以#開頭的其他字符串則都會被認為是注釋赢乓。m3u8分為點播和直播忧侧,點播在第一次請求.m3u8文件后,將下載下來的ts片段進行順序播放即可牌芋。直播則需要過一段時間對.m3u8文件進行一個增量下載蚓炬,并繼續(xù)下載后續(xù)的ts文件。
m3u8中有很多標簽躺屁,下面是項目中用到的一些標簽或主要標簽肯夏。將mp4或者flv文件進行切片很簡單,直接用ffmpeg命令切片即可犀暑。
起始標簽驯击,此標簽必須在整個文件的開頭。
#EXTM3U
結(jié)束標簽耐亏,此標簽必須在整個文件的末尾徊都。
#EXT-X-ENDLIST
當前文件版本,如果不指定則默認為1
#EXT-X-VERSION
所有ts片段最大時長广辰。
#EXT-X-TARGETDURATION
當前ts片段時長暇矫。
#EXTINF
如果沒有#EXT或#開頭的,一般都是ts片段下載地址择吊。路徑可以是絕對路徑李根,也可以是相對路徑,我們項目里使用的是絕對路徑几睛。但相對路徑數(shù)據(jù)量會相對比較小房轿,只不過看視頻的人網(wǎng)速不會太差。
下面是相對路徑地址所森,文件中只有segment1.ts囱持,則表示相對于m3u8的路徑,也就是下面的路徑焕济。
https://data.vod.itc.cn/m3u8
https://data.vod.itc.cn/segment1.ts
常見錯誤
A?background?URLSessionwithidentifier?backgroundSession?already?exists
如果重復(fù)后臺已經(jīng)存在的下載任務(wù)洪唐,會提示這個錯誤。需要在頁面退出或程序退出時吼蚁,調(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];
}
}];
}
文章來源于搜狐技術(shù)產(chǎn)品?问欠,作者劉小壯,轉(zhuǎn)發(fā)至知識小集粒蜈。