<簡書 — 劉小壯>數據庫
NSURLSession
在iOS7
中推出,NSURLSession
的推出旨在替換以前的NSURLConnection
,NSURLSession
的使用相對於以前的NSURLConnection
更簡單,並且不用處理Runloop
相關的東西。後端
2015年RFC 7540
標準發佈了http 2.0
版本,http 2.0
版本中包含不少新的特性,在傳輸速度上也有很明顯的提高。NSURLSession
從iOS9.0
開始,對http 2.0
提供了支持。數組
NSURLSession
由三部分構成:緩存
session
會話進行配置,通常都採用default
。task
,由session
建立。NSURLSession
有三種方式建立:安全
sharedSession
複製代碼
系統維護的一個單例對象,能夠和其餘使用這個session
的task
共享鏈接和請求信息。服務器
sessionWithConfiguration:
複製代碼
在NSURLSession初始化時傳入一個NSURLSessionConfiguration,這樣能夠自定義請求頭、cookie等信息。cookie
sessionWithConfiguration:delegate:delegateQueue:
複製代碼
若是想更好的控制請求過程以及回調線程,須要上面的方法進行初始化操做,並傳入delegate
來設置回調對象和回調的線程。網絡
經過NSURLSession
發起一個網絡請求也比較簡單。session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[task resume];
複製代碼
經過NSURLSession
發起的每一個請求,都會被封裝爲一個NSURLSessionTask
任務,但通常不會直接是NSURLSessionTask
類,而是基於不一樣任務類型,被封裝爲其對應的子類。併發
Get
、Post
請求。cancel
方法。主要方法都定義在父類NSURLSessionTask
中,下面是一些關鍵方法或屬性。
currentRequest
當前正在執行的任務,通常和originalRequest
是同樣的,除非發生重定向纔會有所區別。 originalRequest
主要用於重定向操做,用來記錄重定向前的請求。 taskIdentifier
當前session
下,task
的惟一標示,多個session
之間可能存在相同的標識。 priority
task
中能夠設置優先級,但這個屬性並不表明請求的優先級,而是一個標示。官方已經說明,NSURLSession
並無提供API
能夠改變請求的優先級。 state
當前任務的狀態,能夠經過KVO
的方式監聽狀態的改變。 - resume
開始或繼續請求,建立後的task
默認是掛起的,須要手動調用resume
才能夠開始請求。 - suspend
掛起當前請求。主要是下載請求用的多一些,普通請求掛起後都會從新開始請求。下載請求掛起後,只要不超過NSURLRequest
設置的timeout
時間,調用resume
就是繼續請求。 - cancel
取消當前請求。任務會被標記爲取消,並在將來某個時間調用URLSession:task:didCompleteWithError:
方法。
NSURLSession
提供有普通建立task
的方式,建立後能夠經過重寫代理方法,獲取對應的回調和參數。這種方式對於請求過程比較好控制。
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
複製代碼
除此以外,NSURLSession
也提供了block
的方式建立task
,建立方式簡單如AFN
,直接傳入URL
或NSURLRequest
,便可直接在block
中接收返回數據。和普通建立方式同樣,block
的建立方式建立後默認也是suspend
的狀態,須要調用resume
開始任務。
completionHandler
和delegate
是互斥的,completionHandler
的優先級大於delegate
。相對於普通建立方法,block
方式更偏向於面向結果的建立,能夠直接在completionHandler
中獲取返回結果,但不能控制請求過程。
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)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
對應的全部task
,方法區別在於回調的參數不一樣。以getTasksWithCompletionHandler
爲例,在AFN
中的應用是用來獲取當前session
的task
,並將AFURLSessionManagerTaskDelegate
的回調都置爲nil
,以防止崩潰。
- (void)getTasksWithCompletionHandler:(void (^)(NSArray<NSURLSessionDataTask *> *dataTasks, NSArray<NSURLSessionUploadTask *> *uploadTasks, NSArray<NSURLSessionDownloadTask *> *downloadTasks))completionHandler;
- (void)getAllTasksWithCompletionHandler:(void (^)(NSArray<__kindof NSURLSessionTask *> *tasks))completionHandler);
複製代碼
在初始化NSURLSession
時能夠指定線程,若是不指定線程,則completionHandler
和delegate
的回調方法,都會在子線程中執行。
若是初始化NSURLSession
時指定了delegateQueue
,則回調會在指定的隊列中執行,若是指定的是mainQueue
,則回調在主線程中執行,這樣就避免了切換線程的問題。
[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
複製代碼
對於NSURLSession
的代理方法這裏就不詳細列舉了,方法命名遵循蘋果一向見名知意的原則,用起來很簡單。這裏介紹一下NSURLSession
的代理繼承結構。
NSURLSession
中定義了一系列代理,並遵循上面的繼承關係。根據繼承關係和代理方法的聲明,若是執行某項任務,只須要遵照其中的某個代理便可。
例如執行上傳或普通Post
請求,則遵照NSURLSessionDataDelegate
,執行下載任務則遵循NSURLSessionDownloadDelegate
,父級代理定義的都是公共方法。
HTTP
協議中定義了例如301等重定向狀態碼,經過下面的代理方法,能夠處理重定向任務。發生重定向時能夠根據response
建立一個新的request
,也能夠直接用系統生成的request
,並在completionHandler
回調中傳入,若是想終止此次重定向,在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
負責對NSURLSession
初始化時進行配置,經過NSURLSessionConfiguration
能夠設置請求的Cookie
、密鑰、緩存、請求頭等參數,將網絡請求的一些配置參數從NSURLSession
中分離出來。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
複製代碼
NSURLSessionConfiguration
提供三種初始化方法,下面是請求的方法的一些解釋。
@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;
複製代碼
NSURLSessionConfiguration
提供defaultSessionConfiguration
的方式建立,但這並非單例方法,而是類方法,建立的是不一樣對象。經過這種方式建立的configuration
,並不會共享cookie
、cache
、密鑰等,而是不一樣configuration
都須要單獨設置。
這塊網上不少人理解都是錯的,並無真的在項目裏使用或者沒有留意過,如和其餘人有出入,以我爲準。
@property (class, readonly, strong) NSURLSessionConfiguration *ephemeralSessionConfiguration;
複製代碼
建立臨時的configuration
,經過這種方式建立的對象,和普通的對象主要區別在於URLCache
、URLCredentialStorage
、HTTPCookieStorage
上面。一樣的,Ephemeral
也不是單例方法,而只是類方法。
URLCredentialStorage
Ephemeral <__NSCFMemoryURLCredentialStorage: 0x600001bc8320>
HTTPCookieStorage
Ephemeral <NSHTTPCookieStorage cookies count:0>
複製代碼
若是對Ephemeral
方式建立的config
進行打印的話,能夠看到變量類型明顯區別於其餘類型,而且在打印信息前面會有Ephemeral
的標示。經過Ephemeral
的方式建立的config
,不會產生持久化信息,能夠很好保護請求的數據安全性。
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;
複製代碼
identifier
方式通常用於恢復以前的任務,主要用於下載。若是一個下載任務正在進行中,程序被kill
調,能夠在程序退出以前保存identifier
。下次進入程序後經過identifier
恢復以前的任務,系統會將NSURLSession
及NSURLSessionConfiguration
和以前的下載任務進行關聯,並繼續以前的任務。
timeoutIntervalForRequest
複製代碼
設置session
請求間的超時時間,這個超時時間並非請求從開始到結束的時間,而是兩個數據包之間的時間間隔。當任意請求返回後這個值將會被重置,若是在超時時間內未返回則超時。單位爲秒,默認爲60秒。
timeoutIntervalForResource
複製代碼
資源超時時間,通常用於上傳或下載任務,在上傳或下載任務開始後計時,若是到達時間任務未結束,則刪除資源文件。單位爲秒,默認時間是七天。
若是是相同的NSURLSessionConfiguration
對象,會共享請求頭、緩存、cookie
、Credential
,經過Configuration
建立的NSURLSession
,也會擁有對應的請求信息。
@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;
複製代碼
公共請求頭,默認是空的,設置後全部經Confuguration
配置的NSURLSession
,請求頭都會帶有設置的信息。
@property (nullable, retain) NSHTTPCookieStorage *HTTPCookieStorage;
複製代碼
HTTP
請求的Cookie
管理器。若是是經過sharedSession
或backgroundConfiguration
建立的NSURLSession
,默認使用sharedHTTPCookieStorage
的Cookie
數據。若是不想使用Cookie
,則直接設置爲nil
便可,也能夠手動設置爲本身的CookieStorage
。
@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;
複製代碼
證書管理器。若是是經過sharedSession
或backgroundConfiguration
建立的NSURLSession
,默認使用sharedCredentialStorage
的證書。若是不想使用證書,能夠直接設置爲nil
,也能夠本身建立證書管理器。
@property (nullable, retain) NSURLCache *URLCache;
複製代碼
請求緩存,若是不手動設置的話爲nil
,對於NSURLCache
這個類我沒有研究過,不太瞭解。
在NSURLRequest
中能夠設置cachePolicy
請求緩存策略,這裏不對具體值作詳細描述,默認值爲NSURLRequestUseProtocolCachePolicy
使用緩存。
NSURLSessionConfiguration
能夠設置處理緩存的對象,咱們能夠手動設置自定義的緩存對象,若是不設置的話,默認使用系統的sharedURLCache
單例緩存對象。通過configuration
建立的NSURLSession
發出的請求,NSURLRequest
都會使用這個NSURLCache
來處理緩存。
@property (nullable, retain) NSURLCache *URLCache;
複製代碼
NSURLCache
提供了Memory
和Disk
的緩存,在建立時須要爲其分別指定Memory
和Disk
的大小,以及存儲的文件位置。使用NSURLCache
不用考慮磁盤空間不夠,或手動管理內存空間的問題,若是發生內存警告系統會自動清理內存空間。可是NSURLCache
提供的功能很是有限,項目中通常不多直接使用它來處理緩存數據,仍是用數據庫比較多。
[[NSURLCache alloc] initWithMemoryCapacity:30 * 1024 * 1024
diskCapacity:30 * 1024 * 1024
directoryURL:[NSURL URLWithString:filePath]];
複製代碼
使用NSURLCache
還有一個好處,就是能夠由服務端來設置資源過時時間,在請求服務端後,服務端會返回Cache-Control
來講明文件的過時時間。NSURLCache
會根據NSURLResponse
來自動完成過時時間的設置。
限制NSURLSession
的最大鏈接數,經過此方法建立的NSURLSession
和服務端的最大鏈接數量不會超出這裏設置的數量。蘋果爲咱們設置的iOS
端默認爲4,Mac
端默認爲6。
@property NSInteger HTTPMaximumConnectionsPerHost;
複製代碼
HTTP
是基於傳輸層協議TCP
的,經過TCP
發送網絡請求都須要先進行三次握手,創建網絡請求後再發送數據,請求結束時再經歷四次揮手。HTTP1.0
開始支持keep-alive
,keep-alive
能夠保持已經創建的連接,若是是相同的域名,在請求鏈接創建後,後面的請求不會馬上斷開,而是複用現有的鏈接。從HTTP1.1
開始默認開啓keep-alive
。
請求是在請求頭中設置下面的參數,服務器若是支持keep-alive
的話,響應客戶端請求時,也會在響應頭中加上相同的字段。
Connection: Keep-Alive
複製代碼
若是想斷開keep-alive
,能夠在請求頭中加上下面的字段,但通常不推薦這麼作。
Connection: Close
複製代碼
若是經過NSURLSession
來進行網絡請求的話,須要使用同一個NSURLSession
對象,若是建立新的session
對象則不能複用以前的連接。keep-alive
能夠保持請求的鏈接,蘋果容許在iOS
上最大保持有4個鏈接,Mac
則是6個鏈接。
在HTTP1.1
中,基於keep-alive
,還能夠將請求進行管線化。和相同後端服務,TCP
層創建的連接,通常都須要前一個請求返回後,後面的請求再發出。但pipeline
就能夠不依賴以前請求的響應,而發出後面的請求。
pipeline
依賴客戶端和服務器都有實現,服務端收到客戶端的請求後,要按照先進先出的順序進行任務處理和響應。pipeline
依然存在以前非pipeline
的問題,就是前面的請求若是出現問題,會阻塞當前鏈接影響後面的請求。
pipeline
對於請求大文件並無提高做用,只是對於普通請求速度有提高。在NSURLSessionConfiguration
中能夠設置HTTPShouldUsePipelining
爲YES
,開啓管線化,此屬性默認爲NO
。
在平常開發過程當中,常常遇到頁面加載太慢的問題,這很大一部分緣由都是由於網絡致使的。因此,查找網絡耗時的緣由並解決,就是一個很重要的任務了。蘋果對於網絡檢查提供了NSURLSessionTaskMetrics
類來進行檢查,NSURLSessionTaskMetrics
是對應NSURLSessionTaskDelegate
的,每一個task結束時都會回調下面的方法,而且能夠得到一個metrics
對象。
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;
複製代碼
NSURLSessionTaskMetrics
能夠很好的幫助咱們分析網絡請求的過程,以找到耗時緣由。除了這個類以外,NSURLSessionTaskTransactionMetrics
類中承載了更詳細的數據。
@property (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transactionMetrics;
複製代碼
transactionMetrics
數組中每個元素都對應着當前task
的一個請求,通常數組中只會有一個元素,若是發生重定向等狀況,可能會存在多個元素。
@property (copy, readonly) NSDateInterval *taskInterval;
複製代碼
taskInterval
記錄了當前task
從開始請求到最後完成的總耗時,NSDateInterval
中包含了startDate
、endDate
和duration
耗時時間。
@property (assign, readonly) NSUInteger redirectCount;
複製代碼
redirectCount
記錄了重定向次數,在進行下載請求時通常都會進行重定向,來保證下載任務能由後端最合適的節點來處理。
NSURLSessionTaskTransactionMetrics
中的屬性都是用來作統計的,功能都是記錄某個值,並無邏輯上的意義。因此這裏就對一些主要的屬性作一下解釋,基本涵蓋了大部分屬性,其餘就無論了。
這張圖是我從網上扒下來的,標示了NSURLSessionTaskTransactionMetrics
的屬性在請求過程當中處於什麼位置。
// 請求對象
@property (copy, readonly) NSURLRequest *request;
// 響應對象,請求失敗可能會爲nil
@property (nullable, copy, readonly) NSURLResponse *response;
// 請求開始時間
@property (nullable, copy, readonly) NSDate *fetchStartDate;
// DNS解析開始時間
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;
// DNS解析結束時間,若是解析失敗可能爲nil
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;
// 開始創建TCP鏈接時間
@property (nullable, copy, readonly) NSDate *connectStartDate;
// 結束創建TCP鏈接時間
@property (nullable, copy, readonly) NSDate *connectEndDate;
// 開始TLS握手時間
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
// 結束TLS握手時間
@property (nullable, copy, readonly) NSDate *secureConnectionEndDate;
// 開始傳輸請求數據時間
@property (nullable, copy, readonly) NSDate *requestStartDate;
// 結束傳輸請求數據時間
@property (nullable, copy, readonly) NSDate *requestEndDate;
// 接收到服務端響應數據時間
@property (nullable, copy, readonly) NSDate *responseStartDate;
// 服務端響應數據傳輸完成時間
@property (nullable, copy, readonly) NSDate *responseEndDate;
// 網絡協議,例如http/1.1
@property (nullable, copy, readonly) NSString *networkProtocolName;
// 請求是否使用代理
@property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;
// 是否複用已有鏈接
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;
// 資源標識符,表示請求是從Cache、Push、Network哪一種類型加載的
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
// 本地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協議版本,若是是http則是0x0000
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion;
// 是否使用蜂窩數據
@property (readonly, getter=isCellular) BOOL cellular;
複製代碼
下面是我發起一個http
的下載請求,統計獲得的數據。設備是Xcode
模擬器,網絡環境是WiFi
。
(Request) <NSURLRequest: 0x600000c80380> { URL: http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4 }
(Response) <NSHTTPURLResponse: 0x600000ed9420> { 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-15 15:05:59 +0000
(Domain Lookup Start) 2019-12-15 15:05:59 +0000
(Domain Lookup End) 2019-12-15 15:05:59 +0000
(Connect Start) 2019-12-15 15:05:59 +0000
(Secure Connection Start) (null)
(Secure Connection End) (null)
(Connect End) 2019-12-15 15:05:59 +0000
(Request Start) 2019-12-15 15:05:59 +0000
(Request End) 2019-12-15 15:05:59 +0000
(Response Start) 2019-12-15 15:05:59 +0000
(Response End) 2019-12-15 15: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
複製代碼
在初始化NSURLSession
對象並設置代理後,代理對象將會被強引用。根據蘋果官方的註釋來看,這個強持有並不會一直存在,而是在調用URLSession:didBecomeInvalidWithError:
方法後,會將delegate
釋放。
經過調用NSURLSession
的invalidateAndCancel
或finishTasksAndInvalidate
方法,便可將強引用斷開並執行didBecomeInvalidWithError:
代理方法,執行完成後session
就會無效不可使用。也就是隻有在session
無效時,才能夠解除強引用的關係。
有時候爲了保證鏈接複用等問題,通常不會輕易將session
會話invalid
,因此最好不要直接使用NSURLSession
,而是要對其進行一次二次封裝,使用AFN3.0
的緣由之一也在於此。
客戶端有時候須要給服務端上傳大文件,進行大文件確定不能全都加載到內存裏,一口氣都傳給服務器。進行大文件上傳時,通常都會對須要上傳的文件進行分片,分片後逐個文件進行上傳。須要注意的是,分片上傳和斷點續傳並非同一個概念,上傳並不支持斷點續傳。
進行分片上傳時,須要對本地文件進行讀取,咱們使用NSFileHandle
來進行文件讀取。NSFileHandle
提供了一個偏移量的功能,咱們能夠將handle
的當前讀取位置seek
到上次讀取的位置,並設置本次讀取長度,讀取的文件就是咱們指定文件的字節。
- (NSData *)readNextBuffer {
if (self.maxSegment <= self.currentIndex) {
return nil;
}
if(!self.fileHandler){
NSString *filePath = [self uploadFile];
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
self.fileHandler = fileHandle;
}
[self.fileHandler seekToFileOffset:(self.currentIndex) * self.segmentSize];
NSData *data = [self.fileHandler readDataOfLength:self.segmentSize];
return data;
}
複製代碼
上傳文件如今主流的方式,都是採起表單上傳的方式,也就是multipart/from-data
,AFNetworking
對錶單上傳也有頗有的支持。表單上傳須要遵循下面的格式進行上傳,boundary
是一個16進制字符串,能夠是任何且惟一的。boundary
的功能用來進行字段分割,區分開不一樣的參數部分。
multipart/from-data
規範定義在rfc2388,詳細字段能夠看一下規範。
--boundary
Content-Disposition: form-data; name="參數名"
參數值
--boundary
Content-Disposition:form-data;name=」表單控件名」;filename=」上傳文件名」
Content-Type:mime type
要上傳文件二進制數據
--boundary--
複製代碼
拼接上傳文件基本上能夠分爲下面三部分,上傳參數、上傳信息、上傳文件。而且經過UTF-8
格式進行編碼,服務端也採用相同的解碼方式,則能夠得到上傳文件和信息。須要注意的是,換行符數量是固定的,這都是固定的協議格式,不要多或者少,會致使服務端解析失敗。
- (NSData *)writeMultipartFormData:(NSData *)data
parameters:(NSDictionary *)parameters {
if (data.length == 0) {
return nil;
}
NSMutableData *formData = [NSMutableData data];
NSData *lineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];
NSData *boundary = [kBoundary dataUsingEncoding:NSUTF8StringEncoding];
// 拼接上傳參數
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
[formData appendData:boundary];
[formData appendData:lineData];
NSString *thisFieldString = [NSString stringWithFormat:@"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 = [NSString stringWithFormat:@"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: [[NSString stringWithFormat:@"--%@--\r\n", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
return formData;
}
複製代碼
除此以外,表單提交還須要設置請求頭的Content-Type
和Content-Length
,不然會致使請求失敗。其中Content-Length
並非強制要求的,要看後端的具體支持狀況。
設置請求頭時,必定要加上boundary
,這個boundary
和拼接上傳文件的boundary
須要是同一個。服務端從請求頭拿到boundary
,來解析上傳文件。
NSString *headerField = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", kBoundary];
[request setValue:headerField forHTTPHeaderField:@"Content-Type"];
NSUInteger size = [[[NSFileManager defaultManager] attributesOfItemAtPath:uploadPath error:nil] fileSize];
headerField = [NSString stringWithFormat:@"%lu", size];
[request setValue:headerField forHTTPHeaderField:@"Content-Length"];
複製代碼
隨後咱們經過下面的代碼建立NSURLSessionUploadTask
,並調用resume
發起請求,實現對應的代理回調便可。
// 發起網絡請求
NSURLSessionUploadTask *uploadTask = [self.backgroundSession uploadTaskWithRequest:request fromData:fromData];
[uploadTask resume];
// 請求完成後調用,不管成功仍是失敗
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
}
// 更新上傳進度,會回調屢次
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
}
// 數據接收完成回調
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
}
// 處理後臺上傳任務,當前session的上傳任務結束後會回調此方法。
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
}
複製代碼
可是,若是你認爲這就完成一個上傳功能了,too young too simple~
若是經過fromData
的方式進行上傳,並不支持後臺上傳。若是想實現後臺上傳,須要經過fromFile
的方式上傳文件。不止如此,fromData
還有其餘坑。
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
複製代碼
咱們發現經過fromData:
的方式上傳文件,內存漲上去以後一直不能降下來,不管是直接使用NSURLSession
仍是AFNetworking
,都是這樣的。小文件還好,不是很明顯,若是是幾百MB的大文件很明顯就會有一個內存峯值,並且漲上去就不會降下來。WTF?
上傳有兩種方式上傳,若是咱們把fromData:
的上傳改成fromFile:
,就能夠解決內存不降低的問題。因此,咱們能夠把fromData:
的上傳方式,理解爲UIImage
的imageNamed
的方法,上傳後NSData
文件會保存在內存中,不會被回收。而fromFile:
的方式是從本地加載文件,而且上傳完成後能夠被回收。並且若是想支持後臺上傳,就必須用fromFile:
的方式進行上傳。
OK,那找到問題咱們就開幹,改變以前的上傳邏輯,改成fromFile:
的方式上傳。
// 將分片寫入到本地
NSString *filePath = [NSString stringWithFormat:@"%@/%ld", [self segmentDocumentPath], currentIndex];
BOOL write = [formData writeToFile:filePath atomically:YES];
// 建立分片文件夾
- (NSString *)segmentDocumentPath {
NSString *documentName = [fileName md5String];
NSString *filePath = [[SVPUploadCompressor compressorPath] stringByAppendingPathComponent:documentName];
BOOL needCreateDirectory = YES;
BOOL isDirectory = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {
if (isDirectory) {
needCreateDirectory = NO;
} else {
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
}
if (needCreateDirectory) {
[[NSFileManager defaultManager] createDirectoryAtPath:filePath
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
return filePath;
}
複製代碼
由於要經過fromFile:
方法傳一個本地分片的路徑進去,因此須要預先對文件進行分片,並保存在本地。在分片的同時,還須要拼接boundary
信息。
因此咱們在上傳任務開始前,先對文件進行分片並拼接信息,而後將分片文件寫入到本地。爲了方便管理,咱們基於具備惟一性的文件名進行MD5
來建立分片文件夾,分片文件命名經過下標來命名,並寫入到本地。文件上傳完成後,直接刪除整個文件夾便可。固然,這些文件操做都是在異步線程中完成的,防止影響UI線程。
咱們用一個400MB的視頻測試上傳,咱們能夠從上圖看出,圈紅部分是咱們上傳文件的時間。將上傳方式改成fromFile:
後,上傳文件的峯值最高也就是在10MB左右徘徊,這對於iPhone6這樣的低內存老年機來講,是至關友好的,不會致使低端設備崩潰或者卡頓。
用戶在上傳時網絡環境會有不少狀況,WiFi
、4G、弱網等不少狀況。若是上傳分片太大可能會致使失敗率上升,分片文件過小會致使網絡請求太多,產生太多無用的boundary
、header
、數據鏈路等資源的浪費。
爲了解決這個問題,咱們採起的是動態分片大小的策略。根據特定的計算策略,預先使用第一個分片的上傳速度當作測速分片,測速分片的大小是固定的。根據測速的結果,對其餘分片大小進行動態分片,這樣能夠保證分片大小能夠最大限度的利用當前網速。
if ([Reachability reachableViaWiFi]) {
self.segmentSize = 500 * 1024;
} else if ([Reachability reachableViaWWAN]) {
self.segmentSize = 300 * 1024;
}
複製代碼
固然,若是以爲這種分片方式太過複雜,也能夠採起一種閹割版的動態分片策略。即根據網絡狀況作判斷,若是是WiFi
就固定某個分片大小,若是是流量就固定某個分片大小。然而這種策略並不穩定,由於如今不少手機的網速比WiFi
還快,咱們也不能保證WiFi
都是百兆光纖。
上傳的全部任務若是使用的都是同一個NSURLSession
的話,是能夠保持鏈接的,省去創建和斷開鏈接的消耗。在iOS
平臺上,NSURLSession
支持對一個Host
保持4個鏈接,因此,若是咱們採起並行上傳,能夠更好的利用當前的網絡。
並行上傳的數量在iOS
平臺上不要超過4個,最大鏈接數是能夠經過NSURLSessionConfiguration
設置的,並且數量最好不要寫死。一樣的,應該基於當前網絡環境,在上傳任務開始的時候就計算好最大鏈接數,並設置給Configuration
。
通過咱們的線上用戶數據分析,在線上環境使用並行任務的方式上傳,上傳速度相較於串行上傳提高四倍左右。計算方式是每秒文件上傳的大小。
iPhone串行上傳:715 kb/s
iPhone並行上傳:2909 kb/s
複製代碼
分片上傳過程當中可能會由於網速等緣由,致使上傳失敗。失敗的任務應該由單獨的隊列進行管理,而且在合適的時機進行失敗重傳。
例如對一個500MB的文件進行分片,每片是300KB,就會產生1700多個分片文件,每個分片文件就對應一個上傳任務。若是在進行上傳時,一口氣建立1700多個uploadTask
,儘管NSURLSession
是能夠承受的,也不會形成一個很大的內存峯值。可是我以爲這樣並不太好,實際上並不會同時有這麼多請求發出。
/// 已上傳成功片斷數組
@property (nonatomic, strong) NSMutableArray *successSegments;
/// 待上傳隊列的數組
@property (nonatomic, strong) NSMutableArray *uploadSegments;
複製代碼
因此在建立上傳任務時,我設置了一個最大任務數,就是同時向NSURLSession
發起的請求不會超過這個數量。須要注意的是,這個最大任務數是我建立uploadTask
的任務數,並非最大併發數,最大併發數由NSURLSession
來控制,我不作干預。
我將待上傳任務都放在uploadSegments
中,上傳成功後我會從待上傳任務數組中取出一條或多條,並保證同時進行的任務始終不超過最大任務數。失敗的任務理論上來講也是須要等待上傳的,因此我把失敗任務也放在uploadSegments
中,插入到隊列最下面,這樣就保證了待上傳任務完成後,繼續重試失敗任務。
成功的任務我放在successSegments
中,而且始終保持和uploadSegments
沒有交集。兩個隊列中保存的並非uploadTask
,而是分片的索引,這也就是爲何我給分片命名的時候用索引當作名字的緣由。當successSegments
等於分片數量時,就表示全部任務上傳完成。
NSURLSession
是在單獨的進程中運行,因此經過此類發起的網絡請求,是獨立於應用程序運行的,即便App掛起、kill
也不會中止請求。在下載任務時會比較明顯,即使App被kill
下載任務仍然會繼續,而且容許下次啓動App使用此次的下載結果或繼續下載。
和上傳代碼同樣,建立下載任務很簡單,經過NSURLSession
建立一個downloadTask
,並調用resume
便可開啓一個下載任務。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURL *url = [NSURL URLWithString:@"http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
複製代碼
咱們能夠調用suspend
將下載任務掛起,隨後調用resume
方法繼續下載任務,suspend
和resume
須要是成對的。可是suspend
掛起任務是有超時的,默認爲60s,若是超時系統會將TCP
鏈接斷開,咱們再調用resume
是失效的。能夠經過NSURLSessionConfiguration
的timeoutIntervalForResource
來設置上傳和下載的資源耗時。suspend
只針對於下載任務,其餘任務掛起後將會從新開始。
下面兩個方法是下載比較基礎的方法,分別用來接收下載進度和下載完的臨時文件地址。didFinishDownloadingToURL:
方法是required
,當下載結束後下載文件被寫入在Library/Caches
下的一個臨時文件,咱們須要將此文件移動到本身的目錄,臨時目錄在將來的一個時間會被刪掉。
// 從服務器接收數據,下載進度回調
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
CGFloat progress = (CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite;
self.progressView.progress = progress;
}
// 下載完成後回調
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
}
複製代碼
HTTP
協議支持斷點續傳操做,在開始下載請求時經過請求頭設置Range
字段,標示從什麼位置開始下載。
Range:bytes=512000-
複製代碼
服務端收到客戶端請求後,開始從512kb的位置開始傳輸數據,並經過Content-Range
字段告知客戶端傳輸數據的起始位置。
Content-Range:bytes 512000-/1024000
複製代碼
downloadTask
任務開始請求後,能夠調用cancelByProducingResumeData:
方法能夠取消下載,而且能夠得到一個resumeData
,resumeData
中存放一些斷點下載的信息。能夠將resumeData
寫到本地,後面經過這個文件能夠進行斷點續傳。
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];
}];
複製代碼
在建立下載任務前,能夠判斷當前任務有沒有以前待恢復的任務,若是有的話調用downloadTaskWithResumeData:
方法並傳入一個resumeData
,能夠恢復以前的下載,並從新建立一個downloadTask
任務。
NSString *library = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];
NSData *resumeData = [[NSData alloc] initWithContentsOfFile:resumePath];
self.downloadTask = [self.session downloadTaskWithResumeData:resumeData];
[self.downloadTask resume];
複製代碼
經過suspend
和resume
這種方式掛起的任務,downloadTask
是同一個對象,而經過cancel
而後resumeData
恢復的任務,會建立一個新的downloadTask
任務。
當調用downloadTaskWithResumeData:
方法恢復下載後,會回調下面的方法。回調參數fileOffset
是上次文件的下載大小,expectedTotalBytes
是預估的文件總大小。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
複製代碼
經過backgroundSessionConfigurationWithIdentifier
方法建立後臺上傳或後臺下載類型的NSURLSessionConfiguration
,而且設置一個惟一標識,須要保證這個標識在不一樣的session
之間的惟一性。後臺任務只支持http
和https
的任務,其餘協議的任務並不支持。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
複製代碼
經過backgroundSessionConfigurationWithIdentifier
方法建立的NSURLSession
,請求任務將會在系統的單獨進程中進行,所以即便App進程被kill
也不受影響,依然能夠繼續執行請求任務。若是程序被系統kill
調,下次啓動並執行didFinishLaunchingWithOptions
能夠經過相同的identifier
建立NSURLSession
和NSURLSessionConfiguration
,系統會將新建立的NSURLSession
和單獨進程中正在運行的NSURLSession
進行關聯。
在程序啓動並執行didFinishLaunchingWithOptions
方法時,按照下面方法建立NSURLSession
便可將新建立的Session
和以前的Session
綁定,並自動開始執行以前的下載任務。恢復以前的任務後會繼續執行NSURLSession
的代理方法,並執行後面的任務。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
return YES;
}
複製代碼
當應用進入到後臺時,能夠繼續下載,若是客戶端沒有開啓Background Mode
,則不會回調客戶端進度。下次進入前臺時,會繼續回調新的進度。
若是在後臺下載完成,則會經過AppDelegate
的回調方法通知應用來刷新UI。因爲下載是在一個單獨的進程中完成的,即使業務層代碼會中止執行,但下載的回調依然會被調用。在回調時,容許用戶處理業務邏輯,以及刷新UI。
調用此方法後能夠開始刷新UI,調用completionHandler
表示刷新結束,因此上層業務要作一些控制邏輯。didFinishDownloadingToURL
的調用時機會比此方法要晚,依然在那個方法裏能夠判斷下載文件。因爲項目中可能會存在多個下載任務,因此須要經過identifier
對下載任務進行區分。
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
ViewController *vc = (ViewController *)self.window.rootViewController;
vc.completionHandler = completionHandler;
}
複製代碼
須要注意的是,若是存在多個相同名字的identifier
任務,則建立的session
會將同名的任務都繼續執行。NSURLSessionConfiguration
還提供下面的屬性,在session
下載任務完成時是否啓動App,默認爲YES
,若是設置爲NO
則後臺下載會受到影響。
@property BOOL sessionSendsLaunchEvents;
複製代碼
後臺下載過程當中會設計到一系列的代理方法調用,下面是調用順序。
如今不少視頻類App都有視頻下載的功能,視頻下載確定不會是單純的把一個mp4下載下來就能夠,這裏就講一下視頻下載相關的知識。
m3u8
、ts
、mp4
地址從新包一層,請求數據的時候直接請求運營商給的地址,運營商對數據作了一箇中轉操做。m3u8
爲例,有了免流地址,先下載m3u8
文件。這個文件通常都是加密的,下載完成後客戶端會對m3u8
文件進行decode
,獲取到真正的m3u8
文件。m3u8
文件本質上是ts
片斷的集合,視頻播放播的仍是ts
片斷。隨後對m3u8
文件進行解析,獲取到ts
片斷地址,並將ts
下載地址轉成免流地址後逐個下載,也能夠並行下載。m3u8
文件下載後會以固定格式存在文件夾下,文件夾對應被緩存的視頻。ts
片命名以數字命名,例如0.ts
,下標從0開始。ts
片斷下載完成後,生成本地m3u8
文件。m3u8
文件分爲遠端和本地兩種,遠端的就是正常下載的地址,本地m3u8
文件是在播放本地視頻的時候傳入。格式和普通m3u8
文件差很少,區別在於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
複製代碼
HLS(Http Live Streaming)
是蘋果推出的流媒體協議,其中包含兩部分,m3u8
文件和ts
文件。使用ts
文件的緣由是由於多個ts
能夠無縫拼接,而且單個ts
能夠單獨播放。而mp4
因爲格式緣由,被分割的mp4
文件單獨播放會致使畫面撕裂或者音頻缺失的問題。若是單獨下載多個mp4
文件,播放時會致使間斷的問題。
m3u8
是Unicode
版本的m3u
,是蘋果推出的一種視頻格式,是一個基於HTTP
的流媒體傳輸協議。m3u8
協議將一個媒體文件切爲多個小文件,並利用HTTP
協議進行數據傳輸,小文件所在的資源服務器路徑存儲在.m3u8
文件中。客戶端拿到m3u8
文件,便可根據文件中資源文件的路徑,分別下載不一樣的文件。
m3u8
文件必須是utf-8
格式編碼的,在文件中以#EXT
開頭的是標籤,而且大小寫敏感。以#開頭的其餘字符串則都會被認爲是註釋。m3u8
分爲點播和直播,點播在第一次請求.m3u8
文件後,將下載下來的ts
片斷進行順序播放便可。直播則須要過一段時間對.m3u8
文件進行一個增量下載,並繼續下載後續的ts
文件。
m3u8
中有不少標籤,下面是項目中用到的一些標籤或主要標籤。將mp4
或者flv
文件進行切片很簡單,直接用ffmpeg
命令切片便可。
#EXTM3U
#EXT-X-ENDLIST
#EXT-X-VERSION
ts
片斷最大時長。#EXT-X-TARGETDURATION
ts
片斷時長。#EXTINF
若是沒有#EXT
或#開頭的,通常都是ts
片斷下載地址。路徑能夠是絕對路徑,也能夠是相對路徑,咱們項目裏使用的是絕對路徑。但相對路徑數據量會相對比較小,只不過看視頻的人網速不會太差。
下面是相對路徑地址,文件中只有segment1.ts
,則表示相對於m3u8
的路徑,也就是下面的路徑。
https://data.vod.itc.cn/m3u8
https://data.vod.itc.cn/segment1.ts
複製代碼
A background URLSession with identifier backgroundSession already exists
複製代碼
若是重複後臺已經存在的下載任務,會提示這個錯誤。須要在頁面退出或程序退出時,調用finishTasksAndInvalidate
方法將任務invalidate
。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willTerminateNotification)
name:UIApplicationWillTerminateNotification
object:nil];
- (void)willTerminateNotification {
[self.session getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks) {
if (tasks.count) {
[self.session finishTasksAndInvalidate];
}
}];
}
複製代碼