iOS URLSession 网络请求

FreeGuideOnline 最新 2026-07-11

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 格式,核心步骤:

  1. 生成唯一分隔符 boundary
  2. 将每个参数包装成 --boundary 开头的内容块
  3. 添加文件数据的 Content-DispositionContent-Type
  4. 结尾加上 --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 实例。
  • 设置合理的超时与等待策略timeoutIntervalForRequesttimeoutIntervalForResource 区分请求超时和资源整体耗时。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
  • WebSocketURLSessionWebSocketTask 提供全双工通信,使用 sendPingreceive 等方法创建实时应用。
  • 代理模式:实现 URLSessionDataDelegateURLSessionTaskDelegate 等协议,可监听到认证挑战、重定向、计时指标等详情。

9. 推荐学习路径

  1. 上手代码:用 URLSession.shared.data(from:) 完成一个简单的 GET 请求,展示在 SwiftUI 列表中。
  2. 封装通用层:构建 APIClient 类,支持 GET/POST,统一错误处理、Token 注入、日志打印。
  3. 文件处理:尝试上传图片、下载文件,并实现进度显示。
  4. 深入后台:实现一个后台下载管理器,在 App 退出后也能继续下载。
  5. 拥抱并发:将整个网络层迁移到 async/await,享受更简洁的代码。

URLSession 是 iOS 网络编程的基石,掌握它将为你打开与后端交互的大门。通过本文的实战代码和最佳实践,你可以立刻开始构建稳定、高效、安全的网络模块。如果有具体场景疑问,欢迎在下方评论区交流。