• Error Handling - 错误处理
    • retry - 重试
    • retryWhen
    • catchError - 恢复
    • Result

    Error Handling - 错误处理

    一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:

    • retry - 重试
    • catch - 恢复

    retry - 重试

    retry 可以让序列在发生错误后重试:

    1. // 请求 JSON 失败时,立即重试,
    2. // 重试 3 次后仍然失败,就将错误抛出
    3. let rxJson: Observable<JSON> = ...
    4. rxJson
    5. .retry(3)
    6. .subscribe(onNext: { json in
    7. print("取得 JSON 成功: \(json)")
    8. }, onError: { error in
    9. print("取得 JSON 失败: \(error)")
    10. })
    11. .disposed(by: disposeBag)

    以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。

    retryWhen

    如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:

    1. // 请求 JSON 失败时,等待 5 秒后重试,
    2. let retryDelay: Double = 5 // 重试延时 5 秒
    3. rxJson
    4. .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
    5. return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
    6. }
    7. .subscribe(...)
    8. .disposed(by: disposeBag)

    这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:

    1. .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
    2. ...
    3. }

    闭包里面的参数是 Observable<Error> 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。

    如果需要加上一个最大重试次数的限制:

    1. // 请求 JSON 失败时,等待 5 秒后重试,
    2. // 重试 4 次后仍然失败,就将错误抛出
    3. let maxRetryCount = 4 // 最多重试 4 次
    4. let retryDelay: Double = 5 // 重试延时 5 秒
    5. rxJson
    6. .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
    7. return rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
    8. guard index < maxRetryCount else {
    9. return Observable.error(error)
    10. }
    11. return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
    12. }
    13. }
    14. .subscribe(...)
    15. .disposed(by: disposeBag)

    我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 5 秒后重试:

    1. ...
    2. rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
    3. guard index < maxRetryCount else {
    4. return Observable.error(error)
    5. }
    6. return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
    7. }
    8. ...

    我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。


    catchError - 恢复

    catchError 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:

    1. searchBar.rx.text.orEmpty
    2. ...
    3. .flatMapLatest { query -> Observable<[Repository]> in
    4. ...
    5. return searchGitHub(query)
    6. .catchErrorJustReturn([])
    7. }
    8. ...
    9. .bind(to: ...)
    10. .disposed(by: disposeBag)

    我们开头的 Github 搜索就用到了catchErrorJustReturn。当错误产生时,就返回一个空数组,于是就会显示一个空列表页。

    你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:

    1. // 先从网络获取数据,如果获取失败了,就从本地缓存获取数据
    2. let rxData: Observable<Data> = ... // 网络请求的数据
    3. let cahcedData: Observable<Data> = ... // 之前本地缓存的数据
    4. rxData
    5. .catchError { _ in cahcedData }
    6. .subscribe(onNext: { date in
    7. print("获取数据成功: \(date.count)")
    8. })
    9. .disposed(by: disposeBag)

    Result

    如果我们只是想给用户错误提示,那要如何操作呢?

    以下提供一个最为直接的方案,不过这个方案存在一些问题:

    1. // 当用户点击更新按钮时,
    2. // 就立即取出修改后的用户信息。
    3. // 然后发起网络请求,进行更新操作,
    4. // 一旦操作失败就提示用户失败原因
    5. updateUserInfoButton.rx.tap
    6. .withLatestFrom(rxUserInfo)
    7. .flatMapLatest { userInfo -> Observable<Void> in
    8. return update(userInfo)
    9. }
    10. .observeOn(MainScheduler.instance)
    11. .subscribe(onNext: {
    12. print("用户信息更新成功")
    13. }, onError: { error in
    14. print("用户信息更新失败: \(error.localizedDescription)")
    15. })
    16. .disposed(by: disposeBag)

    这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。

    为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如使用枚举 Result

    1. // 自定义一个枚举类型 Result
    2. public enum Result<T> {
    3. case success(T)
    4. case failure(Swift.Error)
    5. }

    然后之前的代码需要修改成:

    1. updateUserInfoButton.rx.tap
    2. .withLatestFrom(rxUserInfo)
    3. .flatMapLatest { userInfo -> Observable<Result<Void>> in
    4. return update(userInfo)
    5. .map(Result.success) // 转换成 Result
    6. .catchError { error in Observable.just(Result.failure(error)) }
    7. }
    8. .observeOn(MainScheduler.instance)
    9. .subscribe(onNext: { result in
    10. switch result { // 处理 Result
    11. case .success:
    12. print("用户信息更新成功")
    13. case .failure(let error):
    14. print("用户信息更新失败: \(error.localizedDescription)")
    15. }
    16. })
    17. .disposed(by: disposeBag)

    这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。就算网络请求失败,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。

    另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift!