大話RxJava:三、RxJava的中級使用方法

iamxiarui_發表於2018-07-02

##寫在前面

前面兩篇文章中介紹幾乎全部都是基礎,而且如果前面兩篇吃透了的話,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傳送的每一項資料都應用一個函式,並在函式中執行變換操作。

如果還是不明白的話,那就畫圖去理解,這個圖也是官方的圖,只不過我重新畫了一下:

map流程圖

從圖中可以看到,這是一對一的轉換,就是一個單獨的資料轉成另一個單獨的資料,這一點需要跟後面的 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

在上面的程式碼中,出現了這兩個類: Func1Action1 ,這是什麼意思呢?

####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() 以實現 不完整定義的回撥

也就是說,這種回撥只呼叫 onNextonError 兩個方法,並不是完整的回撥(完整的是回撥三個方法)。

而對於這種不完整的回撥,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發射的資料,最後將合併後的結果當做它自己的資料序列發射。

好吧,定義總是很迷糊。沒關係,現在嘗試用圖的形式來說明:

FlatMap流程圖

簡單來說就是分別將一組資料中的每個資料進行轉換,轉換後再把轉換後的資料合併到一條序列上進行傳送。

不過需要注意的是,轉換後的每個資料本身其實也是一個可以傳送資料的 Observable ,所以將上面圖簡化一下就是如下圖所示:

FlatMap流程簡圖

從簡圖可以看出,與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日誌:

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 ,現在來看日誌:

Log3

從日誌明顯可以看出,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:

Log4

大家看到這個日誌肯定會有疑問,我當時也非常有疑問,為什麼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);
		}
	});
}
複製程式碼

來看執行的動態圖:

載入多張圖片2

可以看到,在每張圖片的載入過程中都有彈窗提示圖片增加,這就是doOnNext操作符的作用。

##結語

好了,今天的全部內容都講解完畢了。大部分都是用法,而這些用法與基礎用法相比較起來都或多或少複雜了一點,所以我就將它稱為中級運用。

跟前面的基礎一樣,用法講完了就需要了解其原理了。所以後面的文章將會講解一下 變換 的原理,仍然是通過圖文的形式輕輕鬆鬆地去學。

而每次寫文章過程中,都能發現自己學習過程中的理解不當或錯誤的地方,現在分享出來。但是肯定還會有不對的地方,所以希望大家如果有不同意見給予指正或與我交流,謝謝!

大話RxJava:一、初識RxJava與基本運用

大話RxJava:二、輕鬆學原始碼之基礎篇

###參考資料

給 Android 開發者的 RxJava 詳解

RxJava文件和教程

深入淺出RxJava(二:操作符)

###專案原始碼

IamXiaRui-Github-FirstRxJavaDemo


個人部落格:www.iamxiarui.com

原文連結:www.iamxiarui.com/?p=773

相關文章