RxSwift 定制重试逻辑

虽然 RxSwift 提供了丰富的处理错误操作符,如 retry retryWhen catchError ,但做一些定制化的处理机制直接使用这几个操作符可能是不够的,本文介绍了如何定制一些更友好的处理方案,比如,在网络错误时,由用户决定是否需要重新请求。

阅读本文前建议先阅读 RxSwift - 为什么存在 catchError

本节内容灵感来自:https://github.com/ReactiveX/RxSwift/blob/4b0c77246330ec45fe03beef9ea4f5624085d8a0/RxExample/RxExample/Services/ReachabilityService.swift#L78-L92 (opens new window)

# retryWhen

直接使用 retry 通常不是我们想要的,我们可不想在手机没有网络情况下,一直尝试网络请求是不够友好的。

针对上述情况,我们可能采取下面两种方案解决:

  • 结束本次操作,展示错误信息;
  • 结束本次操作,展示错误信息,并给出弹窗由用户决定是否重试。

这里我们来用 retryWhen 完成第二种方案。

为了更好的展示 retry 的效果,本次 demo 引入了框架 [SwiftRandom][https://github.com/thellimist/SwiftRandom]。

/// 自定义的错误
///
/// - notPositive: 不是正数
/// - oversize: 数字过大
enum MyError: Swift.Error {
    case notPositive(value: Int)
    case oversize(value: Int)
}

Observable<Int>
    .deferred { () -> Observable<Int> in
        return Observable.just(Int.random(within: -100...200))
    }
    .map { value -> Int in
        if value <= 0 {
            throw MyError.notPositive(value: value)
        } else if value > 100 {
            throw MyError.oversize(value: value)
        } else {
            return value
        }
    }

上述代码中我们使用 deferred 确保每次订阅都是一个随机值。

当小于 0 时,抛出一个小于 0 的错误;当大于 100 时,抛出一个数值过大的错误。

在后面接入我们的 retryWhen 方法:

.retryWhen { [unowned self] (errorObservable: Observable<MyError>) -> Observable<()> in
    errorObservable
        .flatMap { error -> Observable<()> in
            switch error {
            case let .notPositive(value):
                return showAlert(title: "遇到了一个错误,是否重试?", message: "错误信息\(value) 小于 0", for: self)
                    .map { isEnsure in
                        if isEnsure {
                            return ()
                        } else {
                            throw error
                        }
                }
            case .oversize:
                return Observable.error(error)
            }
    }
}

这次采取的方案是:

  • 当遇到小于 0 时,弹出弹窗,由用户决定是否进行重试;
  • 当大于 100 时,不进行重试。

你可以看到我在这里指明了具体类型 (errorObservable: Observable<MyError>) ,如果遇到的是其他 Error 类型,我们将直接忽略掉。

为了获取所有的 Error ,你可以使用 Observable<Swift.Error>

# catchError

使用 catchError 也能完成类似的需求。

上层事件传递和上一小结完全一样,为了处理通用 Error ,我添加了 LocalizedError

/// 自定义的错误
///
/// - notPositive: 不是正数
/// - oversize: 数字过大
enum MyError: Swift.Error, LocalizedError {
    case notPositive(value: Int)
    case oversize(value: Int)

    var errorDescription: String? {
        switch self {
        case let .notPositive(value):
            return "\(value)不是正数"
        case let .oversize(value):
            return "\(value)过大"
        }
    }
}

Observable<Int>
    .deferred { () -> Observable<Int> in
        return Observable.just(Int.random(within: -100...200))
    }
    .map { value -> Int in
        if value <= 0 {
            throw MyError.notPositive(value: value)
        } else if value > 100 {
            throw MyError.oversize(value: value)
        } else {
            return value
        }
    }

我们在后面接上 catchErrorretry

.catchError { (error) -> Observable<Int> in
    return Observable.create { [unowned self] observer in
        let alert = UIAlertController(title: "遇到了一个错误,重试还是使用默认值 1 替换?", message: "错误信息:\(error.localizedDescription)", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "重试", style: .cancel, handler: { _ in
            observer.on(.error(error))
        }))
        alert.addAction(UIAlertAction(title: "替换", style: .default, handler: { _ in
            observer.on(.next(1))
            observer.on(.completed)
        }))
        self.present(alert, animated: true, completion: nil)
        return Disposables.create {
            alert.dismiss(animated: true, completion: nil)
        }
    }
}
.retry()

我们在 catchError 中返回了一个弹窗 Observable ,在重试的逻辑中向下发射了 Error ,替换的逻辑中向下发射了 1 。

并在后面接上了一个 retry

当选择重试的时候, retry 会接到这个错误,并重新订阅。这里需要注意的是,将 retry 直接接到弹窗O bservable 后面是错误的,此时将重新订阅弹窗 Observable ,永远触发不到最上层的随机数 Observable ,你将永远收到同一个错误。

此处错误代码示例如下:

.catchError { (error) -> Observable<Int> in
    return Observable.create { [unowned self] observer in
        let alert = UIAlertController(title: "遇到了一个错误,重试还是使用默认值 1 替换?", message: "错误信息:\(error.localizedDescription)", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "重试", style: .cancel, handler: { _ in
            observer.on(.error(error))
        }))
        alert.addAction(UIAlertAction(title: "替换", style: .default, handler: { _ in
            observer.on(.next(1))
            observer.on(.completed)
        }))
        self.present(alert, animated: true, completion: nil)
        return Disposables.create {
            alert.dismiss(animated: true, completion: nil)
        }
    }
    .retry()
}

这里我还实现了一个二进制指数退避算法的错误处理,代码如下:

extension ObservableType {

    public func retryWithBinaryExponentialBackoff(maxAttemptCount: Int, interval: TimeInterval) -> Observable<Self.E> {
        return self.asObservable()
            .retryWhen { (errorObservable: Observable<Swift.Error>) -> Observable<()> in
                errorObservable
                    .scan((currentCount: 0, error: Optional<Swift.Error>.none), accumulator: { a, error in
                        return (currentCount: a.currentCount + 1, error: error)
                    })
                    .flatMap({ (currentCount, error) -> Observable<()> in
                        return ((currentCount > maxAttemptCount) ? Observable.error(error!) : Observable.just(()))
                            .delay(pow(2, Double(currentCount)) * interval, scheduler: MainScheduler.instance)
                    })
            }
    }

}

# 总结

RxSwift 为我们提供了基本的错误处理方法,我们还可以进行一定的组合得到我们特定的错误处理方案。

在使用处理各种错误时,但需要明确的是,我们想要 retry 或许 catch 哪个 Observable 的错误。