[Apple官方文檔翻譯]: NSURLSession Programming Guide

關於URL加載系統

這個文檔描述了Foundation框架中的與URL交互的一些類和與服務器交互的標準互聯網協議. 這些類統一稱爲URL加載系統.

URL加載系統是一個一些類和協議組成的允許應用通過URL來訪問內容的合集. 其中核心的類就是NSURL,它負責產生出URL和資源的位置.

爲了支持這些類的運行,Foundation框架提供了很多類來使用,比如:加載內容,上傳數據到服務器,管理cookie,控制返回數據緩存,處理憑證管理和認證.

URL加載系統提供支持以下協議:

  • 文件傳輸協議(ftp://)
  • 超文本傳輸協議(http://)
  • 加密的超文本傳輸協議(https://)
  • 本地文件(file://)
  • 數據(data://)

它還透明的將代理服務器和SOCKS網關當成用戶的首選.

總覽

URL加載系統包含了一些重要的幫助類.這些類主要分成5個列別: 協議支持, 憑證和權限, cookie管理, 配置管理和緩存管理.

總覽

URL加載

在URL加載系統中最常用的功能就是從資源處獲取內容.我們可以通過很多方式獲取,這取決於我們的應用需求.所使用的API取決於使用iOS還是OSX系統以及對獲取的數據形式是文件還是內存數據等.

  • 在iOS7和OSX v10.9 以後,首選的URL請求使用NSURLSession
  • 對於老版本的OSX,可以使用NSURLDownload來下載文件
  • 對於老版本的iOS和OSX,可以使用NSURLConnection來發起請求獲取內容.

使用的特定方法取決於我們想要下載數據到內存還是到磁盤中.

獲取內容(在內存中)

在使用上,有兩種方式獲取內容:

  • 簡單的請求.使用NSURLSession的API去從NSURL所指向的資源處獲取數據.
  • 複雜的請求.請求包含了上傳數據,比如:提供給NSURLSession一個NSURLRequest對象.

考慮到上面兩種方式的選擇,我們的應用有兩種方式獲取請求返回的數據:

  • 塊(block).提供一個完成的塊回調.URL加載類完成請求後會回調快.
  • 代理(delegate). 提供一個代理回調.URL加載類會在請求收到數據就會回調代理,如果需要的話,代理負責累加收到的數據.

除了返回的數據以外,URL加載系統會回調收到的response,一個包含了request的數據,比如:MIME和數據長度.

下載文件

在使用上,也有兩種方式,和上面的獲取內容是一樣的.

這裏,NSURLSession類在iOS中提供了兩種比NSURLDownload高級的下載文件API,他可以在我們的應用在後臺,被關閉(用戶)或者崩潰之後繼續下載文件.

幫助類

URL加載類提供了兩個有幫助類.一個是請求類NSURLRequest,另一個是收到的回執NSURLResponse.

URL請求

一個NSURLRequest類包裝了URL和協議的屬性.還包含了特殊的關於本地緩存的數據的策略,還可以設置請求超時時間.

注意:當應用使用NSMutableURLRequest類初始化一個請求或下載實例的時候,它是從request深拷貝過來的.如果改變了原來的request,不會對這個實例起作用.

一些協議支持屬性.比如:HTTP protocol給通過類目的方式給NSURLRequest添加了一些方法.包括HTTP請求體,請求頭和請求方法.

請求返回

一個請求從服務器返回的響應包含兩部分:元數據內容的描述和內容本身.元數據一般被協議包裝成NSURLResponse類,包含了MIME類型,內容長度,編碼方式等.作爲協議NSURLResponse的子類,可以提供額外的信息.比如NSHTTPURLResponse存儲了請求頭和狀態碼.

重定向和請求改變

一些協議,就像HTTP,提供一了一種方式告訴應用請求的內容已經轉移到別的地方了.URL加載系統通過代理通知這個消息.如果我們的應用實現了這個協議,可以決定是繼續重定向請求內容還是直接返回錯誤.

認證和證書

一些服務器對內容訪問有限制,需要用戶提供認證證書.包含一個用戶名和密碼.認證也包含我們的應用是否信任網站.

URL加載系統提供了類來很好的安全存儲證書.我們的應用可以爲一個請求指定一個整數,或者在app啓動的時候使用,或者存儲到鑰匙串裏.

NSURLCredential類包裝了認證信息(用戶名,密碼等),並存儲.NSURLProtectionSpace類表示一個需要認證的空間.一個受保護的空間可以限制一個在服務器或者代理商的單個的URL.

一個全局的類NSURLCredentialStorage,負責管理證書的存儲,和通過NSURLCredential來找到對應的NSURLProtectionSpace存儲空間.

NSURLAuthenticationChallenge包裝了NSURLProtocol爲實現一個請求需要的認證信息:一個證書,存儲的保護空間,存儲錯誤或者是否需要認證,和進行嘗試認證的次數.NSURLAuthenticationChallenge的實例也特指進行認證的對象.這個對象實現了NSURLAuthenticationChallengeSender協議.

NSURLAuthenticationChallenge實例爲NSURLProtocol的子類需要認證的情況下使用.它也提供了代理方法,讓NSURLConnectionNSURLDownload方便的自定義認證處理.

存儲管理

URL加載系統提供了磁盤和內存兩種存儲方式.允許應用可以通過使用上次緩存的請求響應來減少網絡請求.緩存是基於app爲單位存儲的.緩存需要NSURLConnection根據NSURLRequest設置的緩存策略來工作的.

NSURLCache類提供了方法來設置的大小,緩存位置和管理緩存內容的包裝類NSCachedURLResponse.

NSCachedURLResponse類包裝了NSURLResponse和URL的數據,他還提供了userInfo的字典讓用戶管理自定義的數據.

不是所有的協議都支持響應緩存.目前只有httphttps支持.

NSURLConnection對象可以通過代理方法connection:willCacheResponse控制返回的數據是否緩存.

Cookie存儲

基於跨國界的HTTP協議,客戶端經常使用cookie來提供URL對應數據的緩存.URL加載系統提佛那個了接口來創建和管理cookie.

OSX和iOS提供了NSHTTPCookieStorage類,來管理cookie對象類NSHTTPCookie. 在OSX中,cookie在所有應用共享. 在iOS中cookie只在自己的應用使用.

協議支持

URL加載系統支持http,https,file,ftp,data協議.另外,URL加載系統還支持應用註冊自己應用使用的協議.

使用NSURLSession

NSURLSession類和相關的類提供了通過HTPP下載數據的API接口.這些接口提供了很多代理方法來支持認證和可以讓應用不論是啓動,掛起還是關閉,都可以在後臺下載數據.

爲了使用NSURLSession,我們的應用創建一些列回話,每一個回話都是一組和數據傳輸的任務關聯.比如:我們寫一個瀏覽器,我們的應用就會爲每一個tab或窗口創建一個回話.在每一個回話中,會添加很多任務,每個任務負責自己的下載數據.

就像許多的網絡請求API一樣,NSURLSession也是一個異步的.如果使用默認方式,我們的應用只需要提供一個請求你結束的回調塊,當網絡請求傳輸結束的時候回調用這個回調塊.另外,如果我們提供了自定義的代理對象,需要自己實現所有的代理方法來處理回話回調.

NSURLSession的API提供了請求的狀態和進度屬性,而且也會傳輸給代理者.它支持取消任務,掛起任務和恢復任務.

理解 URL Session 的原理

在一個回話的任務的行爲取決於三件事:回話的類型(取決於創建回話的時候傳入的配置對象),任務的類型和任務創建的時機是在應用在前臺還是後臺.

會話的類型

NSURLSession支持三種回話類型,類型取決於配置對象.

  • 默認的會話.這種類型和其他的基礎框架下載方法類似,使用磁盤緩存和證書存入鑰匙串
  • 臨時的會話.不存儲任何信息到磁盤中.所有的緩存,證書,數據都存儲在內存中.當廢棄回話的時候,內存中的所有緩存會被清除.
  • 後臺的會話.除了分別處理所有的傳輸以外,和默認的回話很像.但是也有一些侷限性.

任務的類型

在回話中,NSURLSession類支持三類任務類型:數據任務,下載任務和上傳任務.

  • 數據任務.收發數據都使用NSData對象.數據任務主要使用在短的,經常與服務器交互的請求.數據任務在每接受一小塊數據後,會返回這次接受的數據.當所有數據傳輸完後,會回調結束塊.
  • 下載任務.主要使用在下載文件,支持後臺下載.
  • 上傳任務.主要使用在上傳文件到服務器.

後臺傳輸注意事項

NSURLSession類支持在應用掛起的時候在後臺傳輸數據.後臺傳輸數據只支持通過後臺會話類型創建的會話來配置.

之所以使用後臺會話類型是因爲重啓應用的進程代價比較大,所以這些傳輸的數據是通過另外的進程來執行的,但是有一些功能限制:
- 會話必須爲每一次傳輸提供一個代理.
- 只支持HTTP和HTTPS.
- 經常會重定向
- 上傳的任務只支持文件(如果使用數據對象或流對象,會在程序退出後失敗)
- 如果後臺傳輸是在應用在後臺的時候創建的,配置對象的discretionary屬性會設置爲YES,意思是這個傳輸可以讓系統來優化執行.

在iOS中,當後臺任務完成或者需要認證的時候,如果應用沒有在運行,系統會自動在後臺喚起app,調用UIApplicationDelegate對象的application:handleEventsForBackgroundURLSession:completionHandler方法.這次調用會帶入會話的標識符和回調,app需要存儲回調,然後使用這個標識符創建一個後臺任務.新創建的會話會自動的關聯到後臺的同一個標識符的任務.當任務完成後,會調用會話的代理方法URLSessionDidFinishEventsForBackgroundURLSession.在代理方法中,調用之前存儲的回調來告訴系統後臺啓動app是安全的.

當啓動app的時候,我們應該立即使用上次未完成的任務的標識符創建後臺任務,這寫些我們創建的後臺任務會自動關聯到系統中的對應的任務.

當應用掛起的時候有任務完成後,會調用代理方法URLSession:downloadTask:didFinishDownloadingToURL:
同樣的,如果任務需要認證,NSURLSession對象會調用代理方法URLSession:task:didReceiveChallenge:completionHandler:或者URLSession:didReceiveChallenge:completionHandler:

上傳和下載的後臺任務在網絡錯誤的時候回被自動重試.不需要使用網絡API來判斷網絡和重試.

生命週期和代理的互相作用

根據使用NSURLSession不同的方式,有必要了解一下完整的會話聲明週期,包括會話如何與代理方法交互,交互順序和調用代理方法時機等等.

代理樣例

#import <Foundation/Foundation.h>


typedef void (^CompletionHandlerType)();

@interface MySessionDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>

@property NSURLSession *backgroundSession;
@property NSURLSession *defaultSession;
@property NSURLSession *ephemeralSession;

#if TARGET_OS_IPHONE
@property NSMutableDictionary *completionHandlerDictionary;
#endif

- (void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier;
- (void) callCompletionHandlerForSession: (NSString *)identifier;


@end

創建和配置會話

NSURLSession類提供了很多配置選項:
- 每個會話的私有存儲空間.支持緩存,cookie,認證信息和協議.
- 鑑權,可以綁定到特定或一組請求.
- 通過URL上傳或下載文件
- 配置一個服務器主機最大的連接數
- 配置每個資源的超時時間觸發時機
- 最大和最小的TLS版本支持
- 自定義的協議字典
- 控制cookie策略
- 控制HTTP管道

因爲在配置對象中有很多配置項,我們可以使用一些通用的設置項.
- 一個配置對象管理會話和任務的行爲.
- 可選的,一個代理對象處理收到的數據和其他事件,比如服務器鑑權,判斷資源加載出否需要轉換成下載等.
- 如果沒有提供代理方法,NSURLSession使用系統的代理方法,可以輕鬆的使用sendAsynchronousRequest:queue:completionHandler:

當初始化完會話對象後,就不能改變配置了.

下面的代碼示例如何創建簡單的,短暫的和後臺會話.

#if TARGET_OS_IPHONE
    self.completionHandlerDictionary = [NSMutableDictionary dictionaryWithCapacity:0];
#endif

    /* Create some configuration objects. */

    NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: @"myBackgroundSessionIdentifier"];
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSessionConfiguration *ephemeralConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration];


    /* Configure caching behavior for the default session.
       Note that iOS requires the cache path to be a path relative
       to the ~/Library/Caches directory, but OS X expects an
       absolute path.
     */
#if TARGET_OS_IPHONE
    NSString *cachePath = @"/MyCacheDirectory";

    NSArray *myPathList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *myPath    = [myPathList  objectAtIndex:0];

    NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];

    NSString *fullCachePath = [[myPath stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:cachePath];
    NSLog(@"Cache path: %@\n", fullCachePath);
#else
    NSString *cachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"/nsurlsessiondemo.cache"];

    NSLog(@"Cache path: %@\n", cachePath);
#endif





    NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384 diskCapacity: 268435456 diskPath: cachePath];
    defaultConfigObject.URLCache = myCache;
    defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;

    /* Create a session for each configurations. */
    self.defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
    self.backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
    self.ephemeralSession = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

當後臺配置有異常的時候,可以複用會話配置創建另外的會話.我們可以隨時更改配置對象.當創建會話的時候,會話對象是對配置對象的深拷貝,所以不會影響之前的會話.比如,我們需要創建另一個會話,設置爲只有在Wi-Fi情況下才能連接,可以像下面一樣:

 ephemeralConfigObject.allowsCellularAccess = NO;

    // ...

    NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

通過系統提供的代理獲取數據

最簡單的方式使用NSURLSession是使用 sendAsynchronousRequest:queue:completionHandler:方法,使用這種方法,只需要提供兩塊代碼.
- 創建個配置會話
- 一個全部數據請求完成的回調塊

使用系統的代理方法,可以簡單如下:

    NSURLSession *delegateFreeSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];

    [[delegateFreeSession dataTaskWithURL: [NSURL URLWithString: @"http://www.example.com/"]
                       completionHandler:^(NSData *data, NSURLResponse *response,
                                           NSError *error) {
                           NSLog(@"Got response %@ with error %@.\n", response, error);
                           NSLog(@"DATA:\n%@\nEND DATA\n",
                                 [[NSString alloc] initWithData: data
                                         encoding: NSUTF8StringEncoding]);
                       }] resume];

使用自定義的代理方法獲取數據

如果使用自定義的代理方法,至少要實現下面兩個代理方法:
- URLSession:dataTask:didReceiveData::提供請求回來的數據,一次一小塊數據.
- URLSession:task:didCompleteWithError::表示任務是否完成

如果我們的應用需要在URLSession:dataTask:didReceiveData:方法之後使用數據,我們自己需要負責存儲所有返回的數據.

比如:瀏覽器需要所有數據回來以後渲染頁面,這樣就需要一個字典存儲對應的數據NSMutableData, 然後使用appendData:方法添加對應的數據.

下面代碼顯示如何創建和啓動任務

    NSURL *url = [NSURL URLWithString: @"http://www.example.com/"];

    NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL: url];
    [dataTask resume];

下載文件

在底層實現上,下載文件和下載數據相似.應用需要實現代理方法:

  • URLSession:downloadTask:didFinishDownloadingToURL:當下載完成提供了臨時的文件(在這個方法返回之後,臨時文件會被刪除)
  • URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:提供了下載進度
  • URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:告訴應用繼續之前被打斷的任務下載完成
  • URLSession:task:didCompleteWithError:告訴應用下載失敗

當我們規劃下載一個後臺任務,技師應用沒啓動,後臺也會下載.但是使用默認和短暫的會話,下載任務必須在app啓動.

在下載過程中,可以通過cancelByProducingResumeData:方法暫停正在執行的任務,如果後續要繼續下載,我們將從這個方法中獲取的數據存儲起來,然後使用downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:來創建新的任務繼續下載.

如果傳輸失敗了,代理方法URLSession:task:didCompleteWithError:會被調用,如果任務可以繼續下載,會在userInfo字典中存儲key爲NSURLSessionDownloadTaskResumeData的值,取到未下載完的數據可以繼續創建新的會話下載.

下載代碼開啓下載文件

NSURL *url = [NSURL URLWithString: @"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/"
                  "Foundation/ObjC_classic/FoundationObjC.pdf"];

    NSURLSessionDownloadTask *downloadTask = [self.backgroundSession downloadTaskWithURL: url];
    [downloadTask resume];

下載任務的代理方法

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"Session %@ download task %@ finished downloading to URL %@\n",
        session, downloadTask, location);

#if 0
    /* Workaround */
    [self callCompletionHandlerForSession:session.configuration.identifier];
#endif

#define READ_THE_FILE 0
#if READ_THE_FILE
    /* Open the newly downloaded file for reading. */
    NSError *err = nil;
    NSFileHandle *fh = [NSFileHandle fileHandleForReadingFromURL:location
        error: &err];

    /* Store this file handle somewhere, and read data from it. */
    // ...

#else
    NSError *err = nil;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *cacheDir = [[NSHomeDirectory()
        stringByAppendingPathComponent:@"Library"]
        stringByAppendingPathComponent:@"Caches"];
    NSURL *cacheDirURL = [NSURL fileURLWithPath:cacheDir];
    if ([fileManager moveItemAtURL:location
        toURL:cacheDirURL
        error: &err]) {

        /* Store some reference to the new URL */
    } else {
        /* Handle the error. */
    }
#endif

}

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    NSLog(@"Session %@ download task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.\n",
        session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
    NSLog(@"Session %@ download task %@ resumed at offset %lld bytes out of an expected %lld bytes.\n",
        session, downloadTask, fileOffset, expectedTotalBytes);
}

上傳Body內容

應用發送POST請求會攜帶Body內容,內容有三種形式: NSData對象,文件和流對象.

  • 如果應用內存中已經有上傳的數據,使用NSData對象上傳.
  • 如果上傳的內容在磁盤中上的文件中,或者執行歐泰任務,或者爲了釋放內存而將數據存入文件,都可以使用文件方式.
  • 在通過網絡收到的數據源或者轉換現有的NSURLConnection對象時,使用流對象.

不論選擇哪種上傳內容的方式,如果我們提供了自己的代理,代理方法URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:會告訴我們上傳進度.

另外,如果使用流對象上傳,必須提供一個自定義的會話代理方法實現uploadTaskWithRequest:fromData:completionHandler:來創建上傳任務.

使用NSData對象上傳

通過NSData對象上傳需要調用uploadTaskWithRequest:fromData:或者uploadTaskWithRequest:fromData:completionHandler:方法創建任務,提供內容.

會話會計算內容大小存入Header字段Content-Length中,默認也會提供Content-Type字段.

我們還可以添加額外的Header字段.

使用文件上傳

通過文件上傳使用方法uploadTaskWithRequest:fromFile:uploadTaskWithRequest:fromFile:completionHandler:方法創建.需要提供一個URL指定文件位置.

會話會計算內容大小存入Header字段Content-Length中,默認也會提供Content-Type字段.

我們還可以添加額外的Header字段.

使用流對象上傳

使用方法uploadTaskWithStreamedRequest:創建任務.我們的應用提供了一個個流對象關聯的請求,請求會從流對象讀取內容.

應用必須提供Header字段:Content-LengthContent-Type.

另外,由於會話不能重讀流中的信息,所以在任務重試的時候需要提供一個新的流對象.可以使用方法URLSession:task:needNewBodyStream:,當方法調用時候,我們負責創建新的流對象.

使用下載任務上傳文件

當使用下載需要上傳文件的時候,只能使用NSData對象或者流對象放在請求Body中.

如果使用了流對象,必須實現代理方法URLSession:task:needNewBodyStream:,用於在認證失敗的時候接受事件.

處理鑑權和自定義的TLS鏈驗證

如果遠端服務器返回狀態碼標識需要鑑權或者需要在連接的時候需要鑑權,NSURLSession會回到鑑權相關的代理方法.

  • 會話級別的挑戰.遇到這些問題的時候NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, or NSURLAuthenticationMethodServerTrust,NSURLSession對象會調用代理方法URLSession:didReceiveChallenge:completionHandler:,如果沒有實現會話的這個代理方法,會話會回調URLSession:task:didReceiveChallenge:completionHandler:去處理.
  • 非會話級別的挑戰.NSURLSession類會調用代理方法URLSession:task:didReceiveChallenge:completionHandler:.如果應用提供了會話的代理,我們需要處理針對每個會話單獨處理鑑權.這個時候代理方法URLSession:didReceiveChallenge:completionHandler:不會被調用.

處理iOS後臺活動

如果使用NSURLSession,當後臺下載任務完成的時候會在後臺啓動app,代理方法application:handleEventsForBackgroundURLSession:completionHandler:負責重新創建合適的會話和保存回調.

後臺下載的代理方法

#if TARGET_OS_IPHONE
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    NSLog(@"Background URL session %@ finished events.\n", session);

    if (session.configuration.identifier)
        [self callCompletionHandlerForSession: session.configuration.identifier];
}

- (void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier
{
    if ([ self.completionHandlerDictionary objectForKey: identifier]) {
        NSLog(@"Error: Got multiple handlers for a single session identifier.  This should not happen.\n");
    }

    [ self.completionHandlerDictionary setObject:handler forKey: identifier];
}

- (void) callCompletionHandlerForSession: (NSString *)identifier
{
    CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];

    if (handler) {
        [self.completionHandlerDictionary removeObjectForKey: identifier];
        NSLog(@"Calling completion handler.\n");

        handler();
    }
}
#endif
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
    NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: identifier];

    NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self.mySessionDelegate delegateQueue: [NSOperationQueue mainQueue]];

    NSLog(@"Rejoining session %@\n", identifier);

    [ self.mySessionDelegate addCompletionHandler: completionHandler forSession: identifier];
}

使用NSURLConnection

使用 NSURLDownload

NSURLDownload只適用在OSX,在iOS不支持.

URL數據編碼

使用基礎框架的方法CFURLCreateStringByAddingPercentEscapesCFURLCreateStringByReplacingPercentEscapesUsingEncoding來進行URL編碼. 這些方法允許我們制定額外的字符列表.

按照 RFC 3986, 在URL保留字爲:

    reserved    = gen-delims / sub-delims

      gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"

      sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
                  / "*" / "+" / "," / ";" / "="

utf-8編碼的字符串如下:

CFStringRef originalString = ...

CFStringRef encodedString = CFURLCreateStringByAddingPercentEscapes(
    kCFAllocatorDefault,
    originalString,
    NULL,
    CFSTR(":/?#[]@!$&'()*+,;="),
    kCFStringEncodingUTF8);

解碼

CFStringRef decodedString = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(
    kCFAllocatorDefault,
    encodedString,
    CFSTR(""),
    kCFStringEncodingUTF8);

處理重定向和其他的請求改變

當服務器認定一個請求需要客戶端重新創建一個新的不同的請求的時候回產生重定向.NSURLSessionNSURLConnection會通過代理方法通知代理.

爲了處理重定向,代理必須實現下面幾個方法:

  • 對於NSURLSession,實現URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:方法
  • 對於NSURLConnection,實現connection:willSendRequest:redirectResponse:方法.

在這些方法中,代理可以檢查新的請求,導致重定向的響應,也可以通過回調塊返回一個新的請求.

代理可以做:

  • 允許重定向,簡單的返回提供的請求.
  • 返回創建的一個新的請求
  • 拒絕重定向

另外,代理可以取消重定向和連接.使用NSURLSession的話,代理任務的cancel方法來取消.使用NSURLConnection或者NSURLDownload,代理調用NSURLConnectionNSURLDownloadcancel方法.

如果NSURLProtocol子類處理了請求,爲了標準化格式而改邊了請求,代理也可以在connection:willSendRequest:redirectResponse方法裏收到消息.比如:將http://www.apple.com改成http://www.apple.com/. 這是因爲標準化的需求,或者請求使用的緩存版本問題.

#if FOR_NSURLSESSION
- (void)URLSession:(NSURLSession *)session
        task:(NSURLSessionTask *)task
        willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse
        newRequest:(NSURLRequest *)request
        completionHandler:(void (^)(NSURLRequest *))completionHandler
#elif FOR_NSURLCONNECTION
-(NSURLRequest *)connection:(NSURLConnection *)connection
            willSendRequest:(NSURLRequest *)request
           redirectResponse:(NSURLResponse *)redirectResponse
#else // FOR_NSURLDOWNLOAD
-(NSURLRequest *)download:(NSURLConnection *)connection
            willSendRequest:(NSURLRequest *)request
           redirectResponse:(NSURLResponse *)redirectResponse
#endif
{
    NSURLRequest *newRequest = request;
    if (redirectResponse) {
        newRequest = nil;
    }

#if FOR_NSURLSESSION
    completionHandler(newRequest);
#else
    return newRequest;
#endif
}

如果所有的重定向相關的代理方法都沒有實現,默認所有的改變都被允許.

認證挑戰和TLS鏈驗證

一個NSURLRequest對象會經常遇到認證挑戰,或者需要連接服務器任務.NSURLSessionNSURLConnection類會在面臨認證挑戰的時候通知代理方法.

決定如何響應認證挑戰

如果一個請求需要認證,反饋給app的方式取決於這個請求使用的是哪個方式.NSURLSession還是NSURLConnection

  • 如果使用NSURLSession,所有的認證消息會通知代理方法.
  • 如果使用NSURLConnection或者NSURLDownload.代理會收到connection:canAuthenticateAgainstProtectionSpace:或者download:canAuthenticateAgainstProtectionSpace:消息.這樣允許app在嘗試認證之前分析服務器的協議,認證方式等.如果app沒有準備好認證,返回NO.這樣系統嘗試從用戶的鑰匙串中查找認證信息.
  • 如果代理沒有實現connection:canAuthenticateAgainstProtectionSpace:或者download:canAuthenticateAgainstProtectionSpace:方法,系統使用客戶端證書認證.

下一步,如果代理同意處理認證,並且沒有可以用的認證信息,代理會受到下面的某種消息:

 URLSession:didReceiveChallenge:completionHandler:
URLSession:task:didReceiveChallenge:completionHandler:
connection:didReceiveAuthenticationChallenge:
download:didReceiveAuthenticationChallenge:

爲了繼續連接,代理有三種選擇:
- 提供一個認證信息
- 嘗試沒有認證的連接
- 取消認證挑戰

爲了幫助認證挑戰,NSURLAuthenticationChallenge對象的方法包含了關於觸發認證挑戰的信息,嘗試認證挑戰的次數和之前的認證證書.

如果認證失敗了(比如用戶改了密碼),可以使用屬性proposedCredential來獲取認證挑戰.代理方法可以使用這個入口來給用戶提示.

通過previousFailureCount屬性可以獲取之前認證嘗試的次數.代理可以將信息展示給用戶,用戶可以看到之前是否失敗或者是否到達最大嘗試次數.

響應認證挑戰

通過代理方法connection:didReceiveAuthenticationChallenge來回應認證信息有三種方式.

提供一個認證

爲了嘗試認證,應用需要創建一個NSURLCredential對象,包含服務器需要的信息.我們可以通過調用authenticationMethod方法來獲取訪問認證挑戰的保護區內容.

  • HTTP基本的認證(NSURLAuthenticationMethodHTTPBasic)需要用戶名和密碼.應用通過方法credentialWithUser:password:persistence:創建一個NSURLCredential對象提示用戶輸入信息.
  • HTTP的摘要認證(NSURLAuthenticationMethodHTTPDigest),和基本認證一樣,需要用戶名和密碼.
  • 客戶端認證(NSURLAuthenticationMethodClientCertificate)需要系統標識和服務器需要的所有證書.通過credentialWithIdentity:certificates:persistence:來創建NSURLCredential對象.
  • 服務器信任認證(NSURLAuthenticationMethodServerTrust)需要提供一個認證挑戰的保護空間.通過credentialForTrust:來創建一個NSURLCredential對象.

當我們創建了NSURLCredential對象之後
- 對於NSURLSession對象,通過提供的回調方法傳遞回去.
- 對於NSURLConnectionNSURLDownload通過useCredential:forAuthenticationChallenge:傳回去.

不認證,繼續執行

如果代理選擇了不提供認證,需要
- 對於NSURLSession,傳遞下面的其中一個值回去.
-NSURLSessionAuthChallengePerformDefaultHandling告訴NSURLSession代理沒有提供一個方法來處理這個認證挑戰.
- NSURLSessionAuthChallengeRejectProtectionSpace拒絕了這次挑戰.這個取決於服務器返回的響應類型,URL加載肯可能會調用多次這個方法來獲取另外的保護空間.
- 對於NSURLConnectionNSURLDownload調用continueWithoutCredentialsForAuthenticationChallenge方法.

取決於協議的實現方式,繼續不認證可能導致連接失敗,會產生一個connectionDidFailWithError消息,或者返回一個不需要認證的內容.

取消連接

代理同樣可以選擇取消認證挑戰

  • 對於NSURLSession在回調塊傳遞NSURLSessionAuthChallengeCancelAuthenticationChallenge
  • 對於NSURLConnectionNSURLDownload,調用cancelAuthenticationChallenge方法.用戶會收到connection:didCancelAuthenticationChallenge:消息,給用戶反饋的機會.

一個認證的例子

下面的例子顯示了認證,創建一個NSURLCredential對象,使用用戶名和密碼認證.如果之前認證失敗,會取消認證並提示給用戶.

-(void)connection:(NSURLConnection *)connection
        didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if ([challenge previousFailureCount] == 0) {
        NSURLCredential *newCredential;
        newCredential = [NSURLCredential credentialWithUser:[self preferencesName]
                                                 password:[self preferencesPassword]
                                              persistence:NSURLCredentialPersistenceNone];
        [[challenge sender] useCredential:newCredential
               forAuthenticationChallenge:challenge];
    } else {
        [[challenge sender] cancelAuthenticationChallenge:challenge];
        // inform the user that the user name and password
        // in the preferences are incorrect
        [self showPreferencesCredentialsAreIncorrectPanel:self];
    }
}

如果代理沒有實現connection:didReceiveAuthenticationChallenge方法,並且請求需要認證,驗證認證需要已經可用的認證存儲或者通過URL提供的信息.如果沒有可用的信息,認證失敗.默認的消息continueWithoutCredentialForAuthenticationChallenge會被實現.

自定義TLS鏈認證

在NSURL的接口中,代理方法處理了應用的TLS鏈認證.除了提供給服務器用戶的認證信息,應用也要檢查在TLS握手過程中服務器的認證信息,然後告訴URL加載系統是否接受或者拒絕認證.

如果提供了非正式的鏈認證方式(比如自己簽名的證書),我們可以:

  • 對於NSURLSession,實現URLSession:didReceiveChallenge:completionHandler:或者URLSession:task:didReceiveChallenge:completionHandler:的代理方法.如果兩個方法都實現了, 會話級別的方法負責處理認證,也就是第一個方法.
  • 對於NSURLConnectionNSURLDownload,實現connection:canAuthenticateAgainstProtectionSpace:download:canAuthenticateAgainstProtectionSpace :方法,如果在保護空間中有認證類型爲NSURLAuthenticationMethodServerTrust,返回YES.

然後實現connection:didReceiveAuthenticationChallenge:download:didReceiveAuthenticationChallenge:方法處理認證.

在我們處理認證的代理方法中,我們需要檢查是否在挑戰保護空間中存在認證類型爲NSURLAuthenticationMethodServerTrust,如果存在,我們可以獲取那些信息.

理解緩存方法

URL加載系統提供了一個符合的存儲方式:磁盤和內存.緩存可以讓應用根據網絡連接來選擇重複使用數據,來提高性能.

在請求中使用緩存

一個請求NSURLRequest對象通過緩存策略屬性NSURLRequestCachePolicy的值來覺得如何使用緩存.這些值有:NSURLRequestUseProtocolCachePolicy,NSURLRequestReloadIgnoringCacheData,NSURLRequestReturnCacheDataElseLoad,NSURLRequestReturnCacheDataDontLoad.

默認的緩存策略是NSURLRequestUseProtocolCachePolicy,它的緩存取決於代理方法的實現.

設置了NSURLRequestReloadIgnoringCacheData值,URL加載系統會忽略緩存,重新請求數據.

設置了NSURLRequestReturnCacheDataElseLoad值,URL加載系統會使用緩存數據,而且會忽略緩存的時間和過期時間,只有在沒有緩存的時候才請求數據.

設置NSURLRequestReturnCacheDataDontLoad值,URL加載系統只會返回緩存數據,不會發起請求.這個有點像離線模式.

目前,只有HTTP和HTTPS的請求會被緩存.

緩存使用HTTP協議的語義

大多數複雜的使用場景是在HTTP請求並設置了NSURLRequestUseProtocolCachePolicy緩存策略.

如果這個請求沒有對頂的NSCachedURLResponse對象,會發起請求,獲取數據.

如果存在一個請求的NSCachedURLResponse緩存數據,URL加載系統會檢查內容是否需要重新驗證.

如果內容需要重新驗證,URL加載系統會生成一個HEAD請求,發送請求到服務器查看內容是否已經改變.如果沒改變,就是用緩存的內容.如果改變了就重新發起數據請求.

如果內容不需要重新驗證,URL加載系統會檢查最大的時間或過期時間.如果內容過期了,URL加載系統也會生成一個HEAD請求,發送請求到服務器查看內容是否已經改變.

通過編程控制緩存

默認,請求的數據緩存是依據請求的緩存策略的,但是可以通過子類的NSURLProtocol協議來控制.

如果應用需要更精確的緩存控制.應用可以實現代理方法,在發送請求之前來決定返回數據是否要緩存.

  • 對於NSURLSession數據和上傳任務,實現URLSession:dataTask:willCacheResponse:completionHandler:方法.這個方法只在數據和上傳任務的時候調用.下載任務的緩存是根據緩存策略的.
  • 對於NSURLConnection,實現connection:willCacheResponse:方法.

對於NSURLSession,在代理方法裏調用回調塊告訴會話哪些內容緩存,對於NSURLConnection,代理方法返回要緩存的數據.

代理方法可能返回下面的一種值:
- 返回一個允許緩存的對象
- 從一個響應對象創建出的一個新的響應對對象–比如一個緩存策略是隻緩存到內存中.
- NULL代表不緩存

我們的代理方法可以在NSCachedURLResponse對象的userInfo字典中插入自定義對象.

注意:如果使用了NSURLSession而且實現了代理方法,代理方法需要調用回調塊.否則,會產生內存泄露.

下面的例子阻止了HTTPS的磁盤緩存,也添加了數據到緩存中.

-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
                 willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    NSCachedURLResponse *newCachedResponse = cachedResponse;

    NSDictionary *newUserInfo;
    newUserInfo = [NSDictionary dictionaryWithObject:[NSDate date]
                                                 forKey:@"Cached Date"];
    if ([[[[cachedResponse response] URL] scheme] isEqual:@"https"]) {
#if ALLOW_IN_MEMORY_CACHING
        newCachedResponse = [[NSCachedURLResponse alloc]
                                initWithResponse:[cachedResponse response]
                                    data:[cachedResponse data]
                                    userInfo:newUserInfo
                                    storagePolicy:NSURLCacheStorageAllowedInMemoryOnly];
#else // !ALLOW_IN_MEMORY_CACHING
        newCachedResponse = nil
#endif // ALLOW_IN_MEMORY_CACHING
    } else {
        newCachedResponse = [[NSCachedURLResponse alloc]
                                initWithResponse:[cachedResponse response]
                                    data:[cachedResponse data]
                                    userInfo:newUserInfo
                                    storagePolicy:[cachedResponse storagePolicy]];
    }
    return newCachedResponse;
}

Cookie和自定義協議

如果應用需要管理cookie.

比如添加或刪除指定的cookie.

Cookie存儲

由於HTTP的無狀態性質,客戶端經常使用Cookie來存儲特定的URL對應的數據.URL加載系統提供了接口來創建和管理Cookie,作爲請求的一部分或者響應服務器的一部分.

NSHTTPCookie是Cookie的包裝類.提供了訪問Cookie屬性的方法.也提供了從HTTP Cookie 頭部信息轉換爲NSHTTPCookie對象,或者從NSHTTPCookie對象轉換爲適合的NSURLRequest請求.URL加載系統自動的發送與請求適合的Cookie,除非請求指定了不發送Cookie.另外Cookie從NSURLResponse返回的策略與當前Cookie策略一致.

NSHTTPCookieStorage提供了管理所有應用共享的NSHTTPCookie對象.

在iOS中,Cookie不能在應用中共享.

NSHTTPCookieStorage類允許應用設置Cookie的接受策略.接受策略控制着Cookie是否一直被接受或者拒絕.

改變Cookie接受策略會影響所有的正在運行的應用

當一個應用改變了Cookie管理接受策略,NSHTTPCookieStorage會發送NSHTTPCookieManagerCookiesChangedNotificationNSHTTPCookieStorageAcceptPolicyChangedNotification通知.

協議的支持

URL加載系統設計成允許應用擴展協議來支持數據傳輸.URL加載系統原生支持http,https,file,ftp,data協議.

我們可以創建一個NSURLProtocol的子類,然後通過NSURLProtocol的方法registerClass註冊.當NSURLSession,NSURLConnection,NSURLDownload對象爲了一個請求對象創建的時候,URL加載系統會按照註冊順序的倒序來遍歷註冊類.遇到的第一個canInitWithRequest方法返回YES的類去處理這個請求.

如果自定義的協議需要爲請求或相應添加屬性,需要創建NSURLRequest, NSMutableURLRequest,和 NSURLResponse類來提供訪問方法.NSURLProtocol類負責設置和獲取這些屬性.

NSURLProtocol的子類是被URL加載系統初始化的,系統會提供一個實現NSURLProtocolClient協議的方法.NSURLProtocol子類從NSURLProtocolClient協議發送消息給實現這個協議的類,來告訴URL加載系統一些動作:收到數據,重定位一個新的地址,完成加載等.如果子類支持認證,必須實現NSURLAuthenticationChallengeSender協議.

URL Session 生命週期

我們有兩種方式使用 NSURLSession 接口:使用系統提供的代理和使用自己的代理.通常,如果需要做下面的事情,就需要自己實現代理:
- 當應用沒有運行的時候,使用後臺下載或上傳數據.
- 自定義任務
- 自定義SSL證書認證
- 數據服務器返回的MIME類型判斷下載的數據否存儲到磁盤
- 上傳數據使用流對象
- 控制緩存大小
- 控制HTTP重定向

如果我們的應用不需要這些功能,使用系統的代理就可以了.根據實現方式的不同,查看不同的內容:

系統提供的代理方式下會話的生命週期

我們經常使用系統的代理方式使用會話.如果需要使用後臺上傳和下載,或者需要處理認證或者緩存,就需要提供一個代理,實現一些會話的協議,任務的協議或者兩者都有.這些代理有很多用處:
- 當使用下載任務時,NSURLSession對象使用代理方法告訴代理下載的數據文件存放的位置. 如果代理需要後臺下載和上傳消息,必須提供NSURLSessionDownloadDelegate的所有方法實現.
- 代理可以處理部分認證挑戰
- 代理提供了請求體基於data上傳的方式
- 代理可以判斷HTTP是否需要重定向
- NSURLSession對象通過代理方法告訴代理每一個傳輸的狀態.數據任務的代理方法會告訴代理每一份數據從服務器返回的.
- 當傳輸結束時,結束的代理方法會被調用
- 當應用不需要會話的時候,通過invalidateAndCancel方法和finishTasksAndInvalidate方法結束.

NSURLSession對象不會通過error參數傳遞服務端的錯誤,這個error都是客戶端的錯誤,比如無法解析域名,連接失敗.服務端的錯誤都是通過NSHTTPURLResponse對象的HTTP碼來傳遞的.

自定義代理方式下會話的生命週期

我們可以使用系統提供的代理,也可以自定義代理.如果需要處理後臺下載和上傳,鑑權或者緩存,就必須設置自己的代理,實現相關方法.

  1. 創建一個會話的配置對象.對於後臺任務的會話,需要有一個唯一的標識,應用保存這個唯一標識,當應用crash或者關閉的時候,重啓後與相應的任務做關聯.
  2. 用這個配置對象創建一個會話.
  3. 爲每一個資源請求創建任務對象放到會話裏.任務對象初始化是掛起的狀態,需要應用調用resume方法開啓任務執行. 任務對象都是根據用途來繼承自NSURLSessionTask—NSURLSessionDataTask, NSURLSessionUploadTask, 或者 NSURLSessionDownloadTask類.這些任務對象和NSURLConnection比較像,但是有更多的控制方法.通常我們會往會話裏放置多個任務對象,這裏我們描述的是一個任務對象的生命週期.
  4. 如果遠程服務端返回的code碼錶示需要認認證並且是連接時的認證挑戰(比如SSL證書),NSURLSession會調用認證代理髮放.
    • 對於會話級別的挑戰-NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, or NSURLAuthenticationMethodServerTrust會話會調用URLSession:didReceiveChallenge:completionHandler:方法.如果代理沒有實現會話的代理方法,會話對象會調用URLSession:task:didReceiveChallenge:completionHandler:方法.
    • 對於非會話級別的認證挑戰,會話會調用URLSession:task:didReceiveChallenge:completionHandler:方法來處理認證挑戰.如果代理實現了會話級別的認證方法,應用必須處理來自會話級別和任務級別的兩種認證信息.URLSession:didReceiveChallenge:completionHandler:代理方法不會在非會話級別的認證時候調用.如果一個上傳任務的認證失敗了,而且任務是通過流對象上傳數據的,會調用URLSession:task:needNewBodyStream:方法,代理需要爲新的請求提供一個NSInputStream對象.
  5. 對於HTTP響應的重定向跳轉,會話的代理方法會調用URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:方法.代理方法的實現需要返回一個新的請求對象(NSURLRequest),或者返回nil表示不跳轉.
    • 如果代理方法實現了,流程迴轉到步驟4
    • 如果代理沒有實現這個方法,重定向會遵從最大的重定向數來跳轉.
  6. 對於下載的任務對象會調用downloadTaskWithResumeData:downloadTaskWithResumeData:completionHandler:方法,會話對象會調用URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:方法來處理.
  7. 對於一個數據任務對象,會話對象會調用URLSession:dataTask:didReceiveResponse:completionHandler:方法來確定是否要將數據任務轉換成下載任務轉.如果應用選擇轉換成下載任務,會話對象會調用URLSession:dataTask:didBecomeDownloadTask:方法並傳遞一個下載任務的對象.這個方法調用完之後,下載任務對象的代理方法開始被調用來接收下載的數據.
  8. 如果任務是通過方法uploadTaskWithStreamedRequest:來創建的,會話的代理方法URLSession:task:needNewBodyStream:來獲取數據.
  9. 在上傳任務的請求體中,代理方法URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:會被週期性的調用來報告上傳的進度.
  10. 在給服務器傳輸數據的時候,代理方法都會週期性的收到傳輸數據的進度情況.對於下載任務,方法URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:會調用.對於數據任務,方法URLSession:dataTask:didReceiveData:會被調用.如果我們需要取消一個下載任務,通過調用方法cancelByProducingResumeData:.如果之後還需要繼續下載數據,要調用方法downloadTaskWithResumeData:downloadTaskWithResumeData:completionHandler:來創建一個新的下載任務繼續下載.
  11. 對於數據任務,會話調用URLSession:dataTask:willCacheResponse:completionHandler:方法來確定是否要緩存.如果沒有實現這個方法,默認使用會話中配置對象的緩存策略.
  12. 如果下載任務完成了,會話對象調用URLSession:downloadTask:didFinishDownloadingToURL:方法並提供數據的臨時文件位置.應用必須在這個方法返回之前來讀取或移動下載結果,代理方法返回後,臨時文件就會被移除.
  13. 當任務完成後,當發生錯誤的時候,代理方法會調用URLSession:task:didCompleteWithError:方法來處理.如果任務失敗,大多數應用匯重試,直到用戶取消下載或者服務器返回一個錯誤表示下載不會成功.應用不會立即重試,應該根據網絡情況和服務器是否可連接來決定是否要重試.如果下載任務失敗了,但是可以被繼續下載,那麼會在代理方法NSError對象中userInfo字典中包含key爲NSURLSessionDownloadTaskResumeData的數據.應用可以使用方法downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler:並提供這個數據來繼續下載.如果任務不能被繼續下載,應用需要重新創建一個新的下載任務,步驟會跳轉到3.
  14. 如果請求響應是一個被編碼的多個部分,會話會調用didReceiveResponse方法多次.這時候步驟跳轉到7.
  15. 當不在需要會話的時候,通過方法invalidateAndCancel或者finishTasksAndInvalidate來取消會話.在取消會話之後,代理方法URLSession:didBecomeInvalidWithError:會調用.如果任務正在下載而被我們取消了,會話會調用URLSession:task:didCompleteWithError:來報告這個錯誤.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章