RxSwift 错误处理 - 定制重试逻辑

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

retryWhen

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

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

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

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

为了更好的展示retry的效果,本次demo引入了框架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
}
}

我们在后面接上catchError和retry:

.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直接接到弹窗Observable后面是错误的,此时将重新订阅弹窗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的错误。