NSURLSession最全攻略

該文章屬于劉小壯原創(chuàng)骄恶,轉(zhuǎn)載請注明:劉小壯


NSURLSession

NSURLSessioniOS7中推出娇钱,NSURLSession的推出旨在替換之前的NSURLConnection文搂,NSURLSession的使用相對于之前的NSURLConnection更簡單,而且不用處理Runloop相關(guān)的東西笔喉。

2015年RFC 7540標準發(fā)布了http 2.0版本常挚,http 2.0版本中包含很多新的特性奄毡,在傳輸速度上也有很明顯的提升秧倾。NSURLSessioniOS9.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)維護的一個單例對象蝎宇,可以和其他使用這個sessiontask共享連接和請求信息夫啊。

sessionWithConfiguration:

在NSURLSession初始化時傳入一個NSURLSessionConfiguration撇眯,這樣可以自定義請求頭锚国、cookie等信息血筑。

sessionWithConfiguration:delegate:delegateQueue:

如果想更好的控制請求過程以及回調(diào)線程豺总,需要上面的方法進行初始化操作喻喳,并傳入delegate來設(shè)置回調(diào)對象和回調(diào)的線程表伦。

通過NSURLSession發(fā)起一個網(wǎng)絡(luò)請求也比較簡單。

  1. 創(chuàng)建一個NSURLSessionConfiguration配置請求纲熏。
  2. 通過Configuration創(chuàng)建NSURLSession對象赤套。
  3. 通過session對象發(fā)起網(wǎng)絡(luò)請求,并獲取task對象车柠。
  4. 調(diào)用[task resume]方法發(fā)起網(wǎng)絡(luò)請求。
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];

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耙替,直接傳入URLNSURLRequest,即可直接在block中接收返回數(shù)據(jù)辫诅。和普通創(chuàng)建方式一樣,block的創(chuàng)建方式創(chuàng)建后默認也是suspend的狀態(tài)母截,需要調(diào)用resume開始任務(wù)清寇。

completionHandlerdelegate是互斥的翩迈,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:(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對應(yīng)的所有task忙菠,方法區(qū)別在于回調(diào)的參數(shù)不同。以getTasksWithCompletionHandler為例傍睹,在AFN中的應(yīng)用是用來獲取當前sessiontask,并將AFURLSessionManagerTaskDelegate的回調(diào)都置為nil熊赖,以防止崩潰。

- (void)getTasksWithCompletionHandler:(void (^)(NSArray<NSURLSessionDataTask *> *dataTasks, NSArray<NSURLSessionUploadTask *> *uploadTasks, NSArray<NSURLSessionDownloadTask *> *downloadTasks))completionHandler;

- (void)getAllTasksWithCompletionHandler:(void (^)(NSArray<__kindof NSURLSessionTask *> *tasks))completionHandler);

delegateQueue

在初始化NSURLSession時可以指定線程传趾,如果不指定線程,則completionHandlerdelegate的回調(diào)方法簸呈,都會在子線程中執(zhí)行劫恒。

如果初始化NSURLSession時指定了delegateQueue两嘴,則回調(diào)會在指定的隊列中執(zhí)行,如果指定的是mainQueue贰您,則回調(diào)在主線程中執(zhí)行,這樣就避免了切換線程的問題孽亲。

[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

delegate

對于NSURLSession的代理方法這里就不詳細列舉了,方法命名遵循蘋果一貫見名知意的原則篮绿,用起來很簡單。這里介紹一下NSURLSession的代理繼承結(jié)構(gòu)。

代理繼承關(guān)系

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 = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];

NSURLSessionConfiguration提供三種初始化方法岁钓,下面是請求的方法的一些解釋。

@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;

NSURLSessionConfiguration提供defaultSessionConfiguration的方式創(chuàng)建微王,但這并不是單例方法屡限,而是類方法,創(chuàng)建的是不同對象炕倘。通過這種方式創(chuàng)建的configuration钧大,并不會共享cookiecache罩旋、密鑰等啊央,而是不同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 <NSHTTPCookieStorage cookies count:0>

如果對Ephemeral方式創(chuàng)建的config進行打印的話檩淋,可以看到變量類型明顯區(qū)別于其他類型芬为,并且在打印信息前面會有Ephemeral的標示。通過Ephemeral的方式創(chuàng)建的config蟀悦,不會產(chǎn)生持久化信息媚朦,可以很好保護請求的數(shù)據(jù)安全性。

+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;

identifier方式一般用于恢復之前的任務(wù)日戈,主要用于下載询张。如果一個下載任務(wù)正在進行中,程序被kill調(diào)浙炼,可以在程序退出之前保存identifier份氧。下次進入程序后通過identifier恢復之前的任務(wù)唯袄,系統(tǒng)會將NSURLSessionNSURLSessionConfiguration和之前的下載任務(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管理器化戳。如果是通過sharedSessionbackgroundConfiguration創(chuàng)建的NSURLSession单料,默認使用sharedHTTPCookieStorageCookie數(shù)據(jù)。如果不想使用Cookie点楼,則直接設(shè)置為nil即可扫尖,也可以手動設(shè)置為自己的CookieStorage

@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;

證書管理器掠廓。如果是通過sharedSessionbackgroundConfiguration創(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提供了MemoryDisk的緩存蜻牢,在創(chuàng)建時需要為其分別指定MemoryDisk的大小烤咧,以及存儲的文件位置。使用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。

@property NSInteger HTTPMaximumConnectionsPerHost;

連接復用


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)建立的鏈接,如果是相同的域名并蝗,在請求連接建立后祭犯,后面的請求不會立刻斷開秸妥,而是復用現(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對象則不能復用之前的鏈接督函。keep-alive可以保持請求的連接,蘋果允許在iOS上最大保持有4個連接激挪,Mac則是6個連接辰狡。

pipeline

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è)置HTTPShouldUsePipeliningYES,開啟管線化掌逛,此屬性默認為NO师逸。

NSURLSessionTaskMetrics


在日常開發(fā)過程中,經(jīng)常遇到頁面加載太慢的問題豆混,這很大一部分原因都是因為網(wǎng)絡(luò)導致的篓像。所以动知,查找網(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<NSURLSessionTaskTransactionMetrics *> *transactionMetrics;

transactionMetrics數(shù)組中每一個元素都對應(yīng)著當前task的一個請求弃甥,一般數(shù)組中只會有一個元素爽室,如果發(fā)生重定向等情況,可能會存在多個元素淆攻。

@property (copy, readonly) NSDateInterval *taskInterval;

taskInterval記錄了當前task從開始請求到最后完成的總耗時阔墩,NSDateInterval中包含了startDateendDateduration耗時時間瓶珊。

@property (assign, readonly) NSUInteger redirectCount;

redirectCount記錄了重定向次數(shù)啸箫,在進行下載請求時一般都會進行重定向,來保證下載任務(wù)能由后端最合適的節(jié)點來處理伞芹。

NSURLSessionTaskTransactionMetrics

NSURLSessionTaskTransactionMetrics中的屬性都是用來做統(tǒng)計的忘苛,功能都是記錄某個值,并沒有邏輯上的意義唱较。所以這里就對一些主要的屬性做一下解釋扎唾,基本涵蓋了大部分屬性,其他就不管了南缓。

這張圖是我從網(wǎng)上扒下來的胸遇,標示了NSURLSessionTaskTransactionMetrics的屬性在請求過程中處于什么位置。

請求耗時細節(jié)
// 請求對象
@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) 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協(xié)議版本岔冀,如果是http則是0x0000
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion;
// 是否使用蜂窩數(shù)據(jù)
@property (readonly, getter=isCellular) BOOL cellular;

下面是我發(fā)起一個http的下載請求凯旭,統(tǒng)計得到的數(shù)據(jù)。設(shè)備是Xcode模擬器,網(wǎng)絡(luò)環(huán)境是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

FAQ


NSURLSession的delegate為什么是強引用咐蚯?

在初始化NSURLSession對象并設(shè)置代理后,代理對象將會被強引用弄贿。根據(jù)蘋果官方的注釋來看春锋,這個強持有并不會一直存在,而是在調(diào)用URLSession:didBecomeInvalidWithError:方法后差凹,會將delegate釋放期奔。

通過調(diào)用NSURLSessioninvalidateAndCancelfinishTasksAndInvalidate方法,即可將強引用斷開并執(zhí)行didBecomeInvalidWithError:代理方法危尿,執(zhí)行完成后session就會無效不可以使用呐萌。也就是只有在session無效時,才可以解除強引用的關(guān)系谊娇。

有時候為了保證連接復用等問題肺孤,一般不會輕易將session會話invalid,所以最好不要直接使用NSURLSession济欢,而是要對其進行一次二次封裝赠堵,使用AFN3.0的原因之一也在于此。

NSURLSession文件上傳


表單上傳

客戶端有時候需要給服務(wù)端上傳大文件法褥,進行大文件肯定不能全都加載到內(nèi)存里茫叭,一口氣都傳給服務(wù)器。進行大文件上傳時半等,一般都會對需要上傳的文件進行分片揍愁,分片后逐個文件進行上傳。需要注意的是杀饵,分片上傳和斷點續(xù)傳并不是同一個概念莽囤,上傳并不支持斷點續(xù)傳。

進行分片上傳時凹髓,需要對本地文件進行讀取烁登,我們使用NSFileHandle來進行文件讀取怯屉。NSFileHandle提供了一個偏移量的功能蔚舀,我們可以將handle的當前讀取位置seek到上次讀取的位置,并設(shè)置本次讀取長度锨络,讀取的文件就是我們指定文件的字節(jié)赌躺。

- (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;
}

上傳文件現(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é)議格式羔飞,不要多或者少绎狭,會導致服務(wù)端解析失敗。

- (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];
    
    // 拼接上傳參數(shù)
    [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;
}

除此之外褥傍,表單提交還需要設(shè)置請求頭的Content-TypeContent-Length儡嘶,否則會導致請求失敗。其中Content-Length并不是強制要求的恍风,要看后端的具體支持情況蹦狂。

設(shè)置請求頭時,一定要加上boundary朋贬,這個boundary和拼接上傳文件的boundary需要是同一個凯楔。服務(wù)端從請求頭拿到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"];

隨后我們通過下面的代碼創(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:的上傳方式钳幅,理解為UIImageimageNamed的方法,上傳后NSData文件會保存在內(nèi)存中炎滞,不會被回收敢艰。而fromFile:的方式是從本地加載文件,并且上傳完成后可以被回收册赛。而且如果想支持后臺上傳钠导,就必須用fromFile:的方式進行上傳。

OK森瘪,那找到問題我們就開干牡属,改變之前的上傳邏輯,改為fromFile:的方式上傳扼睬。

// 將分片寫入到本地
NSString *filePath = [NSString stringWithFormat:@"%@/%ld", [self segmentDocumentPath], currentIndex];
BOOL write = [formData writeToFile:filePath atomically:YES];

// 創(chuàng)建分片文件夾
- (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:方法傳一個本地分片的路徑進去逮栅,所以需要預(yù)先對文件進行分片,并保存在本地窗宇。在分片的同時措伐,還需要拼接boundary信息。

所以我們在上傳任務(wù)開始前担映,先對文件進行分片并拼接信息废士,然后將分片文件寫入到本地叫潦。為了方便管理蝇完,我們基于具有唯一性的文件名進行MD5來創(chuàng)建分片文件夾,分片文件命名通過下標來命名,并寫入到本地短蜕。文件上傳完成后氢架,直接刪除整個文件夾即可。當然朋魔,這些文件操作都是在異步線程中完成的岖研,防止影響UI線程。

內(nèi)存占用

我們用一個400MB的視頻測試上傳警检,我們可以從上圖看出孙援,圈紅部分是我們上傳文件的時間。將上傳方式改為fromFile:后扇雕,上傳文件的峰值最高也就是在10MB左右徘徊拓售,這對于iPhone6這樣的低內(nèi)存老年機來說,是相當友好的镶奉,不會導致低端設(shè)備崩潰或者卡頓础淤。

動態(tài)分片

用戶在上傳時網(wǎng)絡(luò)環(huán)境會有很多情況,WiFi哨苛、4G鸽凶、弱網(wǎng)等很多情況。如果上傳分片太大可能會導致失敗率上升建峭,分片文件太小會導致網(wǎng)絡(luò)請求太多玻侥,產(chǎn)生太多無用的boundaryheader亿蒸、數(shù)據(jù)鏈路等資源的浪費使碾。

為了解決這個問題,我們采取的是動態(tài)分片大小的策略祝懂。根據(jù)特定的計算策略票摇,預(yù)先使用第一個分片的上傳速度當做測速分片,測速分片的大小是固定的砚蓬。根據(jù)測速的結(jié)果矢门,對其他分片大小進行動態(tài)分片,這樣可以保證分片大小可以最大限度的利用當前網(wǎng)速灰蛙。

if ([Reachability reachableViaWiFi]) {
    self.segmentSize = 500 * 1024;
} else if ([Reachability reachableViaWWAN]) {
    self.segmentSize = 300 * 1024;
}

當然祟剔,如果覺得這種分片方式太過復雜,也可以采取一種閹割版的動態(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)速等原因矩乐,導致上傳失敗。失敗的任務(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文件下載


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 = [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];

我們可以調(diào)用suspend將下載任務(wù)掛起,隨后調(diào)用resume方法繼續(xù)下載任務(wù)页滚,suspendresume需要是成對的召边。但是suspend掛起任務(wù)是有超時的,默認為60s裹驰,如果超時系統(tǒng)會將TCP連接斷開隧熙,我們再調(diào)用resume是失效的』昧郑可以通過NSURLSessionConfigurationtimeoutIntervalForResource來設(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 {
    CGFloat progress = (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ù)有沒有之前待恢復的任務(wù)术羔,如果有的話調(diào)用downloadTaskWithResumeData:方法并傳入一個resumeData赢赊,可以恢復之前的下載,并重新創(chuàng)建一個downloadTask任務(wù)级历。

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];

通過suspendresume這種方式掛起的任務(wù)释移,downloadTask是同一個對象,而通過cancel然后resumeData恢復的任務(wù)寥殖,會創(chuàng)建一個新的downloadTask任務(wù)玩讳。

當調(diào)用downloadTaskWithResumeData:方法恢復下載后,會回調(diào)下面的方法嚼贡⊙浚回調(diào)參數(shù)fileOffset是上次文件的下載大小,expectedTotalBytes是預(yù)估的文件總大小粤策。

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;

后臺下載

通過backgroundSessionConfigurationWithIdentifier方法創(chuàng)建后臺上傳或后臺下載類型的NSURLSessionConfiguration樟澜,并且設(shè)置一個唯一標識,需要保證這個標識在不同的session之間的唯一性。后臺任務(wù)只支持httphttps的任務(wù)秩贰,其他協(xié)議的任務(wù)并不支持霹俺。

NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];

通過backgroundSessionConfigurationWithIdentifier方法創(chuàng)建的NSURLSession,請求任務(wù)將會在系統(tǒng)的單獨進程中進行毒费,因此即使App進程被kill也不受影響丙唧,依然可以繼續(xù)執(zhí)行請求任務(wù)。如果程序被系統(tǒng)kill調(diào)觅玻,下次啟動并執(zhí)行didFinishLaunchingWithOptions可以通過相同的identifier創(chuàng)建NSURLSessionNSURLSessionConfiguration想际,系統(tǒng)會將新創(chuàng)建的NSURLSession和單獨進程中正在運行的NSURLSession進行關(guān)聯(lián)。

在程序啟動并執(zhí)行didFinishLaunchingWithOptions方法時串塑,按照下面方法創(chuàng)建NSURLSession即可將新創(chuàng)建的Session和之前的Session綁定沼琉,并自動開始執(zhí)行之前的下載任務(wù)北苟∽耍恢復之前的任務(wù)后會繼續(xù)執(zhí)行NSURLSession的代理方法,并執(zhí)行后面的任務(wù)友鼻。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];
    [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    return YES;
}

當應(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則后臺下載會受到影響扩淀。

@property BOOL sessionSendsLaunchEvents;

后臺下載過程中會設(shè)計到一系列的代理方法調(diào)用楔敌,下面是調(diào)用順序。

后臺下載時序圖

視頻文件下載

現(xiàn)在很多視頻類App都有視頻下載的功能驻谆,視頻下載肯定不會是單純的把一個mp4下載下來就可以卵凑,這里就講一下視頻下載相關(guān)的知識。

  1. 視頻地址一般都是從服務(wù)端獲取的胜臊,所以需要先請求接口獲取下載地址勺卢。這個地址可以是某個接口就已經(jīng)請求下來的,也可以是某個固定格式拼接的象对。
  2. 現(xiàn)在有很多視頻App都是有免流服務(wù)的黑忱,例如騰訊大王卡、螞蟻寶卡之類的勒魔,免流服務(wù)的本質(zhì)就是對m3u8甫煞、tsmp4地址重新包一層冠绢,請求數(shù)據(jù)的時候直接請求運營商給的地址抚吠,運營商對數(shù)據(jù)做了一個中轉(zhuǎn)操作。
  3. 以流視頻m3u8為例弟胀,有了免流地址楷力,先下載m3u8文件。這個文件一般都是加密的孵户,下載完成后客戶端會對m3u8文件進行decode萧朝,獲取到真正的m3u8文件。
  4. m3u8文件本質(zhì)上是ts片段的集合延届,視頻播放播的還是ts片段剪勿。隨后對m3u8文件進行解析,獲取到ts片段地址方庭,并將ts下載地址轉(zhuǎn)成免流地址后逐個下載厕吉,也可以并行下載。
  5. m3u8文件下載后會以固定格式存在文件夾下械念,文件夾對應(yīng)被緩存的視頻头朱。ts片命名以數(shù)字命名,例如0.ts龄减,下標從0開始项钮。
  6. 所有ts片段下載完成后,生成本地m3u8文件。
  7. 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文件單獨播放會導致畫面撕裂或者音頻缺失的問題入愧。如果單獨下載多個mp4文件鄙漏,播放時會導致間斷的問題。

m3u8Unicode版本的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 URLSession with identifier backgroundSession already exists

如果重復后臺已經(jīng)存在的下載任務(wù),會提示這個錯誤嫁审。需要在頁面退出或程序退出時跋炕,調(diào)用finishTasksAndInvalidate方法將任務(wù)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];
        }
    }];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末律适,一起剝皮案震驚了整個濱河市辐烂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捂贿,老刑警劉巖纠修,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異厂僧,居然都是意外死亡扣草,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門颜屠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辰妙,“玉大人,你說我怎么就攤上這事汽纤∩细冢” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵蕴坪,是天一觀的道長肴掷。 經(jīng)常有香客問我敬锐,道長,這世上最難降的妖魔是什么呆瞻? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任台夺,我火速辦了婚禮,結(jié)果婚禮上痴脾,老公的妹妹穿的比我還像新娘颤介。我一直安慰自己,他們只是感情好赞赖,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布滚朵。 她就那樣靜靜地躺著,像睡著了一般前域。 火紅的嫁衣襯著肌膚如雪辕近。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天匿垄,我揣著相機與錄音移宅,去河邊找鬼。 笑死椿疗,一個胖子當著我的面吹牛漏峰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播届榄,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼浅乔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了痒蓬?” 一聲冷哼從身側(cè)響起童擎,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎攻晒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體班挖,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡鲁捏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了萧芙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片给梅。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖双揪,靈堂內(nèi)的尸體忽然破棺而出动羽,到底是詐尸還是另有隱情,我是刑警寧澤渔期,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布运吓,位于F島的核電站渴邦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拘哨。R本人自食惡果不足惜谋梭,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望倦青。 院中可真熱鬧瓮床,春花似錦、人聲如沸产镐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽癣亚。三九已至峭沦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逃糟,已是汗流浹背吼鱼。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绰咽,地道東北人菇肃。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像取募,于是被迫代替她去往敵國和親琐谤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353