侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

使用Combine实现带错误处理和重试机制的网络请求

2025-12-12 / 0 评论 / 4 阅读

题目

使用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应对数据积压
  • 共享Publishershare()避免重复创建网络请求
  • 测试技巧:使用testScheduler控制时间相关的重试逻辑
  • Combine+SwiftUI:搭配@Published属性包装器实现数据绑定