What's new in URLSession
Networking 进阶 30m

URLSession 新特性

What's new in URLSession

2025年6月12日

在 Apple 官方观看视频

一句话判断

如果你还在每个网络调用点手写 JSONDecoder + decode() 的样板代码,这场 Session 直接帮你把这层废代码删掉了。

这场 Session 讲了什么

URLSession 今年的核心动作是把”请求 + 解码”压缩成一个调用。新的 data(from:as:) 方法接受一个 Codable 类型参数,返回解码后的对象,整个 data -> JSONDecoder -> decode -> 错误处理 的链条被吞进了框架内部。这对中大型项目影响不小——随便一个工程里几十个网络调用点,每个省 5-8 行,加起来就是几百行废代码。

第二个大动作是 Private Access Tokens v2 的原生集成。URLSession 现在会在底层自动协商隐私访问令牌,服务端拿到令牌就能确认”这是真设备上的真 app”,但完全不知道用户是谁。这意味着反机器人验证正在从”你自己的业务逻辑”变成”系统替你搞定”。对需要防刷的登录、注册、支付接口来说,这是架构层面的变化。

此外还有内置重试策略、连接级性能指标、调试捕获增强等几个实用更新。重试支持指数退避(Exponential Backoff),不用再在每个项目里手写 retry 逻辑了。URLSessionTaskMetrics 下沉到了 TLS 握手和 DNS 解析层面,网络调优终于有靠谱的数据可看。

值得深挖的点

类型化响应 API 的设计取舍

data(from:as:) 看起来只是省了几行代码,但它背后的取舍值得注意。框架内部使用默认 JSONDecoder 配置——这意味着 keyDecodingStrategy.useDefaultKeysdateDecodingStrategy.deferredToDate。如果你的后端用 snake_case,或者日期格式是 ISO8601 以外的,直接调用会崩。

这引出一个实际问题:你没法给类型化 API 传 decoder 配置。Apple 的设计意图很明确——用 CodableCodingKeys 来适配差异,而不是让框架猜你的意图。但现实是,大部分项目已经有统一的 decoder 配置层,突然多出一条”默认配置”的调用路径,反而增加了行为不一致的风险。我的判断是:新接口适合快速原型和简单数据模型,生产代码里涉及日期、嵌套、多态的场景,老老实实用你自己的 decoder。别为了”新”而新。

Private Access Tokens v2 的落地路径

PAT v2 的协议本身不新(IETF RFC 9578),但 URLSession 原生支持意味着 iOS/macOS 端的接入成本趋近于零——开发者不需要写任何客户端代码。服务端需要实现 Privacy Pass 协议验证逻辑,这倒是需要花点功夫。

这个方案和传统 CAPTCHA 的根本区别在于:CAPTCHA 是”让用户证明自己是人”,PAT 是”让设备证明自己是真设备”。用户体验上没有弹窗、没有拼图、没有点选,验证过程完全透明。但代价是只覆盖 Apple 设备,Android 和 Web 端仍需传统方案兜底。对于 iOS 为主的产品,可以把 PAT 作为首选验证手段,其他平台 fallback 到 CAPTCHA,整体成本会降不少。

代码片段

1. 类型化响应:一行替代请求+解码

let url = URL(string: "https://api.example.com/users/1")!
let (data, _) = try await URLSession.shared.data(from: url)
let user = try JSONDecoder().decode(User.self, from: data)
// 上面三行现在只需要:
let user: User = try await URLSession.shared.data(from: url, as: User.self)

坑:后端返回 snake_case 字段名时会直接解码失败,必须在 UserCodingKeys 里手动映射,或者退回到自定义 decoder。

2. 内置指数退避重试

let config = URLSessionConfiguration.default
config.retryPolicy = .exponentialBackoff(
    maxRetries: 3,
    initialDelay: .seconds(1),
    retryableStatusCodes: [429, 500, 502, 503]
)
let session = URLSession(configuration: config)

坑:POST 请求默认不重试——因为不是幂等的。如果你的 POST 是幂等的(比如带了 idempotency key),需要显式声明,否则重试策略不会生效。

3. 调试捕获网络流量

#if DEBUG
let config = URLSessionConfiguration.default
config.capturesUPI = true
let session = URLSession(configuration: config)
// Console.app 中按 session 过滤即可查看完整请求/响应
#endif

坑:务必用 #if DEBUG 包裹,否则 Release 包会捕获含 token 的请求数据,这是安全漏洞。

最佳实践

建议分三步走。

第一步,今天就把项目里的重试逻辑统一收口。大部分项目的重试代码散落在各个 APIClient 里,用法不一致,有的甚至没有。用 URLSessionConfiguration.retryPolicy 做一个项目级的工厂方法,所有 session 从这里出,重试行为立刻统一。

第二步,新写的网络调用全部用 data(from:as:),但老代码不急着动。等下次重构某个模块时顺手迁移,不要专门开一个”迁移到新 API”的 PR——收益不够大,风险倒是不小。

第三步,如果服务有防刷需求(登录、注册、支付),现在就开始调研服务端的 Privacy Pass 接入。客户端不需要改任何代码,但服务端的工作量不小,早点排期。

还有什么值得关注

  • URLSessionTaskMetrics 新增了 TLS 握手和 DNS 解析的独立耗时,排查”为什么这个请求慢”终于不用抓包了。
  • HTTP/3 连接建立优化在 Session 里被提及,但实际性能提升取决于你的用户网络环境和 CDN 配置,别指望开个开关就能提速。
  • @Generable 宏可以简化 Codable 模型定义,但和类型化响应 API 是两个独立特性,别搞混了。
URLSession 网络 HTTP Swift Concurrency 隐私