iOS URLSession 网络请求
iOS URLSession 网络请求完全指南
为什么你需要掌握 URLSession
在开发 iOS 应用时,几乎离不开与服务器的数据交互。URLSession 是苹果提供的原生网络框架,它替代了古老的 NSURLConnection,不仅支持后台下载上传、缓存管理、身份验证等高级特性,还能无缝配合现代 Swift 的 async/await 语法,是你构建任何网络层的基础。
无论你是刚入门的小白,还是想系统梳理知识的中级开发者,这篇教程都将带你从零开始,构建一个可复用的网络请求工具。
1. 基础概念:URLSession 的组成
一个基本的 URLSession 网络请求由三个核心部分组成:
- URLSession:负责协调一组相关的网络数据传输任务。
- URLSessionConfiguration:决定 Session 的行为,如缓存策略、超时时间、是否允许蜂窝数据。
- URLSessionTask:一个具体的任务(请求),包括
data task(数据任务)、upload task(上传)、download task(下载)等。
1.1 三种主要的 Session 配置
// 默认配置:持久化缓存、凭证存储于钥匙串
let defaultConfig = URLSessionConfiguration.default
// 临机会话:不写入缓存、cookie 或凭证,适合隐私要求高的请求
let ephemeralConfig = URLSessionConfiguration.ephemeral
// 后台配置:App 被挂起时仍能继续传输,必须提供标识符
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.yourapp.background")
对于大多数常规请求,使用 URLSession.shared 单例即可,它是基于默认配置的全局会话,简单快捷,适合无复杂需求的小型项目。
1.2 创建一个 Data Task 并发起请求
最基础场景 —— 使用 dataTask 拉取 JSON 数据:
let url = URL(string: "https://api.example.com/posts")!
// 使用共享会话
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("请求失败: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("服务器返回无效状态码")
return
}
if let data = data {
print("收到数据: \(data)")
// 在此处解析 JSON
}
}.resume() // 所有任务默认挂起,必须显式调用 resume()
⚠️ 注意:闭包默认在后台队列执行,更新 UI 需要切回主线程。
2. 使用 async/await 现代化网络层
从 iOS 15 开始,URLSession 全面支持 Swift 结构化并发,代码更简洁、更安全。
2.1 使用 data(from:delegate:)
代替传统的闭包回调:
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://api.example.com/posts")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
let posts = try JSONDecoder().decode([Post].self, from: data)
return posts
}
// 调用时
Task {
do {
let posts = try await fetchPosts()
// 注意:async 函数内部已在主线程,如果函数本身标记为 @MainActor 或调用在 MainActor.run 中
DispatchQueue.main.async {
self.posts = posts
}
} catch {
print("请求出错: \(error)")
}
}
2.2 自定义 Session 配合 async/await
需要更精细的控制时,创建自己的 URLSession 实例:
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30 // 请求超时 30 秒
config.waitsForConnectivity = true // 无网络时等待而不是直接报错
config.allowsCellularAccess = false // 仅 WiFi
let session = URLSession(configuration: config)
let (data, _) = try await session.data(from: url)
3. 发送 POST 请求并上传 JSON 数据
与 GET 不同,POST 需要创建 URLRequest 并设置 HTTP 方法和请求体。
func createPost(title: String, content: String) async throws -> Post {
let url = URL(string: "https://api.example.com/posts")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["title": title, "content": content]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
// 错误处理同上...
return try JSONDecoder().decode(Post.self, from: data)
}
对于表单数据,可以设置 Content-Type: application/x-www-form-urlencoded 并构建 httpBody 字符串。
4. 上传文件与多部分表单(Multipart)
4.1 上传文件数据
使用 uploadTask 可以显示上传进度,特别适合图片、视频等大文件。
let imageData = image.jpegData(compressionQuality: 0.8)!
let url = URL(string: "https://api.example.com/upload")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let task = URLSession.shared.uploadTask(with: request, from: imageData) { data, response, error in
// 处理响应
}
task.resume()
4.2 手动构建 multipart/form-data
调用上传接口常见需要 multipart 格式,核心步骤:
- 生成唯一分隔符
boundary - 将每个参数包装成
--boundary开头的内容块 - 添加文件数据的
Content-Disposition和Content-Type - 结尾加上
--boundary--
示例片段:
let boundary = "Boundary-\(UUID().uuidString)"
var body = Data()
let key = "file"
// 添加参数
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"userId\"\r\n\r\n".data(using: .utf8)!)
body.append("12345\r\n".data(using: .utf8)!)
// 添加文件
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"\(key)\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!)
body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
body.append(imageData)
body.append("\r\n".data(using: .utf8)!)
// 结束
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = body
5. 下载文件与断点续传
downloadTask 可以直接将数据保存到磁盘,节省内存。
5.1 简单下载
let downloadURL = URL(string: "https://example.com/large-file.zip")!
let downloadTask = URLSession.shared.downloadTask(with: downloadURL) { location, response, error in
guard let tempLocation = location else { return }
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsURL.appendingPathComponent("file.zip")
try? FileManager.default.moveItem(at: tempLocation, to: destinationURL)
print("文件已保存至:\(destinationURL)")
}
downloadTask.resume()
5.2 支持断点续传
使用 resumeData 属性恢复暂停的下载:
var resumeData: Data?
// 暂停下载并保存断点数据
downloadTask.cancel { data in
resumeData = data
}
// 稍后恢复
if let data = resumeData {
let resumeTask = URLSession.shared.downloadTask(withResumeData: data)
resumeTask.resume()
}
后台配置的下载任务即使 App 退出也能自动续传,是离线体验的关键。
6. 处理响应与错误
一个健壮的网络层必须有完善的错误处理机制。
6.1 状态码检查
定义自定义错误类型:
enum NetworkError: Error {
case invalidURL
case noData
case unauthorized
case serverError(statusCode: Int)
case decodingError(Error)
case unknown
}
处理函数:
func validateResponse(_ response: URLResponse?, data: Data?) throws -> Data {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.unknown
}
switch httpResponse.statusCode {
case 200...299:
guard let data = data else { throw NetworkError.noData }
return data
case 401:
throw NetworkError.unauthorized
case 500...:
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
default:
throw NetworkError.unknown
}
}
6.2 重试与超时控制
配合 async/await 可轻松实现指数退避重试:
func requestWithRetry(url: URL, retries: Int = 3) async throws -> Data {
for _ in 0..<retries {
do {
let (data, response) = try await URLSession.shared.data(from: url)
return try validateResponse(response, data: data)
} catch {
// 可设定仅对网络错误重试
continue
}
}
throw NetworkError.unknown
}
7. 最佳实践与性能优化
- 复用 Session:避免为每一个请求创建新的 Session,尤其当使用同一个配置时,全局管理一个 Session 实例。
- 设置合理的超时与等待策略:
timeoutIntervalForRequest和timeoutIntervalForResource区分请求超时和资源整体耗时。waitsForConnectivity能让用户在无网络时不至于瞬间报错。 - 主线程上报结果:传统闭包模式记得用
DispatchQueue.main.async;使用@MainActor标记ObservableObject的更新方法或用MainActor.run。 - 缓存策略:利用
URLCache减少重复请求。URLSessionConfiguration自带的缓存行为配合服务器Cache-Control头可以透明工作。 - 监控网络状态:结合
NWPathMonitor提前判断网络可用性,提升用户体验。 - 安全性:务必使用 HTTPS,不要忽略 ATS(App Transport Security)异常;对于敏感数据可启用 SSL Pinning 增强安全。
8. 进阶主题速览
- 后台传输:使用
URLSessionConfiguration.background和实现URLSessionDownloadDelegate来处理 App 挂起后的下载任务。完成后系统会唤醒 App,你可以在application(_:handleEventsForBackgroundURLSession:completionHandler:)里保存completionHandler。 - WebSocket:
URLSessionWebSocketTask提供全双工通信,使用sendPing、receive等方法创建实时应用。 - 代理模式:实现
URLSessionDataDelegate、URLSessionTaskDelegate等协议,可监听到认证挑战、重定向、计时指标等详情。
9. 推荐学习路径
- 上手代码:用
URLSession.shared.data(from:)完成一个简单的 GET 请求,展示在 SwiftUI 列表中。 - 封装通用层:构建
APIClient类,支持 GET/POST,统一错误处理、Token 注入、日志打印。 - 文件处理:尝试上传图片、下载文件,并实现进度显示。
- 深入后台:实现一个后台下载管理器,在 App 退出后也能继续下载。
- 拥抱并发:将整个网络层迁移到 async/await,享受更简洁的代码。
URLSession 是 iOS 网络编程的基石,掌握它将为你打开与后端交互的大门。通过本文的实战代码和最佳实践,你可以立刻开始构建稳定、高效、安全的网络模块。如果有具体场景疑问,欢迎在下方评论区交流。