详细解析几个和网络请求有关的类(六) —— 使用NSURLSession(二)

版本记录

版本号 时间
V1.0 2018.03.10

前言

我们做APP发起网络请求,一般都是使用框架,这些框架的底层也都是苹果的API,接下来几篇就一起来看一下和网络有关的几个类。感兴趣的可以看上面几篇文章。
1. 详细解析几个和网络请求有关的类 (一) —— NSURLSession
2. 详细解析几个和网络请求有关的类(二) —— NSURLRequest和NSMutableURLRequest
3. 详细解析几个和网络请求有关的类(三) —— NSURLConnection
4. 详细解析几个和网络请求有关的类(四) —— NSURLSession和NSURLConnection的区别
5. 详细解析几个和网络请求有关的类(五) —— 关于NSURL加载系统(一)

回顾

上一篇主要介绍了关于URL系统这个苹果框架的概览和描述,包括URL加载系统的各个组成部分。本篇主要介绍一个关于NSURLSession的使用。


Using NSURLSession - 使用NSURLSession

NSURLSession类和相关的类提供了一个通过HTTP下载内容的API。此API提供了一组丰富的代理方法来支持身份验证,并使您的应用能够在应用未运行时执行后台下载,或者在iOS中暂停应用时执行后台下载。

要使用NSURLSession API,您的应用程序会创建一系列会话,每个会话都会协调一组相关的数据传输任务。例如,如果您正在编写Web浏览器,则您的应用程序可能会为每个选项卡或窗口创建一个会话。在每个会话中,您的应用程序会添加一系列任务,其中每个任务均表示对特定网址的请求(如果原始网址返回HTTP重定向,则表示任何后续网址)。

像大多数网络API一样,NSURLSession API是高度异步的。如果您使用默认的系统提供的代理,则必须提供完成处理程序块,以便在传输成功完成或发生错误时将数据返回到您的应用程序。或者,如果您提供自己的自定义委托对象,那么任务对象会调用这些委托的方法并传递从服务器接收的数据(或者,在传输完成时用于文件下载)。

注意:完成回调主要是作为使用自定义代理的替代方案。如果使用接受完成回调的方法创建任务,则不会调用响应和数据传递的代理方法。

NSURLSession API除了将这些信息传递给委代理之外,还提供状态和进度属性。它支持取消,重新启动(恢复)和暂停任务,并且可以在停止的地方恢复已暂停,取消或失败的下载。


Understanding URL Session Concepts - 理解URL Session的概念

会话中任务的行为取决于三件事情:会话类型(由用于创建会话的配置对象的类型决定),任务类型以及创建任务时应用程序是否处于前台。

1. Types of Sessions - 会话类型

NSURLSession API支持三种类型的会话,这取决于用于创建会话的配置对象的类型:

  • 默认会话Default sessions的行为与其他Foundation下载URL的方法类似。他们使用永久性的基于磁盘的缓存并将凭证存储在用户的钥匙串中。
  • 临时会话Ephemeral sessions不会将任何数据存储到磁盘;所有的缓存,凭证存储等都保存在RAM中并与会话绑定。因此,当您的应用程序使会话失效时,它们会自动清除。
  • 后台会话Background sessions与默认会话类似,不同之处在于单独的进程处理所有数据传输。后台会话有一些额外的限制,如Background Transfer Considerations中所述。

2. Types of Tasks - 任务类型

在会话中,NSURLSession类支持三种类型的会话:数据任务,下载任务和上载任务。

  • 数据任务使用NSData对象发送和接收数据。数据任务适用于从应用程序到服务器的简短交互式请求。数据任务可以在接收每一条数据后一次将数据返回给您的应用程序,或者通过完成处理程序一次全部返回数据。
  • 下载任务以文件形式检索数据,并在应用程序未运行时支持后台下载。
  • 上传任务以文件的形式发送数据,并在应用程序未运行时支持后台上传。

3. Background Transfer Considerations - 后台传输注意事项

当您的应用程序被暂停的时候,NSURLSession类支持后台传输。后台传输仅由使用后台会话配置对象(通过调用backgroundSessionConfiguration:)返回的会话创建)。

对于后台会话,由于实际传输是由单独的进程执行的,因为重新启动应用程序的过程相对昂贵,所以有几项功能不可用,导致以下限制:

  • session必须提供事件传递的代理。 (对于上传和下载,代理的行为与进行中的传输相同。)
  • 只支持HTTP和HTTPS协议(没有自定义协议)。
  • 重定向总是被遵循。
  • 只支持从文件上传任务(在程序退出后,从数据对象上传数据或流将失败)。
  • 如果在应用程序处于后台时启动后台传输,则配置对象的discretionary属性将被视为是true的。

注意:在iOS 8和OS X 10.10之前,后台会话不支持数据任务。

iOS和OS X之间重新启动时应用的行为方式略有不同。

在iOS中,当后台传输完成或需要证书时,如果您的应用程序不再运行,iOS会自动在后台重新启动您的应用程序,并在应用程序的UIApplicationDelegate对象中调用application:handleEventsForBackgroundURLSession:completionHandler:方法。此调用提供了导致您的应用程序启动的会话的标识符。您的应用程序应该存储该完成处理程序,使用相同的标识符创建后台配置对象,并使用该配置对象创建会话。新会话将自动与正在进行的背景活动重新关联。稍后,当会话完成最后一个后台下载任务时,它会向会话委托发送URLSessionDidFinishEventsForBackgroundURLSession:消息。在该代理方法中,调用主线程上先前存储的完成处理程序,以便操作系统知道再次挂起应用程序是安全的。

在iOS和OS X中,当用户重新运行应用程序时,应用程序应立即创建具有与应用程序上次运行时的未完成任务的任何会话具有相同标识符的后台配置对象,然后为每个配置对象创建一个会话。这些新会话同样会自动与正在进行的背景活动重新关联。

注意:每个标识符必须创建一个会话(在创建配置对象时指定)。 共享相同标识符的多个会话的行为未定义。

如果任何任务在您的应用程序被暂停时完成,则代理的URLSession:downloadTask:didFinishDownloadingToURL: 方法将随任务和与其关联的新下载文件的URL一起调用。

同样,如果任何任务需要证书,则NSURLSession对象会根据需要调用委托的URLSession:task:didReceiveChallenge:completionHandler:方法或 URLSession:didReceiveChallenge:completionHandler:方法。

网络错误后,URL加载系统会自动重试后台会话中的上传和下载任务。 没有必要使用reachability API来确定何时重试失败的任务。

有关如何使用NSURLSession进行后台传输的示例,请参阅Simple Background Transfer

4. Life Cycle and Delegate Interaction - 生命周期和代理交互

根据您对NSURLSession类所做的工作,可能有助于全面了解会话生命周期,包括会话与其代理进行交互的方式,代理调用的顺序,服务器返回重定向时发生的情况, 当您的应用恢复失败的下载时会发生什么,等等。

有关URL会话生命周期的完整说明,请阅读Life Cycle of a URL Session

5. NSCopying Behavior - NSCopying 行为

会话和任务对象符合NSCopying协议,如下所示:

  • 当您的应用程序复制会话或任务对象时,会返回相同的对象。
  • 当你的应用程序复制一个配置对象时,你会得到一个你可以独立修改的新副本。

Sample Delegate Class Interface - 代理类接口

以下任务部分中的代码片段基于Listing 1-1中显示的类接口。

// Listing 1-1  Sample delegate class interface
 
@import Foundation;
 
NS_ASSUME_NONNULL_BEGIN
typedef void (^CompletionHandler)();
 
@interface MySessionDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, NSURLSessionStreamDelegate>
 
@property NSMutableDictionary <NSString *, CompletionHandler>*completionHandlers;
 
@end
NS_ASSUME_NONNULL_END
import Foundation
 
typealias CompletionHandler = () -> Void
 
class MySessionDelegate : NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate {
    var completionHandlers: [String: CompletionHandler] = [:]
}

Creating and Configuring a Session - 创建和配置Session

NSURLSession API提供了广泛的配置选项:

  • 专用于单个会话的专用存储支持缓存,Cookie,凭证和协议
  • 身份验证,绑定到特定请求(任务)或一组请求(会话)
  • 通过URL上传和下载文件,鼓励将数据(文件内容)与元数据(URL和设置)分开
  • 配置每台主机的最大连接数
  • 如果无法在一定时间内下载整个资源,则会触发资源超时
  • 最小和最大TLS版本支持
  • 自定义代理字典
  • 控制cookie策略
  • 控制HTTP流水线行为

由于大多数设置都包含在单独的配置对象中,因此您可以重复使用常用的设置。在实例化会话对象时,您需要指定以下内容:

  • 管理该会话及其中任务的行为的配置对象
  • 可选地,委托对象处理接收到的数据并处理特定于会话及其中的任务的其他事件,如服务器身份验证,确定资源下载请求是否应转换为下载等等。

如果您不提供委托,则NSURLSession对象使用系统提供的委托。通过这种方式,您可以轻松使用NSURLSession代替在NSURLSession上使用sendAsynchronousRequest:queue:completionHandler:方法的现有代码。

注意:如果您的应用需要执行后台传输,则必须提供自定义代理。

在实例化会话对象后,不能在不创建新会话的情况下更改配置或代理。

Listing 1-2显示了如何创建正常,临时和后台会话的示例。

// Creating session configurations
NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSessionConfiguration *ephemeralConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSessionConfiguration *backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: @"com.myapp.networking.background"];
 
// Configuring caching behavior for the default session
NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *cachePath = [cachesDirectory stringByAppendingPathComponent:@"MyCache"];
 
/* Note:
 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_OSX
cachePath = [cachePath stringByStandardizingPath];
#endif
 
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:16384 diskCapacity:268435456 diskPath:cachePath];
defaultConfiguration.URLCache = cache;
defaultConfiguration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
 
// Creating sessions
id <NSURLSessionDelegate> delegate = [[MySessionDelegate alloc] init];
NSOperationQueue *operationQueue = [NSOperationQueue mainQueue];
 
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfiguration delegate:delegate operationQueue:operationQueue];
NSURLSession *ephemeralSession = [NSURLSession sessionWithConfiguration:ephemeralConfiguration delegate:delegate delegateQueue:operationQueue];
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:delegate delegateQueue:operationQueue];
// Creating session configurations
let defaultConfiguration = URLSessionConfiguration.default()
let ephemeralConfiguration = URLSessionConfiguration.ephemeral()
let backgroundConfiguration = URLSessionConfiguration.backgroundSessionConfiguration(withIdentifier: "com.myapp.networking.background")
 
// Configuring caching behavior for the default session
let cachesDirectoryURL = FileManager.default().urlsForDirectory(.cachesDirectory, inDomains: .userDomainMask).first!
let cacheURL = try! cachesDirectoryURL.appendingPathComponent("MyCache")
var diskPath = cacheURL.path
 
/* Note:
 iOS requires the cache path to be
 a path relative to the ~/Library/Caches directory,
 but OS X expects an absolute path.
 */
#if os(OSX)
diskPath = cacheURL.absoluteString
#endif
 
let cache = URLCache(memoryCapacity:16384, diskCapacity: 268435456, diskPath: diskPath)
defaultConfiguration.urlCache = cache
defaultConfiguration.requestCachePolicy = .useProtocolCachePolicy
// Creating sessions
let delegate = MySessionDelegate()
let operationQueue = OperationQueue.main()
 
let defaultSession = URLSession(configuration: defaultConfiguration, delegate: delegate, delegateQueue: operationQueue)
let ephemeralSession = URLSession(configuration: ephemeralConfiguration, delegate: delegate, delegateQueue: operationQueue)
let backgroundSession = URLSession(configuration: backgroundConfiguration, delegate: delegate, delegateQueue: operationQueue)

除后台配置外,您可以重新使用会话配置对象来创建其他会话。 (您不能重复使用后台会话配置,因为共享相同标识符的两个后台会话对象的行为未定义。)

您也可以随时安全地修改配置对象。 在创建会话时,会话会在配置对象上执行深层复制,因此修改只会影响新会话,而不会影响现有会话。 例如,您可以为仅在您使用Wi-Fi连接时才能检索的内容创建第二个会话,如Listing 1-3所示。

ephemeralConfiguration.allowsCellularAccess = NO;
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration:ephemeralConfiguration delegate:delegate delegateQueue:operationQueue];
ephemeralConfiguration.allowsCellularAccess = false
let ephemeralSessionWiFiOnly = URLSession(configuration: ephemeralConfiguration, delegate: delegate, operationQueue: operationQueue)

Fetching Resources Using System-Provided Delegates - 使用系统提供的代理获取响应

使用NSURLSession最直接的方法是使用系统提供的代理来请求资源。 使用这种方法,您只需在应用程序中提供两段代码:

  • 代码根据该对象创建配置对象和会话
  • 完成处理程序例程,用于在数据完全接收后执行某些操作

使用系统提供的委托,每个请求只需一行代码即可获取特定的URL。 Listing 1-4显示了这个简化形式的一个例子。

注意:系统提供的代理仅提供有限的网络行为定制。 如果您的应用程序有超出基本URL抓取的特殊需求,例如自定义身份验证或后台下载,则此技术不适用。 有关您必须实施完整代理的情况的完整列表,请参阅Life Cycle of a URL Session

// Listing 1-4  Requesting a resource using system-provided delegates

NSURLSession *sessionWithoutADelegate = [NSURLSession sessionWithConfiguration:defaultConfiguration];
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
 
[[sessionWithoutADelegate dataTaskWithURL:url 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];
let sessionWithoutADelegate = URLSession(configuration: defaultConfiguration)
if let url = URL(string: "https://www.example.com/") {
    (sessionWithoutADelegate.dataTask(with: url) { (data, response, error) in
        if let error = error {
            print("Error: \(error)")
        } else if let response = response,
            let data = data,
            let string = String(data: data, encoding: .utf8) {
            print("Response: \(response)")
            print("DATA:\n\(string)\nEND DATA\n")
        }
    }).resume()
}

Fetching Data Using a Custom Delegate - 使用自定义代理获取数据

如果您使用自定义代理来检索数据,那么代理必须至少实现以下方法:

如果您的应用需要在其URLSession:dataTask:didReceiveData:方法返回后使用数据,则您的代码负责以某种方式存储数据。

例如,Web浏览器可能需要在数据到达时随同它之前收到的任何数据一起呈现数据。 为此,它可能会使用将任务对象映射到NSMutableData对象以存储结果的字典,然后使用该对象上的appendData:方法追加新接收的数据。

Listing 1-5显示了如何创建和启动数据任务

NSURL *url = [NSURL URLWithString: @"https://www.example.com/"];
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithURL:url];
[dataTask resume];
if let url = URL(string: "https://www.example.com/") {
    let dataTask = defaultSession.dataTask(with: url)
    dataTask.resume()
}

Downloading Files - 下载文件

在高层次上,下载文件与检索数据类似。 你的应用应该实现下面的委托方法:

重要提示:在此方法返回之前,它必须打开文件进行读取或将其移至永久位置。 当此方法返回时,如果临时文件仍然存在于其原始位置,则该临时文件将被删除。

如果您在后台会话中安排下载,则当您的应用程序未运行时,下载将继续。如果您在标准或临时会话中安排下载,则必须在重新启动应用程序时重新开始下载。

在从服务器传输的过程中,如果用户通知您的应用程序暂停下载,则您的应用程序可以通过调用cancelByProducingResumeData:方法取消该任务。稍后,您的应用可以将返回的重续数据传递给downloadTaskWithResumeData:downloadTaskWithResumeData:completionHandler:方法以创建一个新的下载任务,以继续下载。

如果传输失败,则调用返回NSError对象的代理方法URLSession:task:didCompleteWithError:方法。如果任务可恢复,则该对象的userInfo字典包含NSURLSessionDownloadTaskResumeData键的值;您的应用可以将返回的续传数据传递给downloadTaskWithResumeData:downloadTaskWithResumeData:completionHandler:方法以创建一个新的下载任务,以重试下载。

Listing 1-6提供了一个下载适度大文件的例子。Listing1-7提供了下载任务代理方法的示例。

// Listing 1-6  Download task example

NSURL *url = [NSURL URLWithString:@"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/ObjC_classic/FoundationObjC.pdf"];
NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithURL:url];
[downloadTask resume];
if let url = URL(string: "https://www.example.com/") {
    let dataTask = defaultSession.dataTask(with: url)
    dataTask.resume()
}
// Listing 1-7  Delegate methods for download tasks

- (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);
}
 
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"Session %@ download task %@ finished downloading to URL %@\n", session, downloadTask, location);
 
    // Perform the completion handler for the current session
    self.completionHandlers[session.configuration.identifier]();
 
   // Open the downloaded file for reading
    NSError *readError = nil;
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:location error:readError];
    // ...
 
   // Move the file to a new URL
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *cacheDirectory = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject];
    NSError *moveError = nil;
    if ([fileManager moveItemAtURL:location toURL:cacheDirectory error:moveError]) {
        // ...
    }
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    print("Session \(session) download task \(downloadTask) wrote an additional \(bytesWritten) bytes (total \(totalBytesWritten) bytes) out of an expected \(totalBytesExpectedToWrite) bytes.\n")
}
 
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
    print("Session \(session) download task \(downloadTask) resumed at offset \(fileOffset) bytes out of an expected \(expectedTotalBytes) bytes.\n")
}
 
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    print("Session \(session) download task \(downloadTask) finished downloading to URL \(location)\n")
    
    // Perform the completion handler for the current session
    self.completionHandlers[session.configuration.identifier]()
    
    // Open the downloaded file for reading
    if let fileHandle = try? FileHandle(forReadingFrom: location) {
        // ...
    }
    
    // Move the file to a new URL
    let fileManager = FileManager.default()
    if let cacheDirectory = fileManager.urlsForDirectory(.cachesDirectory, inDomains: .userDomainMask).first {
        do {
            try fileManager.moveItem(at: location, to: cacheDirectory)
        } catch {
            // ...
        }
    }
}

Uploading Body Content - 上传主体内容

您的应用程序可以通过三种方式为HTTP POST请求提供请求正文内容:作为NSData对象,文件file或流stream。一般来说,你的应用程序应该:

  • 如果您的应用程序已经拥有内存中的数据并且没有理由处置它,请使用NSData对象。
  • 如果正在上传的内容作为磁盘上的文件存在,如果您正在进行后台传输,或者如果您的应用程序将其写入磁盘以便释放与该数据关联的内存,则可以使用该文件。
  • 如果您通过网络接收数据,请使用流。

无论您选择哪种风格,如果您的应用程序提供自定义会话委托,则该委托应实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:方法以获取上载进度信息。

此外,如果您的应用程序使用流提供请求主体,则它必须提供实现URLSession:task:needNewBodyStream:方法的自定义会话委托,在 Uploading Body Content Using a Stream中有更详细的描述。

1. Uploading Body Content Using an NSData Object - 使用NSData对象上传体内容

要使用NSData对象上传主体内容,应用程序会调用uploadTaskWithRequest:fromData:uploadTaskWithRequest:fromData:completionHandler:方法来创建上载任务,并通过fromData参数提供请求主体数据。

会话对象根据数据对象的大小计算Content-Length头。

您的应用必须提供服务器可能需要的任何附加头信息(例如内容类型content type)作为URL请求对象的一部分。

// Listing 1-8  Uploading task from data example

NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
NSData *data = [NSData dataWithContentsOfURL:textFileURL];
 
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
[mutableRequest setValue:[NSString stringWithFormat:@"%lld", data.length] forHTTPHeaderField:@"Content-Length"];
[mutableRequest setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
 
NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithRequest:mutableRequest fromData:data];
[uploadTask resume];
let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let data = try? Data(contentsOf: textFileURL) {
    if let url = URL(string: "https://www.example.com/") {
        var mutableRequest = MutableURLRequest(url: url)
        mutableRequest.httpMethod = "POST"
        mutableRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
        mutableRequest.setValue("text/plain", forHTTPHeaderField: "Content-Type")
        
        let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: data)
        uploadTask.resume()
    }
}

2. Uploading Body Content Using a File - 使用文件上传体内容

要从文件上传主体内容,应用程序会调用uploadTaskWithRequest:fromFile:uploadTaskWithRequest:fromFile:completionHandler:方法来创建上载任务,并提供任务读取主体内容的文件URL。

会话对象根据数据对象的大小计算Content-Length头。 如果您的应用没有为Content-Type标头提供值,则会话也会提供一个值。

您的应用程序可以提供服务器可能需要的任何其他标头信息,作为URL请求对象的一部分。

// Listing 1-9  Uploading task from streamed request example

NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
 
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
 
NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithRequest:mutableRequest fromFile:textFileURL];
[uploadTask resume];
let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let url = URL(string: "https://www.example.com/") {
    var mutableRequest = MutableURLRequest(url: url)
    mutableRequest.httpMethod = "POST"
    mutableRequest.httpBodyStream = InputStream(fileAtPath: textFileURL.path!)
    mutableRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
    mutableRequest.setValue("text/plain", forHTTPHeaderField: "Content-Type")
    
    let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: textFileURL)
    uploadTask.resume()
}

3. Uploading Body Content Using a Stream - 使用流上传体内容

要使用流上传正文内容,您的应用程序会调用uploadTaskWithStreamedRequest:方法来创建上传任务。您的应用程序提供了一个请求对象,其中有一个关联的流,任务从中读取主体内容。

您的应用必须提供服务器可能需要的任何附加头信息 - 例如内容类型和长度 - 作为URL请求对象的一部分。

另外,因为会话不一定会回滚提供的流以重新读取数据,所以如果会话必须重试请求(例如,如果验证失败),应用程序将负责提供新流。为此,您的应用程序提供了一个URLSession:task:needNewBodyStream:方法。当调用该方法时,您的应用程序应该执行任何需要获取或创建新主体流的操作,然后使用新流调用提供的完成处理程序块。

注意:因为您的应用必须提供URLSession:task:needNewBodyStream:代理方法(如果它通过流提供主体),此技术与使用系统提供的代理不兼容。

// Listing 1-10  Uploading task from stream example

NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
 
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
mutableRequest.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:textFileURL.path];
[mutableRequest setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
[mutableRequest setValue:[NSString stringWithFormat:@"%lld", data.length] forHTTPHeaderField:@"Content-Length"];
 
NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithStreamedRequest:mutableRequest];
[uploadTask resume];
let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let url = URL(string: "https://www.example.com/") {
    var mutableRequest = MutableURLRequest(url: url)
    mutableRequest.httpMethod = "POST"
    
    let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: textFileURL)
    uploadTask.resume()
}

4. Uploading a File Using a Download Task - 使用下载任务上传文件

要为下载任务上传主体内容,您的应用必须提供NSData对象或正文流作为创建下载请求时提供的NSURLRequest对象的一部分。

如果您使用流提供数据,则应用程序必须提供URLSession:task:needNewBodyStream:代理方法,以在发生身份验证失败时提供新的正文流。 在 Uploading Body Content Using a Stream中进一步描述了此方法。

下载任务的行为就像数据任务,除了将数据返回给您的应用程序。


Handling Authentication and Custom TLS Chain Validation - 处理身份验证和自定义TLS链验证

如果远程服务器返回一个状态码,该状态码指示需要身份验证,并且该身份验证需要连接级别质询(如SSL客户端证书),则NSURLSession将调用身份验证质询代理方法。

注意:Kerberos身份验证是透明处理的。

当具有基于流的上传主体的任务的身份验证失败时,该任务不一定rewind并安全地重用该流。相反,NSURLSession对象调用委托的URLSession:task:needNewBodyStream:代理方法来获取为新请求提供主体数据的新NSInputStream对象。 (如果任务的上传主体是从文件或NSData对象提供的,则会话对象不会进行此调用。)

有关为NSURLSession编写身份验证代理方法的更多信息,请参阅Authentication Challenges and TLS Chain Validation


Handling iOS Background Activity - 处理iOS后台活动

如果您在iOS中使用NSURLSession,则在下载完成时,您的应用程序会自动重新启动。 您的应用程序的应用程序:application:handleEventsForBackgroundURLSession:completionHandler:应用程序代理方法负责重新创建适当的会话,存储完成处理程序,并在会话调用会话代理的URLSessionDidFinishEventsForBackgroundURLSession:方法时调用该处理程序。

Listing 1-11提供了一个在后台创建和启动下载任务的例子。 Listing 1-12和Listing 1-13分别显示了这些会话和应用程序代理方法的示例。

// Listing 1-11  Session background download task for iOS example

SURL *url = [NSURL URLWithString:@"https://www.example.com/"];
 
NSURLSessionDownloadTask *backgroundDownloadTask = [backgroundSession downloadTaskWithURL:url];
[backgroundDownloadTask resume];
if let url = URL(string: "https://www.example.com/") {
    let backgroundDownloadTask = backgroundSession.downloadTask(with: url)
    backgroundDownloadTask.resume()
}
// Listing 1-12  Session delegate methods for iOS background downloads

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    AppDelegate *appDelegate = (AppDelegate *)[[[UIApplication sharedApplication] delegate];
    if (appDelegate.backgroundSessionCompletionHandler) {
        CompletionHandler completionHandler = appDelegate.backgroundSessionCompletionHandler;
        appDelegate.backgroundSessionCompletionHandler = nil;
        completionHandler();
    }
 
    NSLog(@"All tasks are finished");
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    guard let appDelegate = UIApplication.sharedApplication.delegate as? AppDelegate else {
        return
    }
    
    if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
        appDelegate.backgroundSessionCompletionHandler = nil
        completionHandler()
    }
}
// Listing 1-13  App delegate methods for iOS background downloads

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (copy) CompletionHandler backgroundSessionCompletionHandler;
 
@end
 
@implementation AppDelegate
 
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
  completionHandler:(void (^)())completionHandler
{
    self.backgroundSessionCompletionHandler = completionHandler;
}
 
@end
class AppDelegate : UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var backgroundSessionCompletionHandler: CompletionHandler?
    
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {

后记

本篇主要描述了NSURLSession的使用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容