一篇不太一樣的RxJava介紹

W_BinaryTree發表於2017-12-04

距離上篇文章已有半年的時間,雖然這期間沒什麼輸出,但是還是關注著RxJava和國內一些動向/文章等等,感覺很多人對RxJava還有些許誤會和“錯誤”的理解。所以今天我們從最基礎的開始,來了解一下RxJava。

我們先退回一步,忘了RxJava,討論一個我們Android開發,甚至很多開發都會遇到的非常棘手的問題,非同步問題。 舉個例子,我們需要向 MVP/MVVM中的 Model層 取到一組資料,來做我們的首頁展示,比如這樣的:

dribbble.jpg

一個簡單的Dribbble頁面,由於圖片很多,我們不希望在第一次請求就獲得所有的圖片,我們更希望先獲得一些圖片的MetaData,比如url等等。然後在分別非同步載入,來實現一個比較好的使用者體驗。這裡我們先不考慮RecyclerView加Glide的組合,用簡單的虛擬碼來顯示。

我們的 Model 層需要兩個方法,一個是獲取到我們首頁圖片的MetaData,一個是根據MetaData獲取Bitmap。

如果萬物皆可同步,那麼程式碼非常簡單:

interface Model{
    fun getList() : List<MetaData>

    fun getBitmap(metaData : MetaData) : Bitmap
}
複製程式碼

很多同學喜歡RxJava都是因為鏈式呼叫看起來非常舒服,而鏈式呼叫或者說高階函式或者操作符並不是RxJava的專利,Java8的 Stream API和Kotlin都有相關的操作,比如我們的程式碼在kotlin中可以這樣呼叫

    model.getList()
        .map { model.getBitmap(it) }
        .forEach { showBitMap(it) }
複製程式碼

是不是看起來和你們所謂的優雅簡潔的RxJava鏈式呼叫一樣呢?

但是同步意味著阻塞,而網路載入Bitmap大家都知道是非常耗時的。為了不阻塞使用者介面(UI執行緒),我們希望他在後臺非同步執行,執行後再輸出到前臺。 所以我們Android中最簡單直接的方法就是加入CallBack,來實現非同步通訊。我們的程式碼就變成這樣

//定義CallBack
interface CallBack<T> {
    fun onSuccess(t:T)

    fun onError(error:Error)
}

interface Model{
    fun getList(callback:CallBack<List<MetaData>>)

    fun getBitmap(metaData:MetaData, callback:Callback<MetaData>)
}
複製程式碼

看過很多RxJava教程的同學肯定覺得這裡我要講Callback Hell(回撥地獄)了,然後開始展示程式碼RxJava來解決回撥地獄的問題,但如果這樣我這篇文章也沒什麼意義了,豈不是和很多入門文章都一樣了?

我們先來看看為什麼我們會出現回撥地獄?而在同步的時候卻可以保持我們喜歡的**“鏈式呼叫”** 我們在同步的時候,我們做的事情可以簡化成這樣: 進入主介面 -> 通過getList方法獲取 List -> 根據list逐一操作獲取bitmap -> 顯示bitmap 可以看到,我們確實是一條鏈,所以很簡單的通過stream api來實現**“鏈式呼叫”**。

但是非同步的時候呢? 進入主介面 -> getList(callback:CallBack<List>)方法將我們的CallBack傳給後臺 -> 等待後臺回撥我們的CallBack

重點來了,與同步的不同,我們這裡不是直接獲得了我們的List。而是在等待著非同步的另一方通知我們。 同步的時候,我們直接拉取資料 :

1.png

而非同步的時候,直觀的看我們應該是在“等待”資料,非同步物件向我們推送資料。

2.png
3.png

所以在我們的角度,我們是被動的,也就是英語中的reactive ,也就是所謂的響應式

我們回到我們的例子:

同步的時候,我們是這樣的

interface Model{
    fun getList() : List<MetaData>

    fun getBitmap(metaData : MetaData) : Bitmap
}
複製程式碼

而非同步的時候,我們的方法沒有了返回值,多了個引數,所以不能使用漂亮的**“鏈式呼叫”。 這是因為List 本身,就是一種同步的型別。我們每次操作List,都是對List來拉取**資料。不信?我們來看下:

大家都知道List並不是最基礎的集合,常用的集合還有HashMap,Set,Table,Vector等等等等。他們都有一個共同的父類: Iterable

interface Iterable<out T> {
    fun iterator(): Iterator<T>
} 
複製程式碼

這裡的iterator就是迭代器,他是這個樣子的

interface Iterator<out T> {

    fun next(): T
    
    fun hasNext(): Boolean
}
複製程式碼

使用的時候也就是我們最麻煩的迭代方式:

    val i = iterator()
    while(i.hasNext()){
        val value = i.next()
    }
複製程式碼

所以我們在Java中有了foreach,以及後面的stream api等等語法糖。 這裡我們看到了,我們每次確實首先詢問List,有沒有值,如果有我們獲取這個值,如果沒有,跳出迴圈,對List的操作結束。讀取完畢。

想象一下,如果我們有一種 AsyncList,對他的讀取都是AsyncList來通知我們,然後再和同步的時候一樣使用高階函式比如map/foreach等等該多好。比如

interface Model{
    fun getList() : AsyncList<MetaData>

    fun getBitmap(metaData : MetaData) : Bitmap
}
複製程式碼

我們就可以像同步一樣,

        model.getList()
        .map { model.getBitmap(it) }
        .forEach { showBitMap(it) }
複製程式碼

現在我們來根據Iterable設計我們的 AsyncList,上面我們知道了Iterable是同步的,是拉取資料,我們需要的AsyncList是非同步的,是他推送資料給我們。 我們和List一樣,給所有的非同步集合來一個父類,來設計一個AsyncIterable,我們知道Iterable提供Iterator通過我們主動詢問Iteratornext,hasNext等方法我們主動拉取資料。 所以我們的AsyncIterable理論上來說,應該是我們通過註冊AsyncIterator的方式,將我們的AsyncIterator傳遞給AsyncIterable,讓他來通知我們,實現非同步和推送資料。 所以我們的AsyncIterable的實現應該是這樣的

interface AsyncIterable<T> {
    fun iterator(iterator : AsyncIterator<T>) 
} 
複製程式碼

(看起來好像有點眼熟?)

我們再來設計AsyncIterator,同步的方式兩個方法,一個是hasNext,也就是我們主動詢問iterable接下來之後還有沒有值的過程,如果是非同步的方式,這應該是我們的AsyncIterable,來通知我們,他接下來以後還有沒有值。 所以變成了這樣:

    fun hasNext(has : Boolean) 
複製程式碼

對的,通過這種類似CallBack的方式,通知我們有沒有值。true就是還有值,一旦接收到false,就代表迭代結束,我們的AsyncIterable已經遍歷完成了。 另一個方法 next() 就是我們來主動詢問,當前的值是什麼。所以我們的AsyncIterable就是通過這個方法,來通知我們當前的值是什麼,依然還是通過類似CallBack的方式:

    fun onNext(current:T)
複製程式碼

(是不是有些眼熟?(手動滑稽))

這裡有兩個問題: 第一個問題:我們在這裡隱藏了一個錯誤,因為hasNext()方法返回 false的時候不一定是沒有接下來的值了,也有可能是處理當前值的時候出現了某些個錯誤或者異常,這樣他就不能處理接下來的值,這時候我們的app就會崩潰。所以在非同步的時候,我們希望我們的AsyncIterable在出錯的時候,可以通知我們他出錯了,我們也就不進行接下來的處理了。所以我們有了:

    fun onError(e:Throwable)
複製程式碼

(是不是也有些眼熟?(手動滑稽))

第二個問題,在hasNext方法顯然有些過於多餘,因為在同步的時候,我們並不知道他究竟接下來有沒有值,所以我們每次訪問List的時候,要詢問還有沒有接下來的值,我們再進行下一步。而非同步的時候,我們的AsyncIterable肯定知道他自己接下來有沒有值了,我們只希望在最後他沒有值的時候通知我們結束了即可,也就是說我們之前的 hasNext(true)都是多餘的。我們其實只關心hasNext(true)被呼叫的時候。所以我們把他簡化成只有最後結束的時候才呼叫的方法:

    fun onComplete()
複製程式碼

這樣,我們有了我們的AsyncIterator

interface AsyncIterator<T> {

    fun onNext(current:T): 
    
    fun onComplete()

    fun onError(e:Throwable)
}
複製程式碼

對的,他就是我們RxJava中的 Observer,而我們的 Asynciterable 就對應著我們的Observable。

interface Observable<T> {
    fun subscribe(observer : Observer<T>) 
} 
複製程式碼

由此,我給Observable下一個的定義:

Observable 是一組非同步資料的集合

對的,他就是一個集合,和List,Set,Vector一樣。他是一組資料,Collection可以包含0,1很多甚至無限個資料。所以Observable也可以包含0,1,n,甚至無限個資料。

當我們在處理Collection出現異常時(比如NullPointerException),我們的程式會崩潰,不會有接下來的處理。所以我們的Observable在收到onError之後,也不會再有資料推送給我們。

Collection可以通過高階函式(High Oroder Function)進行組合,變換等等,所以作為集合之一的Observable也可以進行組合,變換。

Iterable進行操作,我們是通過getIterator方法,來獲得Iterator來進行主動詢問,拉取資料實現迭代。

Observable進行操作,我們是通過subscribe方法,來註冊Observer來進行被動獲取資料,由Obseravble來推送資料,實現迭代。

我們費了這麼大力氣,終於抽象出來一個非同步的集合。那麼他的好處是什麼呢?

  1. 首先,這種推送資料的方式才是我們直觀的,非同步操作方法,我們在上文了解了,非同步操作的時候,作為接收方。我們是被動的,我們沒辦法詢問生產方到底有沒有完成非同步任務。只有生產方自己才知道他有沒有完成生產,所以他在完成生產後通知我們,並把結果交給我們這是一種直觀的解決方案。而Java或者其他高階語言沒有提供這一方案。我們才自定義CallBack來實現回撥。

  2. 在使用CallBack方案的時候,你知道的資訊太多了。舉個例子,我們上文中的

    fun getList(callback:CallBack<List<MetaData>>)
複製程式碼

這個方法。我們通過callback知道了,這應該是一個非同步操作。可能是耗時的,所以我們可能需要一個執行緒來執行他,執行之後,他又會給我一個List,而這個list卻又是同步的。你需要關心的事情太多了。 俗話說,把握現在 展望未來! 我們能處理好現在的事情就已經很不錯了,Observable則解決了這一問題。我們上面的方法改完之後應該是這樣的

    fun getList() : Observable<List<MetaData>>
複製程式碼

最正確的可能應該是這樣的:

    fun getList() : Observable<MetaData>
複製程式碼

對的,因為Observable本身就是個集合,無需再和同步的List巢狀使用。但是由於伺服器設計原因, Observable<List>這種使用方式還是很常見的。 在Observable我們無需關心這個方法究竟是怎麼生成的。我們像往常一樣迭代資料,我們只需要知道,他生產出資料之後,會通知我即可。 至於你到底怎麼生產資料給我?在什麼執行緒?是同步的非同步的?有沒有阻塞?

I don't really care!

  1. 操作符,對的因為Observable是一個數學上的集合。集合就可以進行一系列的變換,通過我們定義的高階函式,比如map,filter等等。這些操作符不是RxJava的專利,他是我們對集合的一些常見操作。我們對List,Vector等等也應該可以進行這些操作,而Java本身沒有提供這些。在Java 8後通過stream API補充了這些方法,而RxJava的一大優勢就是不僅僅提供了這個非同步的集合類Observable。還提供了上百個常用的操作符。

##總結

通過這篇文章,我的目的是讓你理解究竟什麼是Observable,為什麼Observable是這麼設計的,好處是什麼,解決了什麼問題。 而答案也很明顯。 Observable是一組非同步資料的集合,因為非同步操作和同步操作有著本質上的區別(推送資料和拉取資料)所以我們根據iterable反過來設計observable。 好處是保持了數學上的集合定義,擺脫了Callback,通過操作符(高階函式)可以對集合實現一些變換操作。解決了通常情況非同步操作不直觀,複雜,回撥地獄等等問題。

####參考文獻(部分連結可能需要梯子)

  1. A Playful Introduction to Rx by Erik Meijer
  2. Expert to Expert: Brian Beckman and Erik Meijer - Inside the .NET Reactive Framework (Rx)
  3. ReactiveX

相關文章