iOS 开发手册IOS DEVELOPMENT

CHAPTER 07客户端通信 · NETWORKING

网络层

App 大多是个"漂亮的网络客户端"。本章讲透 URLSession 与 async/await、Codable 编解码、缓存与重试、Mock 与端到端类型安全接口设计。

开始阅读 难度中级 先修并发 + Codable 约2.5小时
07.1

网络层职责

一个健康的网络层应把"请求构造、编解码、错误处理、缓存、鉴权"集中管理,而不是把 URL 字符串撒在 VC 里。目标是:UI 只说"我要用户数据",拿到的就是类型安全的模型。

分层建议

把网络层拆成:传输层(URLSession 封装)→ 接口层(每个 API 一个方法)→ 业务层(ViewModel 调用)。接口层用协议描述,便于 Mock 替换真实网络。

07.2

URLSession 基础

现代写法直接用 async/await 的 data(from:) / data(for:),并在不在主线程的上下文中执行,避免阻塞 UI。

func getUsers() async throws -> [User] {
    var req = URLRequest(url: URL(string: "https://api.example.com/users")!)
    req.httpMethod = "GET"
    req.setValue("application/json", forHTTPHeaderField: "Accept")

    let (data, response) = try await URLSession.shared.data(for: req)
    guard let http = response as? HTTPURLResponse,
          (200...299).contains(http.statusCode) else {
        throw APIError.badStatus
    }
    return try JSONDecoder().decode([User].self, from: data)
}
配置专属 session

生产不滥用 URLSession.shared。用 URLSessionConfiguration 建带超时、缓存策略、自定义代理的专属 session,并按需控制并发数。

07.3

Codable 编解码

Codable = Decodable & Encodable,让 JSON 与 Swift 模型双向映射。字段名不一致时用 CodingKeys 或 KeyDecodingStrategy 对齐。

struct User: Codable, Identifiable {
    let id: Int
    let name: String
    let avatarURL: URL

    // JSON 是 snake_case:user_name / avatar_url
    enum CodingKeys: String, CodingKey {
        case id
        case name = "user_name"
        case avatarURL = "avatar_url"
    }
}

// 全局策略:自动把 snake_case 转 camelCase
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
服务端字段漂移

服务端多加字段不会崩(只要模型是 struct 且非全部 required 可选);但删字段或改类型会抛 DecodingError。生产应 catch 解码错误并上报,而不是整页白屏。

07.4

请求封装与中间件

把公共逻辑(鉴权头、BaseURL、错误归一化)收进一个 APIClient,业务方法只描述"路径+参数+返回类型"。

protocol APIClient {
    func request(_ endpoint: Endpoint) async throws -> T
}

struct Endpoint {
    let path: String
    let method: String
    let query: [URLQueryItem]
}

struct LiveAPIClient: APIClient {
    let baseURL = URL(string: "https://api.example.com")!
    func request(_ endpoint: Endpoint) async throws -> T {
        var comp = URLComponents(url: baseURL.appendingPathComponent(endpoint.path),
                                 resolvingAgainstBaseURL: false)!
        comp.queryItems = endpoint.query
        var req = URLRequest(url: comp.url!)
        req.httpMethod = endpoint.method
        req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        let (data, _) = try await URLSession.shared.data(for: req)
        return try JSONDecoder().decode(T.self, from: data)
    }
}
07.5

缓存与重试

两个工程重点:用 URLCache 减少重复请求,用指数退避(exponential backoff)应对瞬时故障。

// 1. 配置磁盘缓存
let config = URLSessionConfiguration.default
config.urlCache = URLCache(memoryCapacity: 10_000_000,
                           diskCapacity: 50_000_000,
                           directory: nil)

// 2. 指数退避重试
func retry(_ op: () async throws -> T, attempts: Int = 3) async throws -> T {
    for i in 0..
缓存策略选择

.returnCacheDataElseLoad 适合弱实时数据(如配置);强实时(如余额)应禁用缓存或加校验。POST 请求默认不缓存,需手动控制。

07.6

Mock 与测试

网络层必须可 Mock:让 ViewModel 依赖 APIClient 协议,测试时注入返回固定数据的 Stub,UI/业务测试完全不触网。

struct MockAPIClient: APIClient {
    func request(_ endpoint: Endpoint) async throws -> T {
        // 直接返回桩数据,不联网
        let json = #"{"id":1,"user_name":"林可","avatar_url":"https://x/y.png"}"#
        return try JSONDecoder().decode(T.self, from: Data(json.utf8))
    }
}

let vm = UserListViewModel(client: MockAPIClient())
// 单元测试:断言 vm.users 被正确填充,无需真实网络
07.7

安全(ATS 与证书)

iOS 默认开启 App Transport Security(ATS),强制 HTTPS。内网自签证书需谨慎配置例外,或用证书绑定(pinning)防中间人。

不要全局关 ATS

NSAllowsArbitraryLoads: true 会关闭所有 HTTPS 强制,属于安全反模式,上架审核也会被拒。确需例外时按域名精确配置。

07.8

常见陷阱与 FAQ

为什么请求偶发失败却查不到原因?

常被吞掉的错误有:解码失败、非 2xx 没拦截、后台任务被系统回收。统一在网络层 catch 所有错误并打日志/上报,UI 层只拿到归一化的业务错误。

Alamofire 还有必要用吗?

URLSession 现代 API 已足够强大,简单项目无需第三方。Alamofire 在复杂重试链、响应序列化、多表单、鉴权拦截等场景仍有价值,按团队熟悉度取舍。

大文件下载怎么做不卡?

用 URLSessionDownloadTask 或 bytes(for:) 流式下载到磁盘,配合后台 session 让 App 退到后台也能继续,完成后用文件 URL 而非内存 Data 处理。

07.9

实战练习与资源

延伸资源