是時候學習 RxSwift 了

eleven0220 8年前發布 | 10K 次閱讀 RxSwift

相信在過去的一段時間里,對 RxSwift 多少有過接觸或耳聞,或者已經積累了不少實戰經驗。此文主要針對那些在門口徘徊,想進又拍踩坑的同學。

為什么要學習 RxSwift

當決定做一件事情時,至少要知道為什么。RxSwift 官網舉了 幾個例子 ,比如可以統一處理 Delegate , KVO , Notification ,可以綁定 UI,方便網絡請求的處理等等。但這些更多的是描述可以用 RxSwift 來做什么,跟為什么要使用 RxSwift 還是會有點不同。

我們先來分析下 GUI 編程的本質,我喜歡把它抽象為視圖和數據的結合。其中視圖負責兩件事:展示和交互,展示什么由數據決定。

其中單向數據流可以通過之前介紹的 ReSwift 完成。看起來好像沒 RxSwift 什么事情,其實不然,RxSwift 可以在 UniDirectional Data Flow 的各個階段都發揮作用,從而讓 Data 的處理和流動更加簡潔和清晰。

  1. 通過對 RxCocoa 的各種回調進行統一處理,方便了「Interact」的處理。
  2. 通過對 Observable 的 transform 和 composite,方便了 Action 的生成(比如使用 throttle 來壓縮 Action )。
  3. 通過對網絡請求以及其他異步數據的獲取進行 Observable 封裝,方便了異步數據的處理。
  4. 通過 RxCocoa 的 binding,方便了數據的渲染。

所以 ReSwift 規范了數據流,RxSwift 為數據的處理提供了方便,這兩個類庫的結合,可以產生清晰的架構和易維護的代碼。

當然,前提是對它們有足夠的了解,尤其是 RxSwift,也就是我們今天的主角。

什么是 RxSwift

在 GUI 編程中,我認為比較復雜的有三個部分:

  1. 非原生 UI 效果的實現(比如產品經理們經常冒出來的各種想法)。
  2. 大量狀態的維護。
  3. 異步數據的處理。

1)不在這次的討論范疇(這里的學問也很多,比如流暢性和性能)。2) 可以通過單向數據流來解決(結合 Immutable Data)。3) 可以通過 RxSwift 來解決。那么 RxSwift 是如何處理異步數據的呢?

在說 RxSwift 之前,先來說下 Rx, ReactiveX 是一種編程模型,最初由微軟開發,結合了觀察者模式、迭代器模式和函數式編程的精華,來更方便地處理異步數據流。其中最重要的一個概念是 Observable 。

舉個簡單的例子,當別人在跟你說話時,你就是那個觀察者,別人就是那個 Observable ,它有幾個特點:

  1. 可能會不斷地跟你說話。( onNext: )
  2. 可能會說錯話。( onError: )
  3. 結束會說話。( onCompleted )

你在聽到對方說的話后,也可以有幾種反應:

  1. 根據說的話,做相應的事,比如對方讓你借本書給他。( subscribe )
  2. 把對方說的話,加工下再傳達給其他人,比如對方說小張好像不太舒服,你傳達給其他人時就變成了小張失戀了。( map: )
  3. 參考其他人說的話再做處理,比如 A 說某家店很好吃,B 說某家店一般般,你需要結合兩個人的意見再做定奪。( zip: )

所以,從生活中也能看到 Rx 的影子。「有些事情急不得,你得等它自己熟」,異步,其實就是跟時間打交道,不同的時間,拿到的數據也會不一樣。

這里的核心是當數據有變化時,能夠立刻知曉,并且通過組合和轉換后,可以即時作出響應。有點像塔防,先在路上的各個節點埋好武器,然后等著小怪獸們過來。

RxSwift Workflow

大致分為這么幾個階段:先把 Native Object 變成 Observable,再通過 Observable 內置的各種強大的轉換和組合能力變成新的 Observable,最后消費新的 Observable 的數據。

Native Object -> Observable

.rx extension

假設需要處理點擊事件,正常的做法是給 Tap Gesture 添加一個 Target-Action,然后在那里實現具體的邏輯,這樣的問題在于需要重新取名字,而且丟失了上下文。RxSwift (確切說是 RxCocoa) 給系統的諸多原生控件(包括像 URLSession )提供了 rx 擴展,所以點擊的處理就變成了這樣:

let tapBackground = UITapGestureRecognizer()

tapBackground.rx.event
    .subscribe(onNext: { [weak self] _ in
        self?.view.endEditing(true)
    })
    .addDisposableTo(disposeBag)

view.addGestureRecognizer(tapBackground)

是不是簡潔了很多。

Observable.create

通過這個方法,可以將 Native 的 object 包裝成 Observable ,比如對網絡請求的封裝:

public func response(_ request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
    return Observable.create { observer in
        let task = self.dataTaskWithRequest(request) { (data, response, error) in
            observer.on(.next(data, httpResponse))
            observer.on(.completed)
        }

        task.resume()

        return Disposables.create {
            task.cancel()
        }
    }
}

出于代碼的簡潔,略去了對 error 的處理,使用姿勢類似

let disposeBag = DisposeBag()

response(aRequest)
  .subscribe(onNext: { data in
    print(data)
  })
  .addDisposableTo(disposeBag)

這里有兩個注意點:

  1. Observerable 返回的是一個 Disposable ,表示「可扔掉」的,扔哪里呢,就扔到剛剛創建的袋子里,這樣當袋子被回收( dealloc )時,會順便執行一下 Disposable.dispose() ,之前創建 Disposable 時申請的資源就會被一并釋放掉。
  2. 如果有多個 subscriber 來 subscribe response(aRequest) 那么會創建多個請求,從代碼也可以看得出來,來一個 observer 就創建一個 task,然后執行。這很有可能不是我們想要的,如何讓多個 subscriber 共享一個結果,這個后面會提到。

Variable()

Variable(value) 可以把 value 變成一個 Observable ,不過前提是使用新的賦值方式 aVariable.value = newValue ,來看個 Demo

let magicNumber = 42

let magicNumberVariable = Variable(magicNumber)
magicNumberVariable.asObservable().subscribe(onNext: {
    print("magic number is \($0)")
})

magicNumberVariable.value = 73

// output
// 
// magic number is 42
// magic number is 73

起初看到時,覺得還蠻神奇的,跟進去看了下,發現是通過 subject 來做的,大意是把 value 存到一個內部變量 _value 里,當調用 value 方法時,先更新 _value 值,然后調用內部的 _subject.on(.next(newValue)) 方法告知 subscriber。

Subject

Subject 簡單來說是一個可以主動發射數據的 Observable ,多了 onNext(value) , onError(error) , ‘onCompleted’ 方法,可謂全能型選手。

let disposeBag = DisposeBag()
let subject = PublishSubject<String>()

subject.addObserver("1").addDisposableTo(disposeBag)
subject.onNext(":dog:")
subject.onNext(":cat:")

subject.addObserver("2").addDisposableTo(disposeBag)
subject.onNext(":a:")
subject.onNext(":b:")

記得在 RAC 時代,subject 是一個不太推薦使用的功能,因為過于強大了,容易失控。RxSwift 里倒是沒有太提及,但還是少用為佳。

Observable -> New Observable

Observable 的強大不僅在于它能實時更新 value,還在于它能被修改/過濾/組合等,這樣就能隨心所欲地構造自己想要的數據,還不用擔心數據發生變化了卻不知道的情況。

Combine

Combine 就是把多個 Observable 組合起來使用,比如 zip (小提示:如果對這些函數不太敏感,可以 實際操作下 ,體會會更深些)

zip 對應現實中的例子就是拉鏈,拉鏈需要兩個元素這樣才能拉上去,這里也一樣,只有當兩個 Observable 都有了新的值時,subscribe 才會被觸發。

let stringSubject = PublishSubject<String>()
let intSubject = PublishSubject<Int>()

Observable.zip(stringSubject, intSubject) { stringElement, intElement in
    "\(stringElement) \(intElement)"
    }
    .subscribe(onNext: { print($0) })
    .addDisposableTo(disposeBag)

stringSubject.onNext(":a:")
stringSubject.onNext(":b:")

intSubject.onNext(1)
intSubject.onNext(2)

// output
//
// :a: 1
// :b: 2

如果這里 intSubject 始終沒有執行 onNext ,那么將不會有輸出,就像拉鏈掉了一邊的鏈子就拉不上了。

除了 zip ,還有其他的 combine 的姿勢,比如 combineLatest / switchLatest 等。

Transform

這是最常見的操作了,對一個 Observable 的數值做一些小改動,然后產出新的值,依舊是一個 Observable 。

let disposeBag = DisposeBag()
Observable.of(1, 2, 3)
    .map { $0 * $0 }
    .subscribe(onNext: { print($0) })
    .addDisposableTo(disposeBag)

這是大致的實現(摘自官網)

extension ObservableType {
    func myMap<R>(transform: E -> R) -> Observable<R> {
        return Observable.create { observer in
            let subscription = self.subscribe { e in
                    switch e {
                    case .next(let value):
                        let result = transform(value)
                        observer.on(.next(result))
                    case .error(let error):
                        observer.on(.error(error))
                    case .completed:
                        observer.on(.completed)
                    }
                }

            return subscription
        }
    }
}

接受一個 transform 閉包,然后返回一個 Observable ,因為接下來使用者將會對 myMap 的結果進行 subscribe,所以需要在 create 內部 subscribe 一下,不然最開始的那個 Observable 就是個 Cold Observable ,一個 Cold Observable 是不會產生新的數據的。

Filter

Filter 的作用是對 Observable 傳過來的數據進行過濾,只有符合條件的才有資格被 subscribe。寫法上跟 map 差不多,就不贅述了。

Connect

這是挺有意思的一塊,在之前介紹 Observable.create 時有提到過,一個 Observable 被多次 subscribe 就會被多次觸發,如果一個網絡請求只想被觸發一次,同時支持多個 subscriber,就可以使用 publish + connect 的組合。

當一個 Observable 使用了 publish() 方法后,正常的 subscribe 就不會觸發它了,除非 connect() 方法被調用。而且每次 subscribe 不會導致 Observable 重新針對 observer 處理一遍。看一下這張圖

有兩塊需要注意:

  1. connect() 之前的兩次 subscribe 并沒有產生新的 value。
  2. connect() 之后 subscribe 的,只是等待新的 value,同時新的 value 還會分發給之前的 subscriber。
  3. 即使所有的 subscription 被 dispose , Observable 依舊處于 hot 狀態,就好像還以為有人關心新的值一樣。(這可能不是想要的結果)

針對第 3 點,可以使用 refcount() 來代替 connect() ,前者會在沒有 subscriber 時自動「冷」下來,不會再產生新的值。(Demo 取自 這里

let myObservable = Observable<Int>.interval(1, scheduler: MainScheduler.instance).publish().refCount() // 1)

let mySubscription = myObservable.subscribe(onNext: {
    print("Next: \($0)")
})

delay(3) {
    print("Disposing at 3 seconds")
    mySubscription.dispose()
}

delay(6) {
    print("Subscribing again at 6 seconds")
    myObservable.subscribe(onNext: {
        print("Next: \($0)")
    })
}

輸出

Starting at 0 seconds
Next: 0
Next: 1
Next: 2
Disposing at 3 seconds
Subscribing again at 6 seconds
Next: 0
Next: 1

可以看到,3 秒后 subscription dispose,此時沒有任何 subscriber 還關心 Observable ,因此就重置了,所以 6 秒后又回到了初始狀態(如果變成 connect 方法的話,會發現 6 秒后會輸出 Next: 6 / Next: 7 )

那如果后加入的 subscriber 想要之前的數據怎么辦?可以對原始的 Observable 設置 replay(n) ,表示最多返回 n 個元素給后加入的 subscriber。

Tips

上面介紹的是最基本的概念。順便提一下比較常見的幾個問題:

如何處理 Scheduler?

默認代碼都是在當前線程中執行的,如果要手動切換線程,可以使用 subsribeOn 和 observeOn 兩種方式,一般來說后者用得會多一些,那這兩者有什么區別呢?

subscribeOn 跟位置無關,也就是無論在鏈式調用的什么地方, Observable 和 subscription 都會受影響;而 observeOn 則僅對之后的調用產生影響,看個 Demo:

var observable = Observable<Int>.create { (observer: AnyObserver<Int>) -> Disposable in
    print("observable thread: \(Thread.current)")
    observer.onNext(1)
    observer.onCompleted()
    return Disposables.create()
}

let disposeBag = DisposeBag()

observable
    .map({ (e) -> Int in
        print("map1 thread: \(Thread.current)")
        return e + 1
    })
    .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInteractive)) // 1
    .map({ (e) -> Int in
        print("map2 thread: \(Thread.current)")
        return e + 2
    })
    .subscribe(onNext:{ (e) -> Void in
        print("subscribe thread: \(Thread.current)")
    })
    .addDisposableTo(disposeBag)

如果 1) 是 observeOn ,那么輸出如下

observable thread: <NSThread: 0x7f901cc0d510>{number = 1, name = main}
map1 thread: <NSThread: 0x7f901cc0d510>{number = 1, name = main}
map2 thread: <NSThread: 0x7f901ce15560>{number = 3, name = (null)}
subscribe thread: <NSThread: 0x7f901ce15560>{number = 3, name = (null)}

可以看到 observable thread 和 map1 thread 依舊保持當前線程,但 observeOn 之后就變成了另一個線程。

如果 1) 是 subscribeOn ,那么會輸出

observable thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}
map1 thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}
map2 thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}
subscribe thread: <NSThread: 0x7fbdf1e097a0>{number = 3, name = (null)}

可以看到全都變成了 subscribeOn 指定的 Queue。所以 subscribeOn 的感染力很強,連 Observable 都能影響到。

Cold Observable 和 Hot Observable

Cold 相當于 InActive,就像西部世界里,未被激活的機器人一樣;Hot 就是處于工作狀態的機器人。

Subscription 為什么要 Dispose?

因為有了 Subscriber 所以 Observable 被激活,然后內部就會使用各種變量來保存資源,如果不 dispose 的話,這些資源就會一直被 keep,很容易造成內存泄漏。

同時手動 dispose 又嫌麻煩,所以就有了 DisposeBag ,當這個 Bag 被回收時,Bag 里面的 subscription 會自動被 dispose,相當于從 MRC 變成了 ARC。

小結

RxSwift 如果概念上整理清楚了,會發現其實并不難,多從 Observable 的角度去思考問題,多想著轉換和組合,慢慢就會從命令式編程轉到聲明式編程,對于抽象能力和代碼的可讀性都會有提升。

 

來自:http://limboy.me/tech/2016/12/11/time-to-learn-rxswift.html

 

 本文由用戶 eleven0220 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!