NSURLSession最全攻略

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ā)至知識小集粒蜈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末顺献,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子枯怖,更是在濱河造成了極大的恐慌注整,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件度硝,死亡現(xiàn)場離奇詭異肿轨,居然都是意外死亡,警方通過查閱死者的電腦和手機蕊程,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門椒袍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人藻茂,你說我怎么就攤上這事驹暑。” “怎么了辨赐?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵优俘,是天一觀的道長。 經(jīng)常有香客問我掀序,道長帆焕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任不恭,我火速辦了婚禮叶雹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘县袱。我一直安慰自己,他們只是感情好佑力,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布式散。 她就那樣靜靜地躺著,像睡著了一般打颤。 火紅的嫁衣襯著肌膚如雪暴拄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天编饺,我揣著相機與錄音乖篷,去河邊找鬼。 笑死透且,一個胖子當著我的面吹牛撕蔼,可吹牛的內(nèi)容都是我干的豁鲤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼鲸沮,長吁一口氣:“原來是場噩夢啊……” “哼琳骡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起讼溺,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤楣号,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后怒坯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炫狱,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年剔猿,在試婚紗的時候發(fā)現(xiàn)自己被綠了视译。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡艳馒,死狀恐怖憎亚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弄慰,我是刑警寧澤第美,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站陆爽,受9級特大地震影響什往,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜慌闭,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一别威、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驴剔,春花似錦省古、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至布讹,卻和暖如春琳拭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背描验。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工白嘁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人膘流。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓絮缅,卻偏偏與公主長得像鲁沥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盟蚣,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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

  • 在蘋果徹底棄用NSURLConnection之后自己總結(jié)的一個網(wǎng)上的內(nèi)容黍析,加上自己寫的小Demo,很多都是借鑒網(wǎng)絡(luò)...
    付寒宇閱讀 4,278評論 2 13
  • NSURLSession概述1. NSURLSession session類型NSURLSession包括下面3種...
    瞎嘚嘚閱讀 1,882評論 2 2
  • 我們先看一下AFNetworking.h文件都給了我們什么方法 #import <Foundation/Found...
    瀟巖閱讀 613評論 0 1
  • 今天去面試屎开,被問了一道AFNetworking2.0和3.0有什么區(qū)別阐枣,當時心想,這誰不知道啊奄抽,隨口答到:2.0使...
    萌芽的冬天閱讀 12,505評論 4 79
  • iOS開發(fā)系列--網(wǎng)絡(luò)開發(fā) 概覽 大部分應(yīng)用程序都或多或少會牽扯到網(wǎng)絡(luò)開發(fā)蔼两,例如說新浪微博、微信等逞度,這些應(yīng)用本身可...
    lichengjin閱讀 3,657評論 2 7