談?wù)凴xSwift中的錯誤處理

RxSwift中提供了多種不同的錯誤處理操作符空入,它們可以在鏈?zhǔn)讲僮髦邢嗷ソM合以實現(xiàn)復(fù)雜的處理邏輯,下面先簡單介紹一下RxSwift提供的錯誤處理操作,然后通過一些具體的例子來看看如何在實際項目中應(yīng)用。這里不會詳細(xì)介紹RxSwift朝抖,閱讀前需要對Rx的基礎(chǔ)有一定了解。

錯誤處理操作符

throw

Rx中許多操作符中的閉包簽名都是帶有throws修飾符的谍珊,比如說這是map方法的簽名:

func map<R>(_ transform: @escaping (E) throws -> R) -> Observable<R>

我們可以在這樣的操作中拋出錯誤治宣,拋出的錯誤會沿著鏈?zhǔn)讲僮饕徊讲较蛳聜鬟f,比如下面的代碼:

Observable.of(3, 2, 1) // 創(chuàng)建一個包含3個事件的Observable
    .map { (n) -> Int in
        if n < 2 {
            throw CustomError.tooSmall // 1. 拋出一個自定義的錯誤
        } else {
            return n * 2
        }
    }
    .subscribe { event in
        // 2. 這里會收到一個 CustomError.tooSmall 類型的error
    }
    ...

當(dāng)數(shù)字小于2時砌滞,在mapthrow一個自定義的錯誤類型侮邀,這個錯誤會被傳遞到下面的subscribe中。

catchError

RxSwift可以在鏈?zhǔn)讲僮髦胁东@錯誤贝润,不管是Observable自己產(chǎn)生的錯誤還是用戶throw的錯誤绊茧,都可以在catchError操作中進(jìn)行處理,接著上面map的代碼:

Observable.of(3, 2, 1)
    .map { 
        ...
    }
    .catchError({ (error) -> Observable<Int> in
        if case CustomError.tooSmall = error {
            return .just(2) // 1. 在捕獲到tooSmall錯誤后返回一個2
        }
        return .error(error) // 2. 其他錯誤不處理打掘,繼續(xù)沿著操作鏈傳遞
    })
    .subscribe { event in
        // 3. 當(dāng)發(fā)生tooSmall錯誤時這里會收到2华畏,最終的結(jié)果是 3, 2, 2
    }
    ...

這樣的處理方式接近于語言本身的try…catch機制,使用起來十分方便尊蚁。

retry

retry提供了出錯重試的操作亡笑,在一個Observable后面加上retry,當(dāng)這個Observable出錯的時候訂閱方不會收到.error事件横朋,而是重新訂閱這個Observable仑乌,這通常意味著這個Observable創(chuàng)建事件相關(guān)的操作都會重新執(zhí)行一次,比如說這是一個網(wǎng)絡(luò)請求相關(guān)的Observable琴锭,“重新訂閱”會重發(fā)相關(guān)的網(wǎng)絡(luò)請求:

moyaProvider.rx.request(.customData)
    .retry() // 1. 當(dāng)發(fā)生錯誤時重試請求
    .subscribe { 
        // 2. 重試的過程對于訂閱者來說是不可見的晰甚,請求成功后這里才會接收到事件
    }

retry方法還可以帶一個Int類型的參數(shù),表示重試的最大次數(shù)祠够。

retryWhen

retryWhen就是帶條件的retry压汪,可以在特定條件下才進(jìn)行重試。retryWhen的方法簽名比較特別古瓤,它的閉包中接受的參數(shù)不是一個簡單的Error類型,而是一個Observable<Error>類型,使用方法如下:

Observable.of(3, 2, 1)
    .map { 
        ...
    }
    // 1. retryWhen不關(guān)心返回的是什么類型落君,只關(guān)心事件本身穿香,所以直接用Observable<()>即可
    .retryWhen({ (errorObservable) -> Observable<()> in
        // 2. 用flatMap將其轉(zhuǎn)換成其他類型的Observable
        return errorObservable.flatMap({ (error) -> Observable<()> in
            if case CustomError.tooSmall = error {
                return .just(()) // 3. 返回一個next事件表示重試
            }
            return .error(error) // 4. 繼續(xù)返回error表示不處理
        })
    })

閉包返回的Observable可以是任意類型,因為retryWhen只關(guān)心Observable中的事件本身绎速,不關(guān)心其中承載的數(shù)據(jù)類型皮获,所以這里直接用一個空類型即可,如果需要重試的話就將一個帶有.next事件的Observable返回纹冤。

retryWhen這樣設(shè)計的一個優(yōu)點是在出錯的時候可以將它重試的邏輯跟另外一個Observable事件流關(guān)聯(lián)起來(后面我會演示一個例子)洒宝。但是在上面這樣一個簡單的場景中,使用起來未免過于麻煩了萌京,這里可以做一個簡單的封裝雁歌,提供一個(Error) -> Bool類型的閉包來處理判斷邏輯:

extension ObservableType {
    public func retryWhen<Error: Swift.Error>(_ shouldRetry: @escaping (Error) -> Bool) -> Observable<E> {
        return self.retryWhen({ (errorObserver: Observable<Error>) -> Observable<()> in
            return errorObserver.flatMap({ (error) -> Observable<()> in
                if shouldRetry(error) {
                    return .just(())
                }
                return .error(error)
            })
        })
    }
    
    public func retryWhen(_ shouldRetry: @escaping (Swift.Error) -> Bool) -> Observable<E> {
        return self.retryWhen({ (errorObserver: Observable<Swift.Error>) -> Observable<()> in
            return errorObserver.flatMap({ (error) -> Observable<()> in
                if shouldRetry(error) {
                    return .just(())
                }
                return .error(error)
            })
        })
    }
}

將上面這段代碼復(fù)制到你的項目中,之前的重試邏輯就變成了:

...
.retryWhen({ (error) -> Bool in
    if case CustomError.tooSmall = error {
        return true
    }
    return false
})
...

這樣看起來清楚多了知残,減輕了思維負(fù)擔(dān)靠瞎。

實際應(yīng)用

Moya是Swift常用的一個網(wǎng)絡(luò)庫,它提供了Rx的接口求妹,下面的例子以Moya作為網(wǎng)絡(luò)庫來演示乏盐,Moya的一個核心協(xié)議是TargetType,不了解Moya的朋友可以看看它的文檔制恍,基本使用就不再詳細(xì)介紹了父能。下面來看兩個常見的實際應(yīng)用場景

場景一:帶交互的出錯重試

在很多時候,用戶的操作失敗時不能直接重試净神,而是要給一個法竞,讓用戶來決定下一步的操作。例如有一個文件下載的請求强挫,當(dāng)下載失敗的時候需要彈框來詢問是否重試岔霸。也就是說在出錯到重試之間存在一個“中斷”,只有當(dāng)用戶做出選擇之后操作鏈才會繼續(xù)向下執(zhí)行俯渤。

解決方法是使用retryWhen呆细,將參數(shù)中的的Observable<Error>與我們自己業(yè)務(wù)邏輯的Observable關(guān)聯(lián)起來。

首先八匠,我們假定有這樣一個確認(rèn)框的控件絮爷,它的簽名如下:

class ConfirmView: UIView {
    /// 在視圖中顯示一個確認(rèn)框,callback為點擊的回調(diào)梨树,點擊確認(rèn)回調(diào)true坑夯,點擊取消回調(diào)false
    static func show(_ title: String, _ callback: (Bool) -> Void) {
        ...
    }
}

實際的項目中通常都會有很多封裝好的控件類型,借助于RxSwift中所提供的擴展機制抡四,只需要添加一個小小的擴展就可以與Rx的世界無縫對接起來:

extension Reactive where Base: ConfirmView {
    // 1. 在擴展中定義一個show方法柜蜈,不同的是沒有callback參數(shù)仗谆,而是返回一個Observable<Bool>
    static func show(_ title: String) -> Observable<Bool> {
        // 2. 創(chuàng)建一個Observable<Bool>
        return Observable<Bool>.create({ (observer) -> Disposable in
            // 3. 調(diào)用原始的show方法,并在回調(diào)中通過observer發(fā)送結(jié)果
            ConfirmView.show(title, { (confirm) in
                observer.onNext(confirm)
                observer.onCompleted()
            })
            return Disposables.create { 
                // do some cleanup
            }
        })
    }
}

之后就可以通過ConfirmView.rx.show(xxx)的方式來調(diào)用這個方法了淑履,這個方法會彈出一個選擇框等待用戶的選擇隶垮,選擇的結(jié)果通過Observable的事件來進(jìn)行通知。之后我們使用flatMap將這個ObservableretryWhen中的Obverable<Error>關(guān)聯(lián)起來:

...
.retryWhen({ (errorO) -> Observable<()> in
    return errorO.flatMap({ (error) -> Observable<()> in
        if case CustomError.tooSmall = error {
            return ConfirmView.rx
                .show("是否重試?")
                .map {
                    if $0 { // 1. 如果選擇了重試秘噪,則返回.next()表示重試
                        return ()
                    } else {
                        throw error // 2. 否則繼續(xù)返回error將錯誤繼續(xù)向下傳遞
                    }
                }
        }
        return .error(error)
    })
})
.subscribe {
    // 3. 如果上面選擇了重試狸吞,這里不會接收到錯誤事件
}
...

類似的,將不同的操作封裝成Observable這樣簡單的邏輯流指煎,然后通過RxSwift提供的操作加以組合以實現(xiàn)更加復(fù)雜的邏輯蹋偏,這也是Rx所提倡的函數(shù)式思想。

場景二:401認(rèn)證

401錯誤是一種很常見應(yīng)用場景至壤,比如說在我們的應(yīng)用中認(rèn)證流程是這樣的:當(dāng)服務(wù)器需要重新認(rèn)證用戶登錄信息時會返回一個401狀態(tài)碼威始,這時客戶端將認(rèn)證信息添加到請求頭中并重發(fā)當(dāng)前的請求,這一過程對上層的業(yè)務(wù)方應(yīng)該是無感知的崇渗。

這跟之前的例子有一些不同的地方:當(dāng)出錯時我們不能直接retry整個請求字逗,而是要修改原始請求添加自定義的Header,最簡單粗暴的方法是在檢測到401錯誤時發(fā)送一個通知宅广,外面收到通知之后將Header添加到請求頭里:

moyaProvider.request(target)
    .map({ (response) -> Response in
        if response.statusCode == 401 { // 將401轉(zhuǎn)換成自定義的錯誤類型
            // 先發(fā)送通知葫掉,之后再retry
            NotificationCenter.default.post(name: .AddAuthHeader, object: nil)
            throw NetworkError.needAuth
        } else {
            return response
        }
    })
    .retry()

這種做法其實并不好,因為Rx中強調(diào)的是事件流跟狱,原本應(yīng)該是一個連貫的邏輯卻被通知給打斷了俭厚,當(dāng)我們閱讀到這里的時候還得停下來全局搜索通知的名字以查找響應(yīng)的位置,這樣不利于閱讀驶臊,同時也違背了Rx的哲學(xué)挪挤。

我這里所采用的做法是捕獲到錯誤時不進(jìn)行retry,而是返回一個新的網(wǎng)絡(luò)請求关翎。為了讓這個新的網(wǎng)絡(luò)請求與之前的邏輯無縫連接起來扛门,首先需要定義一個代理TargetType:

let ProxyProvider = NetworkProvider<ProxyTarget>()

enum ProxyTarget {
    // 添加Header
    case addHeader(target: TargetType, headers: [String: String]) 
    // ...
}

extension ProxyTarget: TargetType {
    var headers: [String: String]? {
        switch self {
        // 1. 將新增的Header添加到被代理的Target上
        case let .addHeader(target: target, headers: headers):
            return headers.merging(target.headers ?? [:], uniquingKeysWith: { (first, second) -> String in
                return first
            })
        }
    }
    
    // 2. 不需要吹的地方直接返回被代理Target的屬性
    var task: Task {
        switch self {
        case let .addHeader(target: target, headers: _):
            return target.task
        }
    }
    
    // ...
}

ProxyTarget并沒有定義新的網(wǎng)絡(luò)請求,而是用來代理另外一個TargetType纵寝,這里我們只定義了一個addHeader操作论寨,用來修改請求的Header。

最終的實現(xiàn)如下:

provider.request(target)
    .map({ (response) -> Response in
        if response.statusCode == 401 { // 1. 將401轉(zhuǎn)換成自定義的錯誤類型
            throw NetworkError.needAuth
        } else {
            return response
        }
    })
    .catchError({ (error) -> Single<Response> in
        if case NetworkError.needAuth(let response) = error{
            // 2. 捕獲未認(rèn)證的錯誤爽茴,添加認(rèn)證頭后再次重試
            let authHeader = ... // 計算認(rèn)證頭
            let target = ProxyTarget.addHeader(target: token, headers: authHeader)
            return ProxyProvider.rx.request(target, callbackQueue: callbackQueue)
        }
        return Single.error(error)
    })
    .subscribe {
        // 3. 認(rèn)證的過程對于上層的業(yè)務(wù)方是無感知的
    }
    ...

使用map將401轉(zhuǎn)換成自定義的錯誤類型葬凳,之后在catchError中捕獲這個錯誤,使用ProxyTarget加上認(rèn)證頭之后返回一個新的Observable室奏,這樣一來所有相關(guān)的邏輯都被集中在這一系列的鏈?zhǔn)秸{(diào)用中了火焰。

當(dāng)然在實際項目中不僅僅是401這類錯誤,可能還會有許多其他業(yè)務(wù)相關(guān)的錯誤類型胧沫,將它們?nèi)挤旁?code>map中處理顯然不是一個好主意昌简,最好的辦法是將這部分邏輯抽離出來放在Moya的Plugin中占业,這里就不再演示了。

最后

Rx中對于事件流的抽象十分強大江场,可以用來描述各種復(fù)雜的場景纺酸,這里僅僅從錯誤處理的方面列舉了一些簡單的例子窖逗,可以看到Rx的思想跟我們平常所寫的代碼有很大不同址否,思維上的轉(zhuǎn)變才是理解Rx的關(guān)鍵。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碎紊,一起剝皮案震驚了整個濱河市佑附,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仗考,老刑警劉巖音同,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秃嗜,居然都是意外死亡权均,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拄轻,你說我怎么就攤上這事喂击。” “怎么了萤厅?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我塔橡,道長,這世上最難降的妖魔是什么霜第? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任葛家,我火速辦了婚禮,結(jié)果婚禮上泌类,老公的妹妹穿的比我還像新娘癞谒。我一直安慰自己,他們只是感情好末誓,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布扯俱。 她就那樣靜靜地躺著,像睡著了一般喇澡。 火紅的嫁衣襯著肌膚如雪迅栅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天晴玖,我揣著相機與錄音读存,去河邊找鬼为流。 笑死,一個胖子當(dāng)著我的面吹牛让簿,可吹牛的內(nèi)容都是我干的敬察。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼尔当,長吁一口氣:“原來是場噩夢啊……” “哼莲祸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起椭迎,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤锐帜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后畜号,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缴阎,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年简软,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛮拔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡痹升,死狀恐怖建炫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情视卢,我是刑警寧澤踱卵,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站据过,受9級特大地震影響惋砂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绳锅,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一西饵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鳞芙,春花似錦眷柔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喳坠,卻和暖如春鞠评,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背壕鹉。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工剃幌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留聋涨,地道東北人。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓负乡,卻偏偏與公主長得像牍白,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抖棘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 心在慌亂迷霧在前方那十字的墓碑刻著曾經(jīng)的輝煌那墓碑的主人持著榮耀的勛章亡靈在呻吟惡魔在低語他將要蘇醒茂腥,他將要回歸 ...
    但丁如是說閱讀 401評論 0 0
  • 歡迎關(guān)注微信公眾號:說歌詞(shuogeci) 昨天看了《奇葩大會》新一期础芍,“晚上12點好餓哦杈抢,我該不該吃宵夜数尿?”...
    上帝擲色子嗎閱讀 753評論 0 1
  • “竹泫泫以垂露右蹦,柳依依而迎蟬,鷗雙雙以赴水歼捐,鷺軒軒而歸田何陆。” 前兩天偶然看到一部《茅山》的紀(jì)錄片豹储,聽到畫外音讀到陶...
    漏報閱讀 560評論 0 0
  • 新聞贷盲,有數(shù)據(jù)顯示剥扣,2017年上半年巩剖,上海市送餐外賣行業(yè)發(fā)生交通事故76起,平均兩天半就有一人傷亡钠怯。在南京佳魔,上半年涉...
    田園聽雨閱讀 269評論 0 1