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時砌滞,在map
中throw
一個自定義的錯誤類型侮邀,這個錯誤會被傳遞到下面的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
將這個Observable
與retryWhen
中的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)鍵。