iOS 智能重试机制
网络请求、API 调用这种异步操作,失败其实是家常便饭。怎么处理失败、要不要自动重试、怎么重试才能既解决问题又不把服务器搞崩——这几乎是每个 iOS 开发者都会遇到的现实问题。
这篇文章分享一个我自己在用的 Swift 重试实现,核心是指数退避加一点随机抖动,用起来还算顺手。
它长什么样
指数退避:别急着再来一次
每次重试的等待时间按指数往上翻:delay × 2^attempt。这样能避免一窝蜂地反复请求,给服务端留口气。
实际效果大概是:
第 1 次重试:等 1.0 秒
第 2 次重试:等 2.0 秒
第 3 次重试:等 4.0 秒
第 4 次重试:等 8.0 秒
加个 Jitter,避免惊群
多个客户端同时失败、同时重试,很容易造成“惊群效应”——大家都挤在同一时间点打过来。解决办法是给等待时间加一点随机偏移。
// jitter=0.5 时,理论退避 4 秒,实际会落在 2 到 6 秒之间
let jitterAmount = waitTime * jitter
waitTime += Double.random(in: -jitterAmount...jitterAmount)
不是所有错误都值得重试
有些错误重试也没用,比如 404、权限错误。所以留了一个 shouldRetry 闭包,让你自己决定哪些错误才需要重试:
shouldRetry: { error in
if let urlError = error as? URLError {
return [.timedOut, .notConnectedToInternet].contains(urlError.code)
}
return false
}
怎么实现的
核心代码其实不复杂,就是一个 for 循环加上异步等待:
for attempt in 0...maxRetries {
do {
return try await operation()
} catch {
lastError = error
if attempt < maxRetries && shouldRetry(error) {
let waitTime = calculateWaitTime(attempt)
try await Task.sleep(for: .seconds(waitTime))
}
}
}
几个关键点:
- 把最后一次错误存下来,最终要抛出去
- 重试之前检查两个条件:没到最大次数,并且错误类型允许重试
Task.sleep是非阻塞的,不会卡线程
什么时候用得上
网络请求重试
let data = try await withSmartRetry(
maxRetries: 5,
delay: 2.0,
jitter: 0.3
) {
try await apiClient.fetchUserData()
}
模型推理服务
let result = try await withSmartRetry(
shouldRetry: { error in
guard let httpError = error as? HTTPError else { return false }
return (500...599).contains(httpError.statusCode)
}
) {
try await modelService.predict(input: "hello")
}
配置同步
let config = try await withSmartRetry(
maxRetries: 3,
delay: 1.0
) {
try await configService.syncRemoteConfig()
}
几点小建议
- 重试次数别贪多:一般 3 次够用,关键业务可以适当加一点
- 分布式环境记得开 Jitter:不然大家一起重试,等于没重试
- 只对可恢复的错误重试:有些错误重试一百遍也没用
- 留个心眼记一下日志:重试了多少次、成功没有,后面调参数用得着
注意别踩坑
- 重试意味着总耗时变长,别忘了配合超时机制
- 只有幂等操作适合重试。比如“扣款”这种操作,重试前要想清楚
- 任务被取消时,
Task.sleep会抛出CancellationError,重试会自然终止,不用额外处理
总结
智能重试其实就是两件事:退避算法避免压垮服务,抖动策略分散请求高峰。把这套东西封装好,你的异步操作会结实很多,不至于一遇到网络波动就直接崩了。