MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift URLSession高级用法

2022-07-273.0k 阅读

URLSession 简介

在 Swift 开发中,URLSession 是一个强大的类,用于处理与网络的交互。它允许你发送 HTTP 和 HTTPS 请求,并接收来自服务器的响应。URLSession 构建在 URL 加载系统之上,提供了一个简单且可定制的 API 来执行各种网络任务。

URLSession 有三种不同类型的会话配置,每种配置适用于不同的场景:

  1. 默认会话配置 (URLSessionConfiguration.default):这是最常用的配置。它会持久化缓存、证书和 cookie 到磁盘,并使用共享的网络连接。适用于大多数应用程序,尤其是那些需要在不同请求之间保持状态的应用程序。

  2. Ephemeral 会话配置 (URLSessionConfiguration.ephemeral):这种配置不会持久化任何数据到磁盘。缓存、证书和 cookie 都只存在于内存中,当会话结束时就会被销毁。适用于那些需要临时网络连接,且不希望保留任何状态的应用程序。

  3. 后台会话配置 (URLSessionConfiguration.background(withIdentifier: String)):用于在后台执行网络任务。即使应用程序进入后台或被终止,网络任务仍可以继续执行。适用于下载大文件或上传数据等长时间运行的任务。

创建 URLSession

要使用 URLSession,首先需要创建一个实例。你可以通过 URLSession 的构造函数来创建,构造函数接受一个 URLSessionConfiguration 对象和一个 delegate(可选)。

以下是创建一个默认 URLSession 的示例:

let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)

如果需要创建 ephemeral 会话,可以这样做:

let ephemeralConfiguration = URLSessionConfiguration.ephemeral
let ephemeralSession = URLSession(configuration: ephemeralConfiguration)

对于后台会话,需要提供一个唯一的标识符:

let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "com.example.backgroundSession")
let backgroundSession = URLSession(configuration: backgroundConfiguration, delegate: self, delegateQueue: OperationQueue.main)

在上面的后台会话示例中,我们还指定了一个 delegatedelegateQueuedelegate 用于接收会话相关的事件通知,而 delegateQueue 则指定了这些通知在哪个队列中执行。

发送请求

URLSession 提供了多种方法来发送请求,最常用的是 dataTask(with:completionHandler:)uploadTask(with:from:completionHandler:)

使用 dataTask(with:completionHandler:)

dataTask(with:completionHandler:) 方法用于发送请求并接收数据响应。它接受一个 URLRequest 对象,并在请求完成后通过闭包返回数据、响应和错误。

以下是一个简单的 GET 请求示例:

let url = URL(string: "https://api.example.com/data")!
let request = URLRequest(url: url)

let task = session.dataTask(with: request) { (data, response, error) in
    guard let data = data, error == nil else {
        print(error?.localizedDescription ?? "No data")
        return
    }
    
    if let httpResponse = response as? HTTPURLResponse {
        print("Status code: \(httpResponse.statusCode)")
    }
    
    let json = try? JSONSerialization.jsonObject(with: data, options: [])
    print(json)
}

task.resume()

在上述代码中,我们首先创建了一个 URLURLRequest。然后,使用 dataTask(with:completionHandler:) 创建了一个任务,并在闭包中处理响应数据、状态码和可能出现的错误。最后,通过调用 task.resume() 启动任务。

使用 uploadTask(with:from:completionHandler:)

uploadTask(with:from:completionHandler:) 方法用于上传数据,例如文件或表单数据。它接受一个 URLRequest 对象和要上传的数据。

以下是一个上传图片的示例:

let url = URL(string: "https://api.example.com/upload")!
let request = URLRequest(url: url)

let image = UIImage(named: "exampleImage")
let imageData = image?.jpegData(compressionQuality: 0.8)

let task = session.uploadTask(with: request, from: imageData) { (data, response, error) in
    guard let data = data, error == nil else {
        print(error?.localizedDescription ?? "No data")
        return
    }
    
    if let httpResponse = response as? HTTPURLResponse {
        print("Status code: \(httpResponse.statusCode)")
    }
    
    let json = try? JSONSerialization.jsonObject(with: data, options: [])
    print(json)
}

task.resume()

在这个示例中,我们将一个图片转换为 Data 类型,然后使用 uploadTask(with:from:completionHandler:) 方法上传该数据。同样,在闭包中处理响应。

处理响应

在前面的示例中,我们已经看到了如何在完成处理程序中处理响应。但是,URLSession 还提供了通过委托方法来处理响应的方式,这在需要更精细控制时非常有用。

要使用委托方法,需要创建一个遵守 URLSessionDataDelegateURLSessionDownloadDelegate 协议的类,并将其设置为 URLSession 的委托。

URLSessionDataDelegate

URLSessionDataDelegate 协议提供了处理基于数据的请求(如 dataTask)响应的方法。

以下是一个简单的示例,展示如何使用委托方法处理响应:

class MyDataDelegate: NSObject, URLSessionDataDelegate {
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        // 这里可以逐步处理接收到的数据,而不是一次性处理所有数据
        print("Received data: \(data.count) bytes")
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print("Task completed with error: \(error.localizedDescription)")
        } else {
            print("Task completed successfully")
        }
    }
}

let myDelegate = MyDataDelegate()
let sessionWithDelegate = URLSession(configuration: URLSessionConfiguration.default, delegate: myDelegate, delegateQueue: OperationQueue.main)

let url = URL(string: "https://api.example.com/data")!
let request = URLRequest(url: url)

let task = sessionWithDelegate.dataTask(with: request)
task.resume()

在上述代码中,MyDataDelegate 类遵守 URLSessionDataDelegate 协议,并实现了 urlSession(_:dataTask:didReceive:)urlSession(_:task:didCompleteWithError:) 方法。urlSession(_:dataTask:didReceive:) 方法在接收到数据时被调用,urlSession(_:task:didCompleteWithError:) 方法在任务完成时被调用。

URLSessionDownloadDelegate

URLSessionDownloadDelegate 协议用于处理下载任务的响应。它提供了方法来跟踪下载进度、处理下载完成等。

以下是一个下载文件的示例,并使用委托方法处理下载过程:

class MyDownloadDelegate: NSObject, URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        print("Download progress: \(progress * 100)%")
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下载完成后,将文件移动到应用程序的文档目录
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let destinationURL = documentsURL.appendingPathComponent(downloadTask.response?.suggestedFilename ?? "downloadedFile")
        
        do {
            try FileManager.default.moveItem(at: location, to: destinationURL)
            print("File downloaded successfully to \(destinationURL)")
        } catch {
            print("Error moving file: \(error.localizedDescription)")
        }
    }
}

let downloadDelegate = MyDownloadDelegate()
let downloadSession = URLSession(configuration: URLSessionConfiguration.default, delegate: downloadDelegate, delegateQueue: OperationQueue.main)

let downloadURL = URL(string: "https://example.com/largeFile.zip")!
let downloadRequest = URLRequest(url: downloadURL)

let downloadTask = downloadSession.downloadTask(with: downloadRequest)
downloadTask.resume()

在这个示例中,MyDownloadDelegate 类遵守 URLSessionDownloadDelegate 协议,并实现了 urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)urlSession(_:downloadTask:didFinishDownloadingTo:) 方法。urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) 方法用于跟踪下载进度,urlSession(_:downloadTask:didFinishDownloadingTo:) 方法在下载完成后将文件移动到应用程序的文档目录。

处理认证

在许多情况下,服务器要求进行身份验证才能访问资源。URLSession 提供了多种方式来处理认证。

基本认证

基本认证是一种简单的认证方式,它将用户名和密码编码后发送到服务器。

以下是如何在 URLRequest 中设置基本认证的示例:

let username = "yourUsername"
let password = "yourPassword"
let authString = "\(username):\(password)"
let authData = authString.data(using: .utf8)?.base64EncodedString()

let url = URL(string: "https://api.example.com/protectedData")!
var request = URLRequest(url: url)
request.addValue("Basic \(authData!)", forHTTPHeaderField: "Authorization")

let task = session.dataTask(with: request) { (data, response, error) in
    // 处理响应
}

task.resume()

在上述代码中,我们将用户名和密码组合成一个字符串,然后进行 Base64 编码,并将其添加到 Authorization 头字段中。

基于证书的认证

基于证书的认证用于更安全的场景,尤其是在需要验证服务器身份的情况下。

以下是一个简单的示例,展示如何在 URLSession 中使用证书进行认证:

let url = URL(string: "https://api.example.com/protectedData")!
let request = URLRequest(url: url)

let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.urlCredentialStorage = URLCredentialStorage.shared

let certificateURL = Bundle.main.url(forResource: "serverCertificate", withExtension: "cer")!
let certificateData = try! Data(contentsOf: certificateURL)
let certificate = SecCertificateCreateWithData(nil, certificateData as CFData)!

let credential = URLCredential(identity: nil, certificates: [certificate], persistence: .forSession)
sessionConfiguration.urlCredentialStorage?.set(credential, for: URLProtectionSpace(host: url.host!, port: url.port ?? 443, protocol: url.scheme!, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust))

let session = URLSession(configuration: sessionConfiguration)

let task = session.dataTask(with: request) { (data, response, error) in
    // 处理响应
}

task.resume()

在这个示例中,我们从应用程序的资源中加载证书数据,创建一个 URLCredential 对象,并将其设置到 URLSessionConfigurationurlCredentialStorage 中。这样,在与服务器进行交互时,就会使用该证书进行认证。

处理缓存

URLSession 支持缓存机制,可以显著提高应用程序的性能,特别是在处理频繁请求相同数据的情况下。

配置缓存策略

可以通过 URLRequestcachePolicy 属性来配置缓存策略。以下是几种常见的缓存策略:

  1. URLRequest.CachePolicy.useProtocolCachePolicy:这是默认策略,它遵循协议(如 HTTP)的缓存规则。

  2. URLRequest.CachePolicy.reloadIgnoringLocalCacheData:忽略本地缓存数据,始终从服务器重新加载数据。

  3. URLRequest.CachePolicy.returnCacheDataElseLoad:如果本地有缓存数据,则返回缓存数据,否则从服务器加载。

  4. URLRequest.CachePolicy.returnCacheDataDontLoad:始终返回缓存数据,如果没有缓存数据,则返回错误。

以下是一个设置缓存策略的示例:

let url = URL(string: "https://api.example.com/data")!
var request = URLRequest(url: url)
request.cachePolicy = .returnCacheDataElseLoad

let task = session.dataTask(with: request) { (data, response, error) in
    // 处理响应
}

task.resume()

在上述代码中,我们将缓存策略设置为 .returnCacheDataElseLoad,这意味着如果本地有缓存数据,就返回缓存数据,否则从服务器加载。

管理缓存

除了配置缓存策略,还可以直接管理缓存数据。例如,可以清除缓存:

let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)

if let cache = session.configuration.urlCache {
    cache.removeAllCachedResponses()
    print("Cache cleared")
}

在上述代码中,我们获取 URLSessionurlCache,并调用 removeAllCachedResponses() 方法来清除所有缓存响应。

后台任务

正如前面提到的,URLSession 的后台会话配置允许在应用程序进入后台或被终止时继续执行网络任务。

后台下载任务

以下是一个后台下载任务的完整示例:

class BackgroundDownloadDelegate: NSObject, URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        print("Background download progress: \(progress * 100)%")
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let destinationURL = documentsURL.appendingPathComponent(downloadTask.response?.suggestedFilename ?? "downloadedFile")
        
        do {
            try FileManager.default.moveItem(at: location, to: destinationURL)
            print("Background file downloaded successfully to \(destinationURL)")
        } catch {
            print("Error moving background file: \(error.localizedDescription)")
        }
    }
}

let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "com.example.backgroundDownload")
let backgroundDelegate = BackgroundDownloadDelegate()
let backgroundSession = URLSession(configuration: backgroundConfiguration, delegate: backgroundDelegate, delegateQueue: OperationQueue.main)

let downloadURL = URL(string: "https://example.com/largeFile.zip")!
let downloadRequest = URLRequest(url: downloadURL)

let downloadTask = backgroundSession.downloadTask(with: downloadRequest)
downloadTask.resume()

在这个示例中,我们创建了一个后台会话,并设置了一个遵守 URLSessionDownloadDelegate 协议的委托。即使应用程序进入后台或被终止,下载任务仍会继续执行,下载完成后将文件移动到应用程序的文档目录。

后台上传任务

后台上传任务的实现方式与下载任务类似,只是使用 uploadTask 方法。

class BackgroundUploadDelegate: NSObject, URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
        print("Background upload progress: \(progress * 100)%")
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print("Background upload task completed with error: \(error.localizedDescription)")
        } else {
            print("Background upload task completed successfully")
        }
    }
}

let backgroundUploadConfiguration = URLSessionConfiguration.background(withIdentifier: "com.example.backgroundUpload")
let backgroundUploadDelegate = BackgroundUploadDelegate()
let backgroundUploadSession = URLSession(configuration: backgroundUploadConfiguration, delegate: backgroundUploadDelegate, delegateQueue: OperationQueue.main)

let uploadURL = URL(string: "https://api.example.com/upload")!
let request = URLRequest(url: uploadURL)

let image = UIImage(named: "exampleImage")
let imageData = image?.jpegData(compressionQuality: 0.8)

let uploadTask = backgroundUploadSession.uploadTask(with: request, from: imageData)
uploadTask.resume()

在上述代码中,我们创建了一个后台上传任务,并通过实现 URLSessionTaskDelegate 协议中的方法来跟踪上传进度和处理任务完成情况。

高级技巧

并发请求

在实际应用中,可能需要同时发送多个请求,并在所有请求完成后进行处理。可以使用 DispatchGroup 来实现这一点。

以下是一个并发发送多个 GET 请求的示例:

let urls = [
    URL(string: "https://api.example.com/data1")!,
    URL(string: "https://api.example.com/data2")!,
    URL(string: "https://api.example.com/data3")!
]

let dispatchGroup = DispatchGroup()
var results = [Data]()

for url in urls {
    let request = URLRequest(url: url)
    dispatchGroup.enter()
    
    session.dataTask(with: request) { (data, response, error) in
        defer { dispatchGroup.leave() }
        
        guard let data = data, error == nil else {
            print(error?.localizedDescription ?? "No data")
            return
        }
        
        results.append(data)
    }.resume()
}

dispatchGroup.notify(queue: .main) {
    // 所有请求完成后处理结果
    print("All requests completed. Results: \(results.count)")
}

在这个示例中,我们使用 DispatchGroup 来跟踪多个请求的完成情况。对于每个请求,我们调用 dispatchGroup.enter(),在请求完成后调用 dispatchGroup.leave()。最后,使用 dispatchGroup.notify(queue:) 方法在所有请求完成后执行处理逻辑。

自定义请求重试机制

有时,网络请求可能会因为各种原因失败,例如网络不稳定。可以通过自定义重试机制来提高请求的成功率。

以下是一个简单的自定义重试机制示例:

func sendRequestWithRetry(request: URLRequest, maxRetries: Int = 3, retryDelay: TimeInterval = 1.0, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
    var currentRetries = 0
    func sendRequest() {
        let task = session.dataTask(with: request) { (data, response, error) in
            if let error = error, currentRetries < maxRetries {
                currentRetries += 1
                print("Request failed. Retrying (\(currentRetries)/\(maxRetries))...")
                DispatchQueue.main.asyncAfter(deadline: .now() + retryDelay) {
                    sendRequest()
                }
            } else {
                completion(data, response, error)
            }
        }
        task.resume()
    }
    sendRequest()
}

let url = URL(string: "https://api.example.com/data")!
let request = URLRequest(url: url)

sendRequestWithRetry(request: request) { (data, response, error) in
    if let error = error {
        print("Final request failed: \(error.localizedDescription)")
    } else {
        print("Request successful")
    }
}

在上述代码中,sendRequestWithRetry 函数接受一个 URLRequest、最大重试次数和重试延迟。如果请求失败且重试次数未达到最大值,函数会在延迟一段时间后重新发送请求。

通过深入了解和掌握 URLSession 的这些高级用法,你可以在 Swift 应用程序中构建强大且高效的网络功能,无论是处理复杂的认证、管理缓存,还是执行后台任务和并发请求等。这些技巧将帮助你提升应用程序的性能和用户体验。