题目
使用Combine实现带错误处理和重试机制的网络请求
信息
- 类型:问答
- 难度:⭐⭐
考点
Publisher创建,错误处理,重试逻辑,线程调度,Combine生命周期管理
快速回答
实现要点:
- 使用
URLSession.dataTaskPublisher创建网络请求Publisher - 通过
tryMap处理响应和状态码验证 - 使用
retry操作符添加指数退避重试机制 - 用
catch转换不可恢复错误为友好错误类型 - 正确使用
receive(on:)切换主线程更新UI - 通过
AnyCancellable管理订阅生命周期
问题场景
在真实项目中,网络请求需要处理:HTTP错误码、网络波动重试、线程切换、错误转换等场景。Combine框架能通过操作符链式调用优雅解决这些问题。
完整实现方案
import Combine
struct User: Decodable {
let id: Int
let name: String
}
enum APIError: Error {
case invalidResponse
case serverError(Int)
case decodingError
}
class NetworkService {
private var cancellables = Set<AnyCancellable>()
func fetchUser(userId: Int) {
let url = URL(string: "https://api.example.com/users/\(userId)")!
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
throw APIError.serverError(statusCode)
}
return data
}
.decode(type: User.self, decoder: JSONDecoder())
.retry(3) // 简单重试
.catch { error -> AnyPublisher<User, APIError> in
if case APIError.serverError(500) = error {
return Just(User(id: 0, name: "Fallback User"))
.setFailureType(to: APIError.self)
.eraseToAnyPublisher()
}
return Fail(error: error as? APIError ?? .decodingError)
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("最终错误: ", error)
}
},
receiveValue: { user in
print("获取用户: ", user.name)
// 更新UI
}
)
.store(in: &cancellables)
}
}核心考察点解析
1. Publisher创建与数据处理
- dataTaskPublisher:将URLSession请求转换为Publisher
- tryMap:验证HTTP状态码,非200-299抛出错误
- decode:自动将Data解析为Decodable模型
2. 错误处理进阶
- retry操作符:立即重试3次(实际应使用
retry(with:delay:)自定义退避) - catch策略:根据错误类型提供回退值或转换错误
- 错误类型统一:将系统错误转换为领域错误(APIError)
3. 线程调度最佳实践
- receive(on:):在数据流末端切换到主线程,避免阻塞后台线程
- 注意:不应在Publisher创建时切换线程,避免阻塞URLSession的并行队列
4. 生命周期管理
- AnyCancellable:存储订阅,防止ARC提前释放
- store(in:):集中管理多个订阅,便于统一取消
常见错误
- 重试风暴:未设置延迟导致瞬时大量重试请求
- 线程阻塞:在主线程执行耗时解码操作
- 错误吞噬:catch后未返回正确错误类型导致上层无法感知
- 内存泄漏:未保存cancellable导致订阅立即释放
优化建议
// 指数退避重试实现
.retry(3, withDelay: .exponential(initial: 1, multiplier: 2))
// 自定义重试扩展
extension Publisher {
func retry(_ max: Int, withDelay delay: DispatchTimeInterval) -> AnyPublisher<Output, Failure> {
self.catch { _ in
Just(())
.delay(for: .seconds(delay), scheduler: DispatchQueue.global())
.flatMap { _ in self }
}
.retry(max)
.eraseToAnyPublisher()
}
}扩展知识
- Backpressure处理:使用
buffer或自定义Subscriber应对数据积压 - 共享Publisher:
share()避免重复创建网络请求 - 测试技巧:使用
testScheduler控制时间相关的重试逻辑 - Combine+SwiftUI:搭配
@Published属性包装器实现数据绑定