使用NSURLSession <- URL会话编程指南

NSURLSession及其相关的类提供了通过HTTP下载内容的API。这个类提供了丰富的委托方法来支持验证、以及在后台执行下载的能力(当app没有运行,或者iOS app被挂起的时候)。

使用NSURLSession API,app创建一些列的会话,它们每一个都协调一组关联的数据传输任务。例如,如果你正在编写web浏览器,app可以为每个标签或窗口创建一个会话。在每个会话中,app添加一系列的任务,每个任务负责一个指定URL请求(如果原始URL返回HTTP重定向,那就是任何后续的URL)。

和大多数网络API一样,NSURLSession API是高度依赖异步操作的。如果你使用默认配置,系统提供的委托,你必须提供完成处理器代码块,以便在传输完成的时候或出现错误的时候,将数据返回给app。或者,如果你提供自定义的委托对象,则任务对象调用这些委托方法时使用从服务器接收到的数据(或传输完成时,从文件下载)。

注意:完成回调主要用于使用自定义委托的替代方法。如果你使用完成回调的方法创建一个任务,那么用于响应和数据传递的委托方法将不会被调用。

NSURLSession API提供状态和进度属性,并传递这个信息给委托。它支持取消、重启(恢复)、一起挂起任务,并且它提供在挂起、取消、或者下载错误处重新恢复任务的功能。

理解URL会话概念

会话中任务的行为基于三个因素:会话(由创建它的配置对象的类型决定)的类型、任务的类型、以及当任务创建的时候app是否在前台。

会话类型

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

  • 默认会话行为与其他下载URL的Foundation方法很类似。它们使用永久的基于磁盘的缓存,并在用户的钥匙串中存储证书。
  • 短暂会话不会存储任何数据到磁盘:所有缓存、证书存储等等都保存在RAM中,并与会话捆绑。因此,当app使会话无效时,它们将会被自动清除。
  • 除了要使用独立进程处理所有数据传输以外,后台会话和默认会话很类似。后台会话有一些额外的限制,详情见Background Transfer Considerations。
任务类型

在会话中,NSURLSession类支持三种任务类型:数据任务(data tasks)、下载任务(download tasks)、以及上传任务(upload tasks)。

  • 数据任务使用NSData对象来发送和接收数据。数据任务被用于从app到服务器的短的、需要经常交互的请求。数据任务可以在每接到一部分数据的时候就把它返回给app,也可以通过完成处理器一次性返回所有的数据。
  • 下载任务以文件的形式获取数据,并且在app没有运行的时候支持后台下载。
  • 上传任务以文件的形式发送数据,并且在app没有运行的时候支持后台上传。
后台传输注意事项

NSURLSession类支持在app被挂起的时候在后台传输。后台传输只有在会话任务是使用后台会话配置对象(通过调用backgroundSessionConfiguration:返回的对象)创建的时候才会被支持。

由于实际传输是通过独立的进程执行的,并且重器app的进程非常昂贵,所以使用后台会话,一些功能将不可用,这导致了以下限制:

  • 会话必须为事件传递提供委托。(对于上传和下载,委托行为和进程内传输是一样的。)
  • 只支持HTTP和HTTPS协议(没有自定义协议)。
  • 始终遵循重定向。
  • 只支持文件的上传任务(在程序退出之后,上传数据对象、或者数据流都将失败)。
  • 如果后台传输是在app在后台的时候被启动的,那么配置对象的discretionary属性将被是作为true。

注意: 在iOS 8和OS X10.10之前,不支持在后台执行数据任务。

当app重启时,它的行为的方式在iOS和OS X之间有略微的不同。

在iOS中,当后台传输完成或需要证书时,如果app没有在运行,iOS会自动在后台重启app,并调用app的UIApplicationDelegate对象中的application:handleEventsForBackgroundURLSession:completionHandler:方法。这个调用提供导致app重启的会话的识别码。App应该存储完成处理器,使用相同的标识符创建后台配置对象,并使用这个配置对象创建会话。这个新的会话会自动与正在运行的后台活动相关联。之后,当会话完成最后一个后台下载任务的时候,它会给会话委托发送一个URLSessionDidFinishEventsForBackgroundURLSession:消息。在委托方法中,调用之前在主线程中存储的完成处理器,以便让操作系统知道可以再次安全的挂起app。

无论在iOS还时OS X中,当应用重启app的时候,app应该立即使用相同的标识符(这个标识符是app上次运行时未完成任务的任何会话的)创建后台配置对象,然后为每个配置对象创建一个会话。这些新的会话也会自动与正在进行的后台活动重新关联。

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

如果app在挂起期间完成任务,委托的URLSession:downloadTask:didFinishDownloadingToURL:方法将被调用,这个方法会使用与最新下载的文件相关联的任务(task)和URL。

类似的,如果任务请求验证,NSURLSession对象调用委托相应的URLSession:task:didReceiveChallenge:completionHandler: 或 URLSession:didReceiveChallenge:completionHandler:方法。

如果网络发生错误,在后台会话中的上传和下载任务会通过URL加载系统自动重试。没有必要使用可达性API来确定合适重试失败的任务。

对于如何使用NSURLSession在后台进行传输数据的例子,参见Simple Background Transfer。

生命周期和委托交互

根据你正在使用NSURLSession类所做的事,可能对你充分理解会话生命周期会有帮助,包括会话如何与它的委托交互、委托调用的顺序、当收到服务器返回重定向的时候发生了什么、当app恢复一个失败下载的时候发生了什么等等。

关于URL会话生命周期的完整描述,请阅读Life Cycle of a URL Session。

NSCopying行为

会话和任务对象遵守NSCoping协议,如下所述:

  • 当app拷贝会话或任务对象时,你会得到相同的对象。
  • 当app拷贝配置对象时,你会得到一个新的副本,你可以对它进行独立的修改。

委托类接口的示例

下面任务部分中的代码片段,都基于代码清单1-1中的类接口。

代码清单1-1 委托类接口的示例

@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] = [:]

}

创建和配置会话

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

  • 以特定于单一会话的方式为缓存、cookie、证书、以及协议提供存储支持
  • 与特定请求(任务)或一组请求(会话)绑定的验证
  • 通过URL上传和下载文件,推荐将数据(文件的内容)从元数据(URL和设置)中分离出来
  • 配置每个主机的最大连接数
  • 如果整个资源在相当长的时间里没有被下载,则会触发该资源的超时
  • 支持TLS的最小和最大版本
  • 自定义代理字典
  • 控制cookie策略
  • 控制HTTP渠道(pipelining)行为

因为大多数设置被包含在独立的配置对象中,所以你通常可以重用这些设置。当你实例化一个会话对象的时候,请指定如下内容:

  • 一个用于管理会话行为及其任务的配置对象
  • 可选的,委托对象用来处理接收到的传入数据,并处理特定于会话及其任务的其他事件(例如服务器验证、确定是否将资源下载请求转换到下载)。如果你不能够提供委托,NSURLSession对象会使用系统提供的委托。这样,你可以轻松得使用NSURLSession替换使用NSURLSession上的sendAsynchronousRequest:queue:completionHandler:方便方法的现有代码。

注意:如果app需要执行后台传输,它必须提供自定义委托。

当你实例化会话对象之后,除非创建一个新的会话,否则你不能改变该配置或者委托。

代码清单1-2 显示了如何创建一般的、短暂的、和后台会话的例子。

代码清单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连接时检索某个内容,你就可以为该内容创建第二个会话。如代码清单1-3所示。

代码清单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)

使用系统提供的委托获取资源

使用NSURLSession最直接的方式就是实用系统提供的委托请求资源。实用这个方法,你只需要在app中提供两段代码:

  • 创建配置对象以及使用该对象创建会话的代码
  • 完成处理器,在数据接收完成后执行某些操作

实用系统提供的委托,每个请求只需要一行代码你就可以获取特定的URL。代码清单1-4显示了这种简化模式的示例。

注意:系统提供的委托仅提供有限的网络行为的定制。如果app有超过基本URL获取的需求,例如自定义验证、或后台下载,这个技术就不太适用。有关必须实现的各种委托情况的列表,参见Life Cycle of a URL Session。

清单 1-4 适用系统提供的委托请求资源

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

}

使用自定义委托获取数据

如果你使用自定义委托来检索数据,该委托必须实现下面方法中的至少一种:

  • URLSession:dataTask:didReceiveData:将请求中的数据提供给任务,一次一段。
  • URLSession:task:didCompleteWithError:向你的任务表明数据已经接收完成。

如果app需要使用URLSession:dataTask:didReceiveData:方法返回的数据,你的代码负责通过某种方式存储该数据。

例如,web浏览器或许需要显示到达的数据,以及之前接收到的数据。为此,它或许使用字典,把数据映射到NSMutableData对象来储存结果,然后使用这些对象的appendData:方法来添加新接收到的数据。

代码清单1-5 显示了你如何创建和开始一个数据任务。

代码清单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()

}

下载文件

在高级别中,下载文件和接收数据很类似。App应该实现下列委托方法:

  • URLSession:downloadTask:didFinishDownloadingToURL:给下载内容的临时文件提供用于存储的app的URL。

重要:在该方法返回之前,必须打开文件以进行读取,或把它移动到永久位置。当该方法返回时,删除临时文件。

  • URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:提供给应用关于下载进程的状态信息。
  • URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:告诉app它试图恢复之前失败的下载已成功。
  • URLSession:task:didCompleteWithError:告诉app下载失败。

如果你在后台会话中安排下载,那么当app不运行时该下载仍会继续进行。如果你把下载安排在标准或短暂会话中,该下载必须是在app重启的时候才会重新开始。

在服务器传输期间,如果用户暂停了下载,app可以通过调用cancelByProducingResumeData:方法来取消任务。之后,app可以把返回的数据传递给downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法,来创建新的下载任务来继续下载。

如果传输失败,你委托的URLSession:task:didCompleteWithError:方法被调用。如果任务是可恢复的,那么对象的userInfo字典包含一个NSURLSessionDownloadTaskResumeData键的值;应用可以把返回的数据传递给downloadTaskWithResumeData: 或 downloadTaskWithResumeData:completionHandler:方法来创建一个新的下载任务,来重试该下载。

代码清单1-6提供一个下载一般大文件的示例。代码清单1-7提供下载任务委托方法的示例。

代码清单1-6 下载任务示例

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

}

代码清单 1-7 下载任务的委托方法

- (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 {

            // ...

        }

    }

}

上传Body内容

App可以使用三种方式为HTTP POST请求提供请求体(body)内容:NSData对象、文件、或者流。通常,app应该:

  • 如果app在内存中已经有数据且没有理由清除它时,使用NSData对象。
  • 如果你要上传的内容是磁盘中的文件,如果你打算在后台进行传输,或者写入磁盘之后可以清楚内存中相关的数据,请使用文件。
  • 如果你正通过网络接收数据,请使用数据流。

无论你选择哪个类型,如果app提供了自定义会话委托,该委托应该实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:委托方法类获取上传进度的信息。

另外,如果app使用数据流来提供请求体,它必须提供实现URLSession:task:needNewBodyStream:方法的自定义会话委托。更多详情在Uploading Body Content Using a Stream中描述。

使用NSData对象上传Body内容

想要使用NSData对象上传body内容,app需要调用uploadTaskWithRequest:fromData: 或 uploadTaskWithRequest:fromData:completionHandler:方法来创建上传任务,并通过fromData参数提供请求体数据。

会话对象会基于数据对象的尺寸来计算Content-Length(内容长度)头。

App必须提供给服务器要求的额外头部信息(例如,内容类型),作为URL请求对象的一部分。

代码清单 1-8 从数据示例上传任务

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

    }

}
使用文件上传Body内容

想要从文件上传body你容,app应该调用uploadTaskWithRequest:fromFile: 或 uploadTaskWithRequest:fromFile:completionHandler:方法来创建上传任务,并文件的URL。

会话对象给予数据对象的尺寸计算Content-Length(内容长度)头。如果app没有提供Content-Length头,会话也会提供一个。

App可以提供服务器要求的任何额外头部信息,作为请求对象的URL。

清单1-9 从数据流请求上传任务的示例

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

}
使用数据流上传Body内容

想要使用数据流上传body内容,app应该调用uploadTaskWithStreamedRequest:方法类创建上传任务。App提供与body内容数据流相关的请求对象。

App必须提供任何服务器要求的额外信息(例如,内容类型及长度),作为URL请求对象的一部分。

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

注意:因为app必须提供URLSession:task:needNewBodyStream:委托方法,如果它通过数据流提供body,则该技术与系统提供的委托不兼容。

代码清单1-10 从数据流上传任务示例

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

}
使用下载任务上传文件

想要将下载内容作为上传的body内容,app必须提供NSData对象或body数据流,作为创建下载请求是提供的NSURLRequest对象的一部分。

如果你使用数据流提供数据,app必须提供URLSession:task:needNewBodyStream:委托方法来来在验证失败的情况下提供一个新body数据流。这个方法的功能描述在Uploading Body Content Using a Stream中。

下载任务行为和数据任务很相近,除了给app返回数据的方式之外。

处理身份验证和自定义TLS链验证

如果远程服务器返回一个状态码,表明需要进行身份验证,并且如果该身份验证要求连接级别的挑战(例如SSL客户端证书),那么NSURLSession会调用验证挑战委托方法。

  • 对于会话级别挑战——NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, 或 NSURLAuthenticationMethodServerTrust——NSURLSession对象调用会话委托的URLSession:didReceiveChallenge:completionHandler:方法。如果app没有提供会话委托方法,则NSURLSession对象调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。
  • 对于非会话级别的挑战(其他所有),NSURLSession对象调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。如果app提供会话委托,并且你需要处理身份验证,你必须处理在任务级别上的验证,或者提供一个明确调用每个会话任务级别的处理程序。该会话委托的URLSession:didReceiveChallenge:completionHandler:方法在非会话级别挑战中不会被调用。

注意:Kerberos验证是显式处理的。

当基于流的上传body任务验证失败的时候,该任务不必返回以及安全的重用数据流。相反,NSURLSession对象调用委托的URLSession:task:needNewBodyStream: 方法来获取新的NSInputStream对象,它为新请求提供body数据。(如果任务的上传body是由文件或NSData对象提供给的,则会话对象不用进行该回调。)

更多关于为NSURLSession编写身份验证委托方法的信息,请阅读Authentication Challenges and TLS Chain Validation。

处理iOS后台活动

如果你在iOS中使用NSURLSession,app会在下载完成的时候自动重启。App的application:handleEventsForBackgroundURLSession:completionHandler:应用程序委托方法负责重建适当的会话、存储一个完成处理程序、以及当该会话调用会话的委托的URLSessionDidFinishEventsForBackgroundURLSession:方法时调用该完成处理程序。

代码清单1-11 提供一个在后台创建并开始下载任务的示例。代码清单1-12和1-13分别显示了这些会话和app委托方法的示例。

代码清单1-11 后台下载任务会话的iOS例子

NSURL *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()

}

代码清单1-12 iOS后台下载的会话委托方法

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

    }

}

代码清单1-13 iOS后台下载的app委托方法

@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) {

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

推荐阅读更多精彩内容