URLSession 在 2013年随着 iOS7 的发布一起面世的,苹果对它的定位是作为 NSURLConnection 的替代者,在 iOS9 之后苹果官方已经移除了 NSURLConnection,我们使用最广泛的第三方框架:AFNetworking、SDWebImage 的最新版也都已经全部使用了URLSession
URLSession 指的也不仅是同名类 URLSession,还包括一系列相互关联的类,包括:NSURLSession、NSURLSessionConfiguration 以及 NSURLSessionTask 的 4 个子类:NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask、NSURLSessionStreamTask(iOS 9 新增) ,还包括之前就存在的两个类;NSURLRequest 与 NSURLCache
好了 现在我们从URLSession的用法开始聊起
NSURLSession 本身是不会进行请求的,而是通过创建 task 的形式进行网络请求,同一个 URLSession 可以创建多个 task,并且这些 task 之间的 cache 和 cookie 是共享的。
NSURLSession 的使用有如下几步:
有两种方式创建 NSURLSession 对象:
//创建默认的Session对象,会使用全局的Cache,cookie和证书
let session = URLSession.shared
//自定义创建Session对象需要先创建配置对象
let config = URLSessionConfiguration.default
//自定义创建Session对象
let session = URLSession(configuration: config)
//另一种创建方式
//init(configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue: OperationQueue?)
let session = URLSession(configuration: config, delegate: self as! URLSessionDelegate, delegateQueue: nil)
使用代理可以处理处理身份验证失败、重定向、进度更新等功能
The queue should be a serial queue, in order to ensure the correct ordering of callbacks. If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
队列应该是一个串行队列,以确保回调的正确顺序。 如果为零,会话将创建一个串行操作队列,用于执行所有委托方法调用和完成处理程序调用
URLSessionConfiguration可以配置会话(session)属性像timeout、HTTP header、缓存策略。
URLSession 在初始化时会把配置它的 URLSessionConfiguration 对象进行一次 copy,并保存到自己的 configuration 属性中,而且这个属性是只读的,因此之后再修改最初配置 session 的那个 configuration 对象对于session 是没有影响的,也就是说,onfiguration`只在初始化时被读取一次,之后都是不会变化的,如果想更改传输策略(modify your transfer policies),那就得修改完configuration对象之后,再创建一个新的 session 对象
URLSession支持以下三种会话模式:
let configDefault = URLSessionConfiguration.default
//1、默认会话模式(default)
let configEphemeral = URLSessionConfiguration.ephemeral
//2、瞬时会话模式(ephemeral)
let configBackground = URLSessionConfiguration.background(withIdentifier: "test")
//3、后台会话模式(background)
创建了URLSessionConfiguration就可以给他设置各种属性,详细可以看官方文档
configuration.timeoutIntervalForRequest = 30
request 请求超时时间,默认60s
configuration.timeoutIntervalForResource = 300
任务等待整个资源加载的超时时间(单位是秒),但是默认值是7天,就很离谱
configuration.waitsForConnectivity = true
indicates whether the session should wait for connectivity to become available, or fail immediately.
这个属性让这个暂时不可用的连接变成等待可用而不是立即失败。比如说有个连接必须要VPN,但是现在只有蜂窝网,这个属性就可以让这个连接等到有VPN的时候在进行连接。这个属性的默认值是false
,如果连接不可用的时候会立即失败然后报错。
这个等待时间由上面的提到的 timeoutIntervalForResource 确定(所以一定要配置resource time,别用他七天的默认值)。在这期间后台会话会一直等待连接。
如果用代理的话,在等待连接的时候会调用urlSession(_:taskIsWaitingForConnectivity:)
configuration.allowsCellularAccess = false
是否允许会话使用蜂窝网络,默认是true
,在iOS 13之后提供了替代方法allows expensive network access
configuration.allowsConstrainedNetworkAccess = false
是否允许会话使用受限网络接口进行连接,默认为true
。 受限网络接口是用户在设备设置中打开“低数据模式”的接口。
configuration.allowsExpensiveNetworkAccess = false
会话是否允许通过“昂贵的”网络接口进行连接,默认为 true。这个ExpensiveNetwork是由系统决定的,比如说热点还有流量。
一般可以结合上面提到的低数据模式还有网络等待来组合使用,防止在昂贵且受限的网络上进行非必要的网络活动:
configuration.waitsForConnectivity = trueconfiguration.allowsConstrainedNetworkAccess = falseconfiguration.allowsExpensiveNetworkAccess = false
在默认的会话配置中,利用app来直接配置缓存:
let shared = URLCache.sharedprint(shared.memoryCapacity) // 512000 (512Kb)print(shared.diskCapacity) // 10000000 (10Mb)print(shared.currentMemoryUsage)print(shared.currentDiskUsage)// Increase memory cache sizeshared.memoryCapacity = 500_000_000 // 500Mb
我们也可以通过Configuration来配置缓存,使用var urlCache: URLCache?
let cache = URLCache(memoryCapacity: 500_000_000, diskCapacity: 1_000_000_000)configuration.urlCache = cache
也可以直接禁用缓存
configuration.urlCache = nil
configuration.httpAdditionalHeaders = ["User-Agent": "MyApp 1.0"]
使用httpAdditionalHeaders
可以来配置请求头
URLSessionTask是一个表示任务对象的抽象类,可以在URLSession的实例上调用调用对应的方法来创建任务,一共有四种任务类型:
URLSessionDataTask:处理从HTTP get请求中从服务器获取数据到内存中
URLSessionUploadTask:上传硬盘中的文件到服务器,一般是HTTP POST 或 PUT方式
URLSessionDownloadTask:从远程服务器下载文件到临时文件位置
URLSessionStreamTask:基于流的URL会话任务,提供了一个通过 URLSession 创建的 TCP/IP 连接接口
注意:
我们的网络任务默认是挂起状态的
suspend
,在获取到dataTask
以后需要使用resume()
函数来恢复或者开始请求。
- resume():启动任务
- suspend():暂停任务
- cancel():取消任务
URLSessionDataTask是URLSessionTask的一个实体子类,他的方法都定义在了URLSessionTask基之中。
URLSessionDataTask是平时使用频率最高的,平时使用的POST还有GET请求都是通过它来实现的。URLSessionDataTask请求后将数据作为一个或多个NSData对象直接返回到应用程序(在内存中)。
URLSessionDataTask 可以通过 URL 或 URLRequest 创建(使用前者相当于是使用一个对于该 URL 进行标准 GET 请求),如果请求的数据简单并且不需要对获取的数据进行复杂操作,我们使用 Block 处理返回的数据:
// 通过 NSURL 创建func getRequst() { let session = URLSession.shared let url = URL(string: "https://httpbin.org/get") let task = session.dataTask(with: url!) { (data, response, error) in //此处返回的数据是JSON格式的,因此使用NSJSONSerialization进行反序列化处理 if let result = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){ print(result) } } // 使用 resume 任务开始运行 task.resume()}// 通过 NSURLRequest 创建 整个POST请求func postRequst() { let session = URLSession.shared var request = URLRequest(url: URL(string: "https://httpbin.org/post")!) //设置请求方式,默认为GET request.httpMethod = "POST" let task = session.dataTask(with: request) { (data, response, error) in if let result = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){ print(result) } } task.resume()}
urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
urlSession(_:dataTask:didReceive:completionHandler:)
来检查状态码和头,并可以将data task转为download taskurlSession(_:dataTask:didReceive:)
来让应用获取到已到达的数据urlSession(_:dataTask:willCacheResponse:completionHandler:)
来让你决定是否缓存响应func sessionWithDelegate() { //delegateQueue 代理方法执行的线程 let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self as URLSessionDataDelegate, delegateQueue: OperationQueue.main) let request = URLRequest(url: URL(string: "https://httpbin.org/get")!) let task = session.dataTask(with: request) task.resume()} //MARK: - URLSessionDataDelegatefunc urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { print("接收到服务器的响应") // 必须设置对响应进行允许处理才会执行后面的操作 completionHandler(.allow)} func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { //该方法可能被调用多次 print("接受到服务器的数据")} func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { print("请求完成之后调用成功或者失败")}
URLSessionDownloadTask用于下载,有两种方式:Block 和 代理,URLSession 在下载文件的时候,是将数据一点点地写入本地的一个临时文件,这个临时文件系统会很很快删除,所以我们需要把文件从这个临时地址移动到一个永久的地址保存起来,这样才算完整的下载完一个文件,另外,使用 NSURLSessionConfiguration 的 background 模式可以做到后台下载,并且即使应用被 Kill 之后也还可以恢复之前的下载任务。
使用 Block 方式适合下载小文件,并且不需要监听下载进度,并且文件下载完成才会调用 Block。
func downloadTask() { let session = URLSession.shared let request = URLRequest(url: URL(string: "https://cdn.sspai.com/2017/06/19/80b932adce51390f8c79070e8839cc95.jpeg")!) let task = session.downloadTask(with: request) { (location, response, error) in //location 是沙盒中 tmp 文件夹下的一个临时文件路径,tmp 中的文件随时可能被删除,所以我们需要自己需要把下载的文件挪到 Caches 或者 Documents 文件夹中 let locationPath = location!.path let documnets = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last! + "/" + (response?.suggestedFilename)! do { try FileManager.default.moveItem(atPath: locationPath, toPath: documnets) } catch let error as NSError { print(error) } } task.resume()}
关于NSSearchPathForDirectoriesInDomains
iPhone会为每一个应用程序生成一个私有目录,这个目录位于:/Users/sundfsun2009/Library/Application Support/iPhone Simulator/User/Applications下,并随即生成一个数字字母串作为目录名,在每一次应用程序启动时,这个字母数字串都是不同于上一次。
所以通常使用Documents目录进行数据持久化的保存,而这个Documents目录可以通过:NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserdomainMask,true)得到。
使用代理方式适合下载大文件,并且可以随时监听文件的下载进度、暂停文件下载等。
func downloadTask() { let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self as URLSessionDownloadDelegate, delegateQueue: OperationQueue.main) let request = URLRequest(url: URL(string: "http://dldir1.qq.com/qqfile/QQforMac/QQ_V5.5.1.dmg")!) let task = session.downloadTask(with: request) task.resume()} //MARk: - URLSessionDownloadDelegatefunc urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { //下载过程中写入数据会一直调用这个方法 //didWriteData:之前已经下载完的数据量 //bytesWritten:本次写入的数据量 //totalBytesWritten:目前总共写入的数据量 //totalBytesExpectedToWrite:文件总数据量 let progress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite) print("totalBytesExpectedToWrite++++\(progress)")} func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) { //暂停后恢复下载的代理方法} func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { //下载完成后的代理方法 let locationPath = location.path let documnets = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last! + "/" + (downloadTask.response?.suggestedFilename)! do { try FileManager.default.moveItem(atPath: locationPath, toPath: documnets) } catch let error as NSError { print(error) }}
NSURLSession 中使用NSURLSessionUploadTask 上传文件,把需要上传的数据以表单的形式拼接在请求体中即可,创建NSURLSessionUploadTask 有以下两种方式:
/* 创建上传任务,需要提供上传文件二进制数据 */open func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionUploadTask/* 创建上传任务,需要提供上传文件所在的URL路径,不过这个方法常配合“PUT”请求使用 */open func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionUploadTask
表单拼接格式是固定的,必须严格按照规定的格式设置,具体格式如下:
--上传标识符\r\nContent-Disposition:form-data;name="表单控件名称";filename="上传文件名称"\r\nContent-Type: 要上传文件MIME Type \r\n\r\n要上传文件二进制数据\r\n--上传标识符\r\nContent-Disposition: form-data; name=\"参数名1\"\r\n\r\n参数值1\r\n--上传标识符\r\nContent-Disposition: form-data; name=\"参数名2\"\r\n\r\n参数值2\r\n--结束标识符--\r\n
举个图片上传拼接文件参数的例子:
//标识符可以随意,但是前后必须一致let boundary = "test"func uploadFile() { var request = URLRequest(url: URL(string: "http://localhost:8080/Server/upload")!) request.httpMethod = "POST" let contentType = "multipart/form-data; charset=utf-8;boundary=" + boundary // 设置请求头(告诉服务器这次传给你的是文件数据,告诉服务器现在发送的是一个文件上传请求) request.setValue(contentType, forHTTPHeaderField: "Content-Type") let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue.main) let imageData = self.buildBodyData() let uploadTask = session.uploadTask(with: request, from: imageData) { (data, respond, error) in if let dic = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) { print(dic) } } uploadTask.resume()} func buildBodyData() -> Data { var bodyStr = "--" + boundary + "\r\n" bodyStr.append("Content-disposition: form-data; name=\"file\"; filename=\"test.png\"") bodyStr.append("\r\n") bodyStr.append("Content-Type: image/png") bodyStr.append("\r\n\r\n") var bodyData = bodyStr.data(using: String.Encoding.utf8) let path = Bundle.main.path(forResource: "minion_03", ofType: "png") if let imageData = try? Data(contentsOf: URL(fileURLWithPath: path!)) { bodyData?.append(imageData) } let endStr = "\r\n--" + boundary + "--\r\n" bodyData?.append(endStr.data(using: String.Encoding.utf8)!) return bodyData!}
NSURLSessionStreamTask类提供了一个通过NSURLSession创建的TCP/IP连接接口。
需要注意的是,由于URLSession采用的是“异步阻塞”模型,所以在实现DownloadTaskDelegate代理方法更新UI时需要将线程切回主线程。
“阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,
同步与异步同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
典型的异步编程模型比如Node.js举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
阻塞与非阻塞阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
参考:
https://useyourloaf.com/blog/urlsessionconfiguration-quick-guide/
https://xiaovv.me/2017/07/12/Basic-usage-of-NSURLSession/