天天看點

NSURLSession最全攻略NSURLSession的上傳下載下傳

NSURLSession

NSURLSession

iOS7

中推出,

NSURLSession

的推出旨在替換之前的

NSURLConnection

NSURLSession

的使用相對于之前的

NSURLConnection

更簡單,而且不用處理

Runloop

相關的東西。

2015年

RFC 7540

标準釋出了

http 2.0

版本,

http 2.0

版本中包含很多新的特性,在傳輸速度上也有很明顯的提升。

NSURLSession

iOS9.0

開始,對

http 2.0

提供了支援。

NSURLSession

由三部分構成:

  • NSURLSession:請求會話對象,可以用系統提供的單例對象,也可以自己建立。
  • NSURLSessionConfiguration:對

    session

    會話進行配置,一般都采用

    default

  • NSURLSessionTask:負責執行具體請求的

    task

    ,由

    session

    建立。

NSURLSession

有三種方式建立:

sharedSession
           

系統維護的一個單例對象,可以和其他使用這個

session

task

共享連接配接和請求資訊。

sessionWithConfiguration:
           

在NSURLSession初始化時傳入一個NSURLSessionConfiguration,這樣可以自定義請求頭、cookie等資訊。

sessionWithConfiguration:delegate:delegateQueue:
           

如果想更好的控制請求過程以及回調線程,需要上面的方法進行初始化操作,并傳入

delegate

來設定回調對象和回調的線程。

通過

NSURLSession

發起一個網絡請求也比較簡單。

  1. 建立一個NSURLSessionConfiguration配置請求。
  2. 通過Configuration建立NSURLSession對象。
  3. 通過session對象發起網絡請求,并擷取task對象。
  4. 調用[task resume]方法發起網絡請求。
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

發起的每個請求,都會被封裝為一個

NSURLSessionTask

任務,但一般不會直接是

NSURLSessionTask

類,而是基于不同任務類型,被封裝為其對應的子類。

  • NSURLSessionDataTask:處理普通的

    Get

    Post

    請求。
  • NSURLSessionUploadTask:處理上傳請求,可以傳入對應的上傳檔案或路徑。
  • NSURLSessionDownloadTask:處理下載下傳位址,提供斷點續傳功能的

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

delegateQueue

在初始化

NSURLSession

時可以指定線程,如果不指定線程,則

completionHandler

delegate

的回調方法,都會在子線程中執行。

如果初始化

NSURLSession

時指定了

delegateQueue

,則回調會在指定的隊列中執行,如果指定的是

mainQueue

,則回調在主線程中執行,這樣就避免了切換線程的問題。

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

delegate

對于

NSURLSession

的代理方法這裡就不詳細列舉了,方法命名遵循蘋果一貫見名知意的原則,用起來很簡單。這裡介紹一下

NSURLSession

的代理繼承結構。

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

建立方式

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

和之前的下載下傳任務進行關聯,并繼續之前的任務。

timeout

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個連接配接。

pipeline

NSURLSession最全攻略NSURLSession的上傳下載下傳

pipeline

HTTP1.1

中,基于

keep-alive

,還可以将請求進行管線化。和相同後端服務,

TCP

層建立的連結,一般都需要前一個請求傳回後,後面的請求再發出。但

pipeline

就可以不依賴之前請求的響應,而發出後面的請求。

pipeline

依賴用戶端和伺服器都有實作,服務端收到用戶端的請求後,要按照先進先出的順序進行任務處理和響應。

pipeline

依然存在之前非

pipeline

的問題,就是前面的請求如果出現問題,會阻塞目前連接配接影響後面的請求。

pipeline

對于請求大檔案并沒有提升作用,隻是對于普通請求速度有提升。在

NSURLSessionConfiguration

中可以設定

HTTPShouldUsePipelining

YES

,開啟管線化,此屬性預設為

NO

NSURLSessionTaskMetrics

在日常開發過程中,經常遇到頁面加載太慢的問題,這很大一部分原因都是因為網絡導緻的。是以,查找網絡耗時的原因并解決,就是一個很重要的任務了。蘋果對于網絡檢查提供了

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

中的屬性都是用來做統計的,功能都是記錄某個值,并沒有邏輯上的意義。是以這裡就對一些主要的屬性做一下解釋,基本涵蓋了大部分屬性,其他就不管了。

這張圖是我從網上扒下來的,标示了

NSURLSessionTaskTransactionMetrics

的屬性在請求過程中處于什麼位置。

NSURLSession最全攻略NSURLSession的上傳下載下傳

請求耗時細節

// 請求對象
@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
           

FAQ

NSURLSession的delegate為什麼是強引用?

在初始化

NSURLSession

對象并設定代理後,代理對象将會被強引用。根據蘋果官方的注釋來看,這個強持有并不會一直存在,而是在調用

URLSession:didBecomeInvalidWithError:

方法後,會将

delegate

釋放。

通過調用

NSURLSession

invalidateAndCancel

finishTasksAndInvalidate

方法,即可将強引用斷開并執行

didBecomeInvalidWithError:

代理方法,執行完成後

session

就會無效不可以使用。也就是隻有在

session

無效時,才可以解除強引用的關系。

有時候為了保證連接配接複用等問題,一般不會輕易将

session

會話

invalid

,是以最好不要直接使用

NSURLSession

,而是要對其進行一次二次封裝,使用

AFN3.0

的原因之一也在于此。

NSURLSession的上傳下載下傳

檔案上傳

表單上傳

用戶端有時候需要給服務端上傳大檔案,進行大檔案肯定不能全都加載到記憶體裡,一口氣都傳給伺服器。進行大檔案上傳時,一般都會對需要上傳的檔案進行分片,分片後逐個檔案進行上傳。需要注意的是,分片上傳和斷點續傳并不是同一個概念,上傳并不支援斷點續傳。

進行分片上傳時,需要對本地檔案進行讀取,我們使用

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線程。

NSURLSession最全攻略NSURLSession的上傳下載下傳

記憶體占用

我們用一個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;
           

背景下載下傳過程中會設計到一系列的代理方法調用,下面是調用順序。

NSURLSession最全攻略NSURLSession的上傳下載下傳

背景下載下傳時序圖

視訊檔案下載下傳

現在很多視訊類App都有視訊下載下傳的功能,視訊下載下傳肯定不會是單純的把一個mp4下載下傳下來就可以,這裡就講一下視訊下載下傳相關的知識。

  1. 視訊位址一般都是從服務端擷取的,是以需要先請求接口擷取下載下傳位址。這個位址可以是某個接口就已經請求下來的,也可以是某個固定格式拼接的。
  2. 現在有很多視訊App都是有免流服務的,例如騰訊大王卡、螞蟻寶卡之類的,免流服務的本質就是對

    m3u8

    ts

    mp4

    位址重新包一層,請求資料的時候直接請求營運商給的位址,營運商對資料做了一個中轉操作。
  3. 以流視訊

    m3u8

    為例,有了免流位址,先下載下傳

    m3u8

    檔案。這個檔案一般都是加密的,下載下傳完成後用戶端會對

    m3u8

    檔案進行

    decode

    ,擷取到真正的

    m3u8

    檔案。
  4. m3u8

    檔案本質上是

    ts

    片段的集合,視訊播放播的還是

    ts

    片段。随後對

    m3u8

    檔案進行解析,擷取到

    ts

    片段位址,并将

    ts

    下載下傳位址轉成免流位址後逐個下載下傳,也可以并行下載下傳。
  5. m3u8

    檔案下載下傳後會以固定格式存在檔案夾下,檔案夾對應被緩存的視訊。

    ts

    片命名以數字命名,例如

    0.ts

    ,下标從0開始。
  6. 所有

    ts

    片段下載下傳完成後,生成本地

    m3u8

    檔案。
  7. 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
           

m3u8檔案

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

  • 目前檔案版本,如果不指定則預設為1

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

作者:劉小壯

連結:https://www.jianshu.com/p/ac79db251cbf