3、URLSession进阶:URLSessionDelegate、后台下载与上传、认证挑战处理
好,咱们继续深入。上一章我们把URLSession的基本用法捋了一遍,说白了就是「发请求、收数据」这套标准流程。但实际项目中,哪有这么简单?
你想想看,大文件下载要不要进度回调?App退到后台了下载任务怎么办?遇到HTTPS证书验证怎么处理?这些才是真正考验网络层功底的地方。我个人习惯把URLSession的进阶用法分成三个维度:代理回调、后台任务、安全认证。今天咱们一个一个啃。
3.1 URLSessionDelegate:掌控每一个网络事件
默认的dataTask(with:completionHandler:)虽然方便,但有个致命问题——你拿不到中间状态。比如下载一个100MB的视频,你只能等全部完成才知道结果。这在用户体验上简直是灾难。
这时候就需要URLSessionDelegate出场了。它就像网络层的「监听器」,每个关键节点都会回调你。
3.1.1 核心代理方法
我常用的代理方法就这几个,记住它们基本够用:
| 代理方法 | 触发时机 | 典型用途 |
|---|---|---|
urlSession(_:didReceive:completionHandler:) |
收到服务器响应时 | 检查状态码、决定是否继续 |
urlSession(_:didReceive:) |
收到认证挑战时 | 处理HTTPS证书、基本认证 |
urlSession(_:task:didCompleteWithError:) |
任务完成或失败时 | 统一处理错误、清理资源 |
urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) |
下载进度更新时 | 更新UI进度条 |
关键点:代理方法是串行调用的,不会并发。这意味着你可以在回调里安全地更新UI,不用额外加锁。我在项目中吃过这个亏——一开始以为要自己加锁,后来发现URLSession内部已经处理好了。
3.1.2 实战:带进度的下载器
来,直接上代码。这是我项目里一个简化版的下载器:
class ProgressDownloader: NSObject, URLSessionDownloadDelegate {
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
// 注意:delegate queue 传 nil 会使用串行队列
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
// 下载进度回调
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
DispatchQueue.main.async {
// 更新UI进度条
self.progressHandler?(progress)
}
}
// 下载完成回调
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
// location 是临时文件路径,需要立即移动到持久目录
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsPath.appendingPathComponent(downloadTask.response?.suggestedFilename ?? "file")
do {
try FileManager.default.moveItem(at: location, to: destinationURL)
print("下载完成:\(destinationURL)")
} catch {
print("文件移动失败:\(error)")
}
}
}
避坑指南:我曾经在didFinishDownloadingTo里直接使用临时文件路径,结果读取时发现文件已经没了。记住:这个临时文件只在回调作用域内有效,你必须立即把它移到持久目录。
3.2 后台下载与上传:App退出了也不怕
嗯,这里要重点讲。后台传输是URLSession最强大的特性之一,也是坑最多的地方。
3.2.1 后台Session配置
说白了,你只需要把URLSessionConfiguration改成.background(withIdentifier:)就行。但有几个细节要注意:
- 标识符必须唯一:每个后台Session需要一个唯一标识符,App重启后靠它恢复任务
- 代理必须保留:后台Session的代理对象必须一直存活,否则收不到回调
- 系统会接管:App被杀死后,系统会继续下载,下载完成后再唤醒App
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.example.download.background")
backgroundConfig.isDiscretionary = true // 系统决定最佳下载时机(省电模式)
backgroundConfig.sessionSendsLaunchEvents = true // 允许后台唤醒App
let backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
3.2.2 处理App重启后的恢复
这里有个经典问题:App被系统杀死后,下载任务还在继续。等用户再次打开App时,怎么恢复进度?
我的做法是在AppDelegate里处理:
// AppDelegate.swift
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
// 保存 completionHandler,等所有任务完成后调用
// 否则系统会认为后台任务未完成,导致截图变黑
BackgroundManager.shared.completionHandler = completionHandler
// 重新创建Session,系统会自动恢复未完成的任务
_ = BackgroundManager.shared.createBackgroundSession(identifier: identifier)
}
警告:如果你不调用completionHandler,系统会一直保留你的App进程,导致电池消耗异常。我曾经有个版本忘了调,用户反馈手机发烫,查了半天才发现是这个原因。
3.3 认证挑战处理:与服务器握手
最后这部分,说实话很多开发者直接忽略了。但如果你对接的是金融、医疗类App,HTTPS证书验证是必须的。
3.3.1 认证挑战的类型
URLSession的认证挑战分为两种:
- 服务器信任认证(
.serverTrust):验证服务器证书是否可信 - 客户端证书认证(
.clientCertificate):客户端需要提供证书证明身份
最常见的场景是:测试环境用了自签名证书,或者证书过期了,你需要手动处理。
3.3.2 实战:处理自签名证书
我个人不建议在生产环境跳过证书验证,但开发阶段可以这样处理:
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// 只处理服务器信任认证
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// 获取服务器的信任对象
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 这里可以添加自定义验证逻辑
// 比如:检查证书是否在允许列表里
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
}
安全建议:千万不要直接写completionHandler(.performDefaultHandling, nil)就完事了。我曾经见过一个项目,所有认证挑战都直接放行,结果被安全团队打回重做。正确的做法是:只对特定域名放行,其他一律拒绝。
3.3.3 证书固定(Certificate Pinning)
如果你追求更高的安全性,可以考虑证书固定。说白了就是:把服务器的公钥或证书硬编码到App里,只信任这个证书。
func handleServerTrust(serverTrust: SecTrust, host: String) -> Bool {
// 1. 获取服务器证书
guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
return false
}
// 2. 获取公钥
let publicKey = SecCertificateCopyKey(serverCertificate)
// 3. 与本地存储的公钥对比
let localPublicKey = loadLocalPublicKey(for: host)
return publicKey == localPublicKey
}
小技巧:证书固定虽然安全,但证书过期后你需要发版更新。我建议使用公钥固定而不是证书固定,因为公钥通常不会变,即使证书续签了也能正常工作。
3.4 本章小结
好,咱们把URLSession的进阶用法捋了一遍。总结三个核心点:
- 代理回调:用URLSessionDelegate获取中间状态,实现进度、错误处理
- 后台传输:用background配置实现App退出后的下载,记得处理completionHandler
- 认证挑战:处理HTTPS证书验证,生产环境不要跳过,考虑证书固定
下一章我们会讲Combine与URLSession的结合,那才是真正的「网络层响应式编程」。到时候你会发现,用Combine处理网络请求,代码能简洁到让你怀疑人生。