##寫在前面
前面兩篇文章中介紹幾乎全部都是基礎,而且如果前面兩篇吃透了的話,RxJava就算完全入門了。那入門之後就得學一些比較高階一點的用法及其原理了。
所以這篇文章來介紹一下RxJava中另一個核心內容—— 變換 。本來準備連它的原理一起說明,但是變換的原理稍微複雜了點,如果連在一起寫可能篇幅過長,看起來也就比較枯燥。所以暫時不講原始碼,先來看看它的基本使用。當然,依然是通過小案例的形式來說明,這樣就不會枯燥。
說完幾個基本變換操作符之後,再瞭解一下RxJava中的FuncX與ActionX的作用與區別,最後補充一下上一次說執行緒控制Scheduler的時候故意省略的一部分內容,至於省略的原因後面再說。
所以全文的目錄如下:
- 寫在前面
- 神奇的變換
- 回顧
- Map
- FuncX與ActionX
- FlatMap
- 再話Scheduler
- 結語
- 參考資料
- 專案原始碼
##神奇的變換
###回顧
在說變換操作之前,先來回顧一下之前非同步獲取網路圖片的小案例。
//建立被觀察者
Observable.create(new Observable.OnSubscribe<Bitmap>() {
/**
* 複寫call方法
*
* @param subscriber 觀察者物件
*/
@Override
public void call(Subscriber<? super Bitmap> subscriber) {
//通過URL得到圖片的Bitmap物件
Bitmap bitmap = GetBitmapForURL.getBitmap(url);
//回撥觀察者方法
subscriber.onNext(bitmap);
subscriber.onCompleted();
}
})
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.observeOn(AndroidSchedulers.mainThread()) // 指定Subscriber的回撥發生在UI執行緒
.subscribe(new Observer<Bitmap>() { //訂閱觀察者(其實是觀察者訂閱被觀察者)
@Override
public void onNext(Bitmap bitmap) {
mainImageView.setImageBitmap(bitmap);
}
@Override
public void onCompleted() {
mainProgressBar.setVisibility(View.GONE);
Log.i(" onCompleted ---> ", "完成");
}
@Override
public void onError(Throwable e) {
Log.e(" onError --->", e.toString());
}
});
複製程式碼
案例本身是沒有問題的,但是在這裡需要注意一下,在Observable的create方法中就已經規定了傳送的物件的型別是 Bitmap ,而這個Bitmap是通過圖片的Url來獲取得到的,得到後再傳送給Subscriber(就是觀察者Observer,再強調一下後面的文章一律用Subscriber代替Observer,原因上一篇文章已經強調過了,不再贅述)。
簡單來說,就是一開始我們就規定好了要傳送一個物件的型別。
那是否可以這樣設想,我們一開始傳送 String 型別的Url,然後通過某種方式再將得到的 Bitmap 傳送出去呢?
用圖來說就是這樣:
###Map
答案當然是有的,RxJava中提供了一種操作符: Map ,它的官方定義是這樣的:
Map操作符對原始Observable發射的每一項資料應用一個你選擇的函式,然後返回一個發射這些結果的Observable。
這句話簡單來說就是,它對Observable傳送的每一項資料都應用一個函式,並在函式中執行變換操作。
如果還是不明白的話,那就畫圖去理解,這個圖也是官方的圖,只不過我重新畫了一下:
從圖中可以看到,這是一對一的轉換,就是一個單獨的資料轉成另一個單獨的資料,這一點需要跟後面的 flatmap 對比,所以需要留意一下這句話。
好了,瞭解了基本原理,現在就來給之前的程式碼進行一個改造:
//先傳遞String型別的Url
Observable.just(url)
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String s) {
//通過Map轉換成Bitmap型別傳送出去
return GetBitmapForURL.getBitmap(s);
}
})
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.observeOn(AndroidSchedulers.mainThread()) // 指定Subscriber的回撥發生在UI執行緒
//可以看到,這裡接受的型別是Bitmap,而不是String
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
mainImageView.setImageBitmap(bitmap);
mainProgressBar.setVisibility(View.GONE);
}
});
複製程式碼
這裡我們先用just操作符傳遞一個String型別的Url進去,然後在map操作符中,利用 Func1 類的call方法返回一個 Bitmap 出去,最後在 subscribe操作中的 Action1 類中接收一個 Bitmap 物件。
這樣就成功的將初始Observable所傳送引數的型別通過 map 轉換成了其他的型別。這就是 map 操作符的妙用。
###FuncX與ActionX
在上面的程式碼中,出現了這兩個類: Func1 與 Action1 ,這是什麼意思呢?
####ActionX
先來解釋Action1。點開它的原始碼:
/**
* A one-argument action.
* @param <T> the first argument type
*/
public interface Action1<T> extends Action {
void call(T t);
}
複製程式碼
原來是有一個引數的介面,介面中有一個 單引數無返回值的call方法 。由於 onNext(T obj) 和 onError(Throwable error) 也是單引數無返回值的,因此 Action1 可以將 onNext(obj) 和 onError(error) 打包起來傳入 subscribe() 以實現 不完整定義的回撥 。
也就是說,這種回撥只呼叫 onNext 與 onError 兩個方法,並不是完整的回撥(完整的是回撥三個方法)。
而對於這種不完整的回撥,RxJava 會自動根據定義建立出 Subscriber 。
另外,與Action1類似的是 Action0 ,這個也比較常用,依然點進去看一下原始碼:
/**
* A zero-argument action.
*/
public interface Action0 extends Action {
void call();
}
複製程式碼
跟上面的一對比,一下就恍然大悟了,其實就是 無引數無返回值的call方法 ,由於 onCompleted() 方法也是無參無返回值的,因此 Action0 可以被當成一個包裝物件,將 onCompleted() 的內容打包起來,作為一個引數傳入 subscribe() 以實現 不完整定義的回撥 。
除此之外:
RxJava 是提供了多個 ActionX 形式的介面 (例如 Action2, Action3) 的,它們可以被用以包裝不同的無返回值的方法。
好吧,說得通俗一點:
- Action0 就是把 onCompleted() 作為引數傳入 subscribe() 。
- Action1 就是把 onNext() 與 onError() 作為引數傳入 subscribe() 。
####FuncX
瞭解了ActionX之後,來看這個Func1。點進去看原始碼:
/**
* Represents a function with one argument.
* @param <T> the first argument type
* @param <R> the result type
*/
public interface Func1<T, R> extends Function {
R call(T t);
}
複製程式碼
一對比,很容易發現,它跟Action1很相似,也是RxJava的一個介面。但是有一個明顯的區別在於,Func1包裝的是 有返回值 的方法。
而且與ActionX一樣,FuncX也有很多個,主要用於不同個數的引數的方法。我們只要記著一點:
FuncX 和 ActionX 的區別在 FuncX 包裝的是有返回值的方法。
###FlatMap
好了,至此就把Map說完了。還記得之前強調的那句話麼,Map是一對一的轉換,那麼有沒有一對多的轉換呢?當然有,就是現在要說的 FlatMap 。
依然先看官方定義:
FlatMap操作符使用一個指定的函式對原始Observable發射的每一項資料執行變換操作,這個函式返回一個本身也發射資料的Observable,然後FlatMap合併這些Observables發射的資料,最後將合併後的結果當做它自己的資料序列發射。
好吧,定義總是很迷糊。沒關係,現在嘗試用圖的形式來說明:
簡單來說就是分別將一組資料中的每個資料進行轉換,轉換後再把轉換後的資料合併到一條序列上進行傳送。
不過需要注意的是,轉換後的每個資料本身其實也是一個可以傳送資料的 Observable ,所以將上面圖簡化一下就是如下圖所示:
從簡圖可以看出,與Map相比較,FlatMap是能進行一對多的轉換。
好了,閒話不多說,我們來看具體的案例。案例就是一個GridView非同步載入多張網路圖片。
對於這個專案我事先說明兩點:
- 不去說明GridView如何使用
- 這個專案並不是對FlatMap特性介紹的最佳案例,但是為了與前面的案例做對比,暫時就用這個案例。
好了,為了回顧前文from操作符的使用,我們將四個圖片的Url加到一個資料中去,也就是一組Url資料:
private final String url1 = "http://www.iamxiarui.com/wp-content/uploads/2016/06/套路.png";
private final String url2 = "http://www.iamxiarui.com/wp-content/uploads/2016/06/為什麼我的流量又沒了.png";
private final String url3 = "http://www.iamxiarui.com/wp-content/uploads/2016/05/cropped-iamxiarui.com_2016-05-05_14-42-31.jpg";
private final String url4 = "http://www.iamxiarui.com/wp-content/uploads/2016/05/微信.png";
//一組Url資料
private final String[] urls = new String[]{url1, url2, url3, url4};
複製程式碼
然後來看flatmap如何處理:
//先傳遞String型別的Url
Observable.from(urls)
.flatMap(new Func1<String, Observable<String>>() {
@Override
public Observable<String> call(String s) {
return Observable.just(s);
}
})
複製程式碼
可以看到,它是將 一組String型別的Urls 轉換成一個 傳送單獨的String型別Url的Observable 。
既然轉換成了能夠傳送單獨資料的Observable,那麼就簡單多了,就用剛剛學的map操作符吧:
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String s) {
//通過Map轉換成Bitmap型別傳送出去
return GetBitmapForURL.getBitmap(s);
}
})
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.observeOn(AndroidSchedulers.mainThread()) // 指定Subscriber的回撥發生在UI執行緒
//可以看到,這裡接受的型別是Bitmap,而不是String
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
mainImageView.setImageBitmap(bitmap);
mainProgressBar.setVisibility(View.GONE);
}
});
複製程式碼
現在我們來看看執行後的動態圖:
可以看到,它是依次載入各張圖片。
還記得我之前說這個並不是最好的案例麼,為什麼呢?因為Flatmap有一個特性:
FlatMap對這些Observables發射的資料做的合併操作可能是交錯的。
什麼意思呢?也就是這一組資料轉換成單獨資料後可能順序會發生改變,從我這個案例來看,並沒有出現這種情況,所以我說這並不是一個最完美的案例。
那麼有人就問了,如何讓它不產生交錯呢?
RxJava還給我們提供了一個 concatMap 操作符,它類似於最簡單版本的flatMap,但是它 按次序連線 而不是合併那些生成的Observables,然後產生自己的資料序列。
這個比較簡單,我就不寫案例演示了。
好了至此我們就將 常用且非常重要的 變換操作符講完了。後面的文章會具體分析它的原理。
##再話Scheduler
最後呢,想對Scheduler做一些補充。
還記得之前說Scheduler的時候介紹的兩個操作符麼:
- subscribeOn(): 指定subscribe() 訂閱所發生的執行緒,即 call() 執行的執行緒。或者叫做事件產生的執行緒。
- observeOn(): 指定Observer所執行在的執行緒,即onNext()執行的執行緒。或者叫做事件消費的執行緒。
現在我們多介紹兩個操作符。
###doOnSubscribe
之前在說Subscriber與Observer的不同的時候,提到過Subscriber多了兩個方法。其中 onStart() 方法發生在 subscribe() 方法呼叫後且事件傳送之前 是一個進行初始化操作的方法。但是這個初始化操作並不能指定執行緒。
就那我這個案例來說,裡面有一個進度條,如果要顯示進度條的話必須在主執行緒中執行。但是我們事先並不知道subscribeOn()方法會指定什麼樣的執行緒。所以在onStart方法中執行一些初始化操作是比較有風險的。
那該怎麼辦呢?
RxJava中給我們提供了另外一種操作符: doOnSubscribe ,這個操作符跟onStart方法一樣,都是在 subscribe() 方法呼叫後且事件傳送之前 執行,所以我們一樣可以在這裡面進行初始化的操作。而區別在於它可以指定執行緒。
預設情況下, doOnSubscribe() 執行在 subscribe() 發生的執行緒;而如果在 doOnSubscribe() 之後有 subscribeOn() 的話,它將執行在離它最近的 subscribeOn() 所指定的執行緒。
關於這句話我有兩點疑問:
- 預設情況下執行的執行緒是不是subscribe()發生的執行緒?
- 什麼叫做離它最近的subscribeOn()指定的執行緒?
先撇開疑問,來看一下用法:
Observable.just(url) //IO執行緒
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String s) {
Log.i(" map ---> ", "執行");
Log.i(" map ---> ", Thread.currentThread().getName());
return GetBitmapForURL.getBitmap(s);
}
})
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.doOnSubscribe(new Action0() { //需要在主執行緒中執行
@Override
public void call() {
mainProgressBar.setVisibility(View.VISIBLE);
Log.i(" doOnSubscribe ---> ", "執行");
Log.i(" doOnSubscribe ---> ", Thread.currentThread().getName());
}
})
.subscribeOn(AndroidSchedulers.mainThread()) // 指定subscribe()發生在主執行緒
.observeOn(AndroidSchedulers.mainThread()) // 指定Subscriber的回撥發生在主執行緒
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
mainImageView.setImageBitmap(bitmap);
mainProgressBar.setVisibility(View.GONE);
Log.i(" subscribe ---> ", "執行");
Log.i(" subscribe ---> ", Thread.currentThread().getName());
}
});
複製程式碼
下面這是執行的Log日誌:
可以看到,從 onClick() 觸發後,先執行了 doOnSubscribe() 然後執行 map() ,最後執行繫結操作 subscribe() 。也就是說,它確實是在資料傳送之前呼叫的,完全可以做初始化操作。
好了,現在我們來解決疑問,先解決第二點:什麼是最近的? 將程式碼改成這樣:
...
.subscribeOn(Schedulers.newThread()) // 指定subscribe()發生在新執行緒
.doOnSubscribe(new Action0() { //需要在主執行緒中執行
@Override
public void call() {
Log.i(" doOnSubscribe ---> ", "執行");
Log.i(" doOnSubscribe ---> ", Thread.currentThread().getName());
}
})
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.subscribeOn(AndroidSchedulers.mainThread()) // 指定subscribe()發生在主執行緒
...
複製程式碼
我故意將 doOnSubscribe 寫在兩個 subscribeOn 之間,且後面有兩個subscribeOn ,現在來看日誌:
從日誌明顯可以看出,doOnSubscribe() 執行在IO執行緒,所以結論是:
- 如果在doOnSubscribe()之後指定了subscribeOn(),它決定了doOnSubscribe()在哪種執行緒中執行。
- (1)doOnSubscribe()之前的subscribeOn()不會影響它。
- (2)doOnSubscribe()之後的subscribeOn(),且是最近的才會影響它。
再來看第二個疑問:預設執行緒在哪裡? 將程式碼改成這樣:
...
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.doOnSubscribe(new Action0() { //需要在主執行緒中執行
@Override
public void call() {
Log.i(" doOnSubscribe ---> ", "執行");
Log.i(" doOnSubscribe ---> ", Thread.currentThread().getName());
}
})
.observeOn(Schedulers.io()) // 指定Subscriber的回撥發生在io執行緒
...
複製程式碼
來看Log:
大家看到這個日誌肯定會有疑問,我當時也非常有疑問,為什麼subscribeOn() 與 observeOn() 都指定了IO執行緒,且 doOnSubscribe() 之後並沒有 subscribeOn() ,這個時候它應該預設執行在 subscribe() 所線上程。
而 subscribe() 所線上程已經被 observeOn() 指定在了IO執行緒,所以此時它應該執行在IO執行緒才對啊,為什麼還是 main 執行緒呢?
我找了翻看了WiKi,找了很多資料,甚至看了原始碼都沒有找到是什麼原因。
如果有人知道,請告訴我,謝謝!
###doOnNext
由於from與flatmap操作符能傳送多個資料,假設有這樣的需求,需要在每個資料傳送的時候提示一下,告訴我們又發了一個資料,那該如何做呢?
RxJava中給我們提供了一個操作符: doOnNext() ,這個操作符允許我們在每次輸出一個元素之前做一些其他的事情,比如提示啊儲存啊之類的操作。
具體用法很簡單,如下圖所示,這個程式碼也就是上面flatmap案例的完整程式碼:
Observable.from(urls)
.flatMap(new Func1<String, Observable<String>>() {
@Override
public Observable<String> call(String s) {
return Observable.just(s);
}
})
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String s) {
return GetBitmapForURL.getBitmap(s);
}
})
.subscribeOn(Schedulers.io()) // 指定subscribe()發生在IO執行緒
.observeOn(AndroidSchedulers.mainThread()) // 指定後面所發生的回撥發生在主執行緒
.doOnNext(new Action1<Bitmap>() { //每執行一次所要執行的操作
@Override
public void call(Bitmap bitmap) {
Toast.makeText(OtherActivity.this, "圖片增加", Toast.LENGTH_SHORT).show();
}
})
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
//將獲取到的Bitmap物件新增到集合中
list.add(bitmap);
//設定圖片
gvOther.setAdapter(new GridViewAdapter(OtherActivity.this, list));
pbOther.setVisibility(View.GONE);
}
});
}
複製程式碼
來看執行的動態圖:
可以看到,在每張圖片的載入過程中都有彈窗提示圖片增加,這就是doOnNext操作符的作用。
##結語
好了,今天的全部內容都講解完畢了。大部分都是用法,而這些用法與基礎用法相比較起來都或多或少複雜了一點,所以我就將它稱為中級運用。
跟前面的基礎一樣,用法講完了就需要了解其原理了。所以後面的文章將會講解一下 變換 的原理,仍然是通過圖文的形式輕輕鬆鬆地去學。
而每次寫文章過程中,都能發現自己學習過程中的理解不當或錯誤的地方,現在分享出來。但是肯定還會有不對的地方,所以希望大家如果有不同意見給予指正或與我交流,謝謝!
###參考資料
###專案原始碼
IamXiaRui-Github-FirstRxJavaDemo
個人部落格:www.iamxiarui.com