RxJava從今以后你就會了

RxJava技術(shù)分享


京金所—時(shí)光

2016.9.22

這里我拿出來給 Android 開發(fā)者的 RxJava 詳解中的例子

假設(shè)有這樣一個(gè)需求:界面上有一個(gè)自定義的視圖 imageCollectorView ,它的作用是顯示多張圖片,并能使用 addImage(Bitmap) 方法來任意增加顯示的圖片。現(xiàn)在需要程序?qū)⒁粋€(gè)給出的目錄數(shù)組 File[] folders 中每個(gè)目錄下的 png 圖片都加載出來并顯示在 imageCollectorView 中。需要注意的是,由于讀取圖片的這一過程較為耗時(shí),需要放在后臺執(zhí)行,而圖片的顯示則必須在 UI 線程執(zhí)行。常用的實(shí)現(xiàn)方式有多種,我這里貼出其中一種:

`

//開啟一條子線程
new Thread() {
@Override
public void run() {
    super.run();
    //遍歷給出的目錄數(shù)組,獲取每一個(gè)目錄里面的文件的數(shù)組
    for (File folder : folders) {
        File[] files = folder.listFiles();
        //遍歷這個(gè)文件數(shù)組,篩選出來png圖片
        for (File file : files) {
            if (file.getName().endsWith(".png")) {
                //將路徑轉(zhuǎn)換成Bitmap圖片
                final Bitmap bitmap = getBitmapFromFile(file);
                //切換到主線程更新UI。
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        imageCollectorView.addImage(bitmap);
                    }
                });
            }
        }
    }
}

}.start();

`

而如果使用 RxJava ,實(shí)現(xiàn)方式是這樣的:

`
//將文件夾目錄創(chuàng)建成一個(gè)可觀測的序列

Observable.from(folders)

//遍歷目錄數(shù)組中的每一個(gè)文件對象,并將這每一個(gè)文件對象轉(zhuǎn)換成可觀測序列
.flatMap(new Func1<File, Observable<File>>() {
    @Override
    public Observable<File> call(File file) {
        return Observable.from(file.listFiles());
    }
})
//對這每一個(gè)文件對象進(jìn)行過濾,選出png圖片
.filter(new Func1<File, Boolean>() {
    @Override
    public Boolean call(File file) {
        return file.getName().endsWith(".png");
    }
})
//將這每一個(gè)png文件都轉(zhuǎn)成Bitmap傳遞
.map(new Func1<File, Bitmap>() {
    @Override
    public Bitmap call(File file) {
        return getBitmapFromFile(file);
    }
})
//耗時(shí)操作運(yùn)行在子線程
.subscribeOn(Schedulers.io())
//修改UI的操作放在主線程
.observeOn(AndroidSchedulers.mainThread())
//訂閱,展示
.subscribe(new Action1<Bitmap>() {
    @Override
    public void call(Bitmap bitmap) {
        imageCollectorView.addImage(bitmap);
    }
});

`

今天主要從以下幾個(gè)方面來著重介紹RxJava

  • RxJava的重要概念
  • RxJava的操作符
  • RxJava的應(yīng)用場景

一:RxJava的重要概念

1.1 什么是RxJava?

RxJava 在 GitHub 主頁上的自我介紹是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個(gè)在 Java VM 上使用可觀測的序列來組成異步的、基于事件的程序的庫)。

到底什么是RxJava,一個(gè)詞,就是異步。兩個(gè)詞就是異步+鏈?zhǔn)讲僮鳌?/p>

1.2 RxJava難嗎?

RxJava其實(shí)一點(diǎn)也不難。RxJava能做到的東西,別人也一樣可以做到。但是,如果使用RxJava,那么可以比他們邏輯更簡潔。有人說Rx太好用了,也有人說RxJava太難用了。好用是因?yàn)镽xJava使用操作符(函數(shù)式編程)極大的簡化了我們代碼編寫的邏輯,難用可能就是對操作符用的還不習(xí)慣,對命令式編程的思想有些固話。

1.3 但是很多人說學(xué)習(xí)RxJava起點(diǎn)高?。?/h3>

高在哪里。我認(rèn)為是幾個(gè)重要的思想。在我剛學(xué)習(xí)Java語言的時(shí)候,我認(rèn)識了面向?qū)ο蟮木幊趟枷?。簡單來說就是指揮對象幫我們做事。通過一個(gè)點(diǎn).即可。同樣,我認(rèn)為學(xué)習(xí)RxJava也是學(xué)習(xí)一個(gè)編程思想。就是函數(shù)式編程,和響應(yīng)式編程。我不知道這算不算是編程思想,但我知道一旦明白這兩個(gè)東東,對我們學(xué)習(xí)Rx有著意想不到的好處。

今天,今天通過介紹 Rx里面的概念(這部分我覺得才是最重要的一點(diǎn)),Rx里面的操作符,Rx里面的常用場景,還有Rx的擴(kuò)展,將讀者帶入Rx的大門,了解他,觸摸它,使用它,掌握它。

1.4 響應(yīng)式編程:

什么是響應(yīng)式編程(Reactive programming)? RxJava ==>ReactiveX Java

百度百科的定義:

響應(yīng)式編程是一種==面向數(shù)據(jù)流和變化傳播==的==編程范式==。這意味著可以在編程語言中很方便地表達(dá)靜態(tài)或動態(tài)的數(shù)據(jù)流,而相關(guān)的計(jì)算模型會自動將變化的值通過數(shù)據(jù)流進(jìn)行傳播。

例如,在命令式編程環(huán)境中(面向過程和面向?qū)ο蟮恼Z言中),a=b+c表示將表達(dá)式的結(jié)果賦給a,而之后改變b或c的值不會影響a。但在響應(yīng)式編程中,a的值會隨著b或c的更新而更新。響應(yīng)式編程最初是為了簡化交互式用戶界面的創(chuàng)建和實(shí)時(shí)系統(tǒng)動畫的繪制而提出來的一種方法,但它本質(zhì)上是一種通用的編程范式。

這里什么是編程范式呢?說白了就是一種編碼的風(fēng)格和模式。

此時(shí),我們不免會疑惑,數(shù)據(jù)我們知道,那么數(shù)據(jù)流呢?

RxJava Essentials一書中,是這么說的。

響應(yīng)式編程是一種基于異步數(shù)據(jù)流概念的編程模式。數(shù)據(jù)流就像一條河:它可以被觀測,被過濾,被操作,或者為新的消費(fèi)者與另外一條流合并為一條新的流。

響應(yīng)式編程的一個(gè)關(guān)鍵概念是事件。事件可以被等待,可以觸發(fā)過程,也可以觸發(fā)其它事件。

我相信看到這里大部分人已經(jīng)暈了。
不過我是越來越清晰,一字一句的多讀幾遍。

那些年我們錯過的響應(yīng)式編程中是這么介紹的:

響應(yīng)式編程就是與異步數(shù)據(jù)流交互的編程方式。(和上面基本一樣)

一方面,這已經(jīng)不是什么新事物了。事件總線(Event Buses)或一些典型的點(diǎn)擊事件本質(zhì)上就是一個(gè)異步事件流(asynchronous event stream),這樣你就可以觀察它的變化并使其做出一些反應(yīng)(do some side effects)。響應(yīng)式是這樣的一個(gè)思路:除了點(diǎn)擊和懸停(hover)的事件外,你可以給任何事物創(chuàng)建數(shù)據(jù)流。數(shù)據(jù)流無處不在,任何東西都可以成為一個(gè)數(shù)據(jù)流,例如變量、用戶輸入、屬性、緩存、數(shù)據(jù)結(jié)構(gòu)等等。舉個(gè)栗子,你可以把你的微博訂閱功能想象成跟點(diǎn)擊事件一樣的數(shù)據(jù)流,你可以監(jiān)聽這樣的數(shù)據(jù)流,并做出相應(yīng)的反應(yīng)。

最重要的是,你會擁有一些令人驚艷的函數(shù)去結(jié)合、創(chuàng)建和過濾任何一組數(shù)據(jù)流。 這就是”函數(shù)式編程”的魔力所在。一個(gè)數(shù)據(jù)流可以作為另一個(gè)數(shù)據(jù)流的輸入,甚至多個(gè)數(shù)據(jù)流也可以作為另一個(gè)數(shù)據(jù)流的輸入。你可以合并兩個(gè)數(shù)據(jù)流,也可以過濾一個(gè)數(shù)據(jù)流得到另一個(gè)只包含你感興趣的事件的數(shù)據(jù)流,還可以映射一個(gè)數(shù)據(jù)流的值到一個(gè)新的數(shù)據(jù)流里。

最后我說一下我的理解:

首先響應(yīng)式編程僅僅是一種編程的模式,風(fēng)格,并不具有特殊的含義和操作,響應(yīng)式編程他是用來對數(shù)據(jù)流進(jìn)行操作的,所有對數(shù)據(jù)流進(jìn)行異步操作,觀測等行為,我們都可以管這種操作行為叫做響應(yīng)式編程。那么什么又是數(shù)據(jù)流呢?

1.4 數(shù)據(jù)流的概念:

數(shù)據(jù)流是整個(gè)響應(yīng)式編程體系中的核心,要想學(xué)習(xí)響應(yīng)式編程,首先我們就需要搞明白什么是數(shù)據(jù)流。其實(shí),一個(gè)數(shù)據(jù)流是一個(gè)按時(shí)間排序的即將發(fā)生的事件(Ongoing events ordered in time)的序列。就比如我們警察抓小偷,小偷偷東西的時(shí)候,警察抓住他了嗎,小偷偷東西之前做了什么,之后做了什么,警察抓他之前做了什么,抓住他做了什么或者沒抓住又該怎么去做。這一系列事件組成的序列就是數(shù)據(jù)流。數(shù)據(jù)流就像一條河:它可以被觀測,被過濾,被操作,或者為新的消費(fèi)者與另外一條流合并為一條新的流。

也就是說我們的響應(yīng)式編程,就是對這一系列異步事件進(jìn)行處理的一種編程模式。其實(shí),你會發(fā)現(xiàn),看了一大圈,又回到了第一句話,但是我相信,你現(xiàn)在應(yīng)該不會在迷糊了。

1.5 函數(shù)式編程:

其實(shí)我一直搞不明白函數(shù)式編程和響應(yīng)式編程。但是現(xiàn)在我明白了,而且他倆根本就不一樣。

1.5.1 什么是函數(shù)式編程:

應(yīng)該有至少三種編程思想:

  • 命令式編程 Imperative programming
  • 邏輯式編程 Object-oriented Programming
  • 函數(shù)式編程 Functional Programming

命令式編程關(guān)心解決問題的步驟,面向?qū)ο缶幊淌且彩且环N命令式編程,面向過程的C語言必然也是了。
而函數(shù)式編程關(guān)心數(shù)據(jù)的映射,即一種東西和另一種東西之間的對應(yīng)關(guān)系。它的主要思想是把運(yùn)算過程盡量寫成一系列嵌套的函數(shù)調(diào)用。

比如這種運(yùn)算:(1 + 2) * 3 - 4 ;
之前我們寫是這樣:

```
int a = 1 + 2; int b = a * 3; int result = b - 4;
System.out.println(b);
```

然而用函數(shù)式編程思想寫就是這樣:

```
System.out.print(subtract( multiply(add(1,2),3),4) );
```

全部變?yōu)榱撕瘮?shù)調(diào)用,這樣看起來也簡潔、見名之意。(示例來自阮一峰博客)

看到這,想一下,使用RxJava時(shí)是不是全部調(diào)用各種操作符進(jìn)行處理,這就是對事件流進(jìn)行運(yùn)算啊,全部調(diào)用函數(shù)進(jìn)行處理。

在這里我們來對比一下函數(shù)式編程和響應(yīng)式編程。函數(shù)式編程是對數(shù)據(jù)關(guān)系進(jìn)行==映射==,比如說y=ax;這個(gè)一元函數(shù)。函數(shù)式編程我們只需要定義處來這個(gè)映射關(guān)系即參數(shù)a的算法,我們就可以得到y(tǒng)。而響應(yīng)式編程的重點(diǎn)則不在于這個(gè)參數(shù)a,而是當(dāng)我的x發(fā)生改變的時(shí)候(就是我們注冊的x的狀態(tài)改變)的時(shí)候,我要做出一個(gè)什么樣反應(yīng)。這和我大學(xué)學(xué)到的激勵和響應(yīng)是差不多的。我給你一個(gè)什么樣的激勵,你能反饋我一個(gè)什么樣的響應(yīng)

1.6 觀察者模式

在今天,觀察者模式是出現(xiàn)的最常用的軟件設(shè)計(jì)模式之一。它基于subject這個(gè)概念。subject是一種特殊對象,當(dāng)它改變時(shí),那些由它保存的一系列對象將會得到通知。而這一系列對象被稱作Observers(觀察者),它們會對外暴漏了一個(gè)通知方法,當(dāng)subject狀態(tài)發(fā)生變化時(shí)會調(diào)用的這個(gè)方法。

就比如說,當(dāng)A的某狀態(tài)發(fā)生改變或者接收到某種激勵的時(shí)候,B需要對這種激勵做出一個(gè)反饋,那么我們只需要讓A持有B的引用,同時(shí)去調(diào)用B的方法即可(我們最終的目的是將發(fā)生改變的狀態(tài)傳遞出去,通過方法傳遞,我們的B需要這個(gè)狀態(tài))。但是通常,我們的A不知道自己什么時(shí)候能獲取到這個(gè)激勵(什么時(shí)候狀態(tài)會發(fā)生改變),這個(gè)時(shí)候我們的B呢就可以通過注冊,來處理這個(gè)響應(yīng)。同時(shí),我們也可以不用拿到B的引用,我們可以通過一個(gè)中間接口,將發(fā)生改變的狀態(tài)(或者是B需要的狀態(tài))通過接口傳遞過去,同時(shí)B只需要拿到這個(gè)接口的實(shí)例對象即可,這就是B的注冊過程。也是最簡單的回調(diào)。而回調(diào)也是觀察者模式的一種簡化形式,1-1的關(guān)系,當(dāng)然我們平時(shí)不僅setOnClickListener()也addOnXxxListener(),這種一對多的關(guān)系就是觀察者模式。

1.7 擴(kuò)展的觀察者模式

與傳統(tǒng)觀察者模式不同, RxJava的事件回調(diào)方法除了普通事件 onNext() (相當(dāng)于 onClick() / onEvent())之外,還定義了兩個(gè)特殊的事件:onCompleted() 和 onError()。

1.8 幾個(gè)常見的類

在RxJava的世界里,我們有四種角色:

Observable

Observer

Subscriber

Subjects

Observables和Subjects是兩個(gè)“生產(chǎn)”實(shí)體,Observers和Subscribers是兩個(gè)“消費(fèi)”實(shí)體

```
public class Observable<T> {
    final OnSubscribe<T> onSubscribe;
    }
public interface Observer<T>{
    onComplete();onError;onNext();
    }
public abstract class Subscriber<T> implements Observer<T>, Subscription 
public abstract class Subject<T, R> extends Observable<R> implements Observer<T> 
*/
```

總覽:

Creating Observables
Operators that originate new Observables.

  • Create — create an Observable ++from scratch(從頭)++ by calling observer methods ++programmatically(以編程的方式)++
  • ++Defer(延遲創(chuàng)建)++ — do not create the Observable until the observer subscribes, and create a fresh Observable for each observer
  • Empty/Never/Throw — create Observables that have very ++precise(精確的)++ and limited behavior
  • From — ++convert(轉(zhuǎn)換)++ some other object or data structure into an Observable
  • ++Interval(間隔)++ — create an Observable that ++emits(發(fā)射)++ a sequence of integers spaced by a particular time interval
  • Just — convert an object or a set of objects into an Observable that emits that or those objects
  • Range — create an Observable that emits a range of sequential integers
  • Repeat — create an Observable that emits a particular item or sequence of items repeatedly
  • Start — create an Observable that emits the return value of a function
  • Timer — create an Observable that emits a single item after a given delay

一:Create的使用

可以通過create操作去創(chuàng)建一個(gè)被觀察者Observable。Create是最基本的創(chuàng)建Observable的操作符。
image

創(chuàng)建一個(gè)Observable(被觀察者)最重要的就是要和合適的時(shí)機(jī)調(diào)用Subscriber(觀察者)的onNext/onComplete/onError方法。onNext就是發(fā)射處理好的數(shù)據(jù)給Subscriber; onComplete用來告訴Subscriber所有的數(shù)據(jù)都已發(fā)射完畢;onError是在發(fā)生錯誤的時(shí)候發(fā)射一個(gè)Throwable對象給Subscriber。需要注意的一點(diǎn)就是Observable必須調(diào)用所有的Subscriber的onComplete方法并且只能調(diào)用一次,出錯的時(shí)候調(diào)用onError方法也是一樣的,并且一旦調(diào)用后就不能調(diào)用Subscriber的任何其他方法了。

介紹完畢。

我再詳細(xì)介紹一下。

我們可以看到Observable.create(這里經(jīng)常會傳入一個(gè)OnSubscribe接口);

OnSubscribe接口又是什么呢?它繼承自Action1<Subscriber<? super T>這個(gè)接口,他的call(<Subscriber<? super T> subscriber)方法里只有一個(gè)參數(shù),它傳入了一個(gè)觀察者?。?!也就是說我們在使用create的時(shí)候,我們將觀察者包裹在了被觀察者內(nèi)部。并且在被觀察這訂閱觀察者的時(shí)候,我們通過一個(gè)hook對象將觀察者Subscriber的對象傳入到OnSubscribe.call(傳到這里面)。這樣就實(shí)現(xiàn)了由被觀察者在訂閱的時(shí)候調(diào)用觀察者的方法。并且在事件完成的時(shí)候自動取消訂閱。

    Observable<String> observable = Observable.create(new Observable.OnSubscribe<String>() {
        @Override
        public void call(Subscriber<? super String> subscriber) {
            //將依次發(fā)射3次onNext ,之后發(fā)射onComplete(發(fā)射完這個(gè)將不會再有輸出,同時(shí)自動取消注冊)
            subscriber.onNext("Hello第一次發(fā)射");
            subscriber.onNext("World第二次發(fā)射");
            subscriber.onNext("Over 第三次發(fā)射");
            subscriber.onCompleted();
            subscriber.onError(new Throwable());
        }
    })

2.Just、From的使用

為了簡化創(chuàng)建操作,我們平時(shí)最常使用的一般是這兩個(gè)操作符。just(T...):from(T[])。

Just操作符將某個(gè)對象轉(zhuǎn)化為Observable對象,并且將其發(fā)射出去,可以使一個(gè)數(shù)字、一個(gè)字符串、數(shù)組、Iterate對象等。其實(shí)當(dāng)just傳入一個(gè)參數(shù)的時(shí)候,就是調(diào)用create()方法,傳入多參數(shù)的時(shí)候,就是調(diào)用from()方法。從而做到將參數(shù)依次發(fā)射出去

From操作符用來將某個(gè)對象轉(zhuǎn)化為Observable對象,并且依次將其內(nèi)容發(fā)射出去。這個(gè)類似于just,但是just會將這個(gè)對象整個(gè)發(fā)射出去。比如說一個(gè)含有10個(gè)數(shù)字的數(shù)組,使用from就會發(fā)射10次,每次發(fā)射一個(gè)數(shù)字,而使用just會發(fā)射一次來將整個(gè)的數(shù)組發(fā)射出去。


image

3.Defer

Defer操作符只有當(dāng)有Subscriber來訂閱的時(shí)候才會創(chuàng)建一個(gè)新的Observable對象,也就是說每次訂閱都會得到一個(gè)剛創(chuàng)建的最新的Observable對象,這可以確保Observable對象里的數(shù)據(jù)是最新的


image

4.Interval

Interval所創(chuàng)建的Observable對象會從0開始,每隔固定的時(shí)間發(fā)射一個(gè)數(shù)字。需要注意的是這個(gè)對象是運(yùn)行在computation Scheduler,基本上和時(shí)間有關(guān)系的都是運(yùn)行在computation Scheduler 中,所以如果需要在view中顯示結(jié)果,要在主線程中訂閱。


image

5.Range

這個(gè)應(yīng)該好理解一點(diǎn),看圖依次發(fā)射區(qū)間(n,n+m-1)里面的數(shù)字,左閉右開


image

6.Repeat、Timer

Repeat會將一個(gè)Observable對象重復(fù)發(fā)射,我們可以指定其發(fā)射的次數(shù)


image

Timer會在指定時(shí)間后發(fā)射一個(gè)數(shù)字0,注意其也是運(yùn)行在computation Scheduler


image

總覽:

Transforming Observables
Operators that transform items that are emitted by an Observable.

  • Buffer — ++periodically(定期的)++ gather items from an Observable into bundles and emit these bundles rather than emitting the items ++one at a time(一次一個(gè))++
  • FlatMap — transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable
  • GroupBy — divide an Observable into a set of Observables that each emit a different group of items from the original Observable, organized by key
  • Map— transform the items emitted by an Observable by applying a function to each item
  • Scan — apply a function to each item emitted by an Observable, sequentially, and emit each successive value
  • Window — periodically subdivide items from an Observable into Observable windows and emit these windows rather than emitting the items one at a time

RxJava 提供了對事件序列進(jìn)行變換的支持,這是它的核心功能之一,也是大多數(shù)人說『RxJava 真是太好用了』的最大原因。所謂變換,就是將事件序列中的對象或整個(gè)序列進(jìn)行加工處理,轉(zhuǎn)換成不同的事件或事件序列。

1.Buffer

Buffer操作符所要做的事情就是將數(shù)據(jù)按照規(guī)定的大小、或者時(shí)間做一下緩存,然后將緩存的數(shù)據(jù)作為一個(gè)集合發(fā)射出去。

image

在上圖中,按照規(guī)定的大小(count = 3)來收集數(shù)據(jù),之后將他們整合成一個(gè)集合發(fā)射出去。
image

在上圖中,加入了一個(gè)skip參數(shù)用來指定每次發(fā)射一個(gè)集合需要跳過幾個(gè)數(shù)據(jù),圖中指定(count = 2,skip = 3),就會每3個(gè)數(shù)據(jù)發(fā)射一個(gè)包含兩個(gè)數(shù)據(jù)的集合,如果count==skip的話,我們就會發(fā)現(xiàn)其等效于第一種情況了。這里還是很好理解的。

2.flatMap & concatMap

Flatmap是一個(gè)用處特別多的操作符。它可以將數(shù)據(jù)根據(jù)你想要的規(guī)則進(jìn)行轉(zhuǎn)化后再發(fā)射出去。其原理就是將這個(gè)Observable轉(zhuǎn)化為多個(gè)以原Observable發(fā)射的數(shù)據(jù)作為源數(shù)據(jù)的Observable,然后再將這多個(gè)Observable發(fā)射的數(shù)據(jù)整合發(fā)射出來,需要注意的是最后的順序可能會交錯地發(fā)射出來,如果對順序有嚴(yán)格的要求的話可以使用concatmap操作符。


image

如上圖,我們按照一個(gè)圓對應(yīng)兩個(gè)菱形的規(guī)則,將一個(gè)Observable<圓>轉(zhuǎn)換成兩個(gè)Observable<菱形>。并將兩個(gè)Observable<菱形>發(fā)射出去。

3.Map

Map操作符的功能類似于FlatMap,不同之處在于它對數(shù)據(jù)的轉(zhuǎn)化是直接進(jìn)行的,而FlatMap需要通過一些中間的Observables來進(jìn)行。


image

4.GroupBy

GroupBy操作符將原始Observable發(fā)射的數(shù)據(jù)按照key來拆分成一些小的Observable,然后這些小的Observable分別發(fā)射其所包含的的數(shù)據(jù),類似于sql里面的groupBy。在使用中,我們需要提供一個(gè)生成key的規(guī)則,所有key相同的數(shù)據(jù)會包含在同一個(gè)小的Observable種。


image

5.Scan

Scan操作符對一個(gè)序列的數(shù)據(jù)應(yīng)用一個(gè)函數(shù),并將這個(gè)函數(shù)的結(jié)果發(fā)射出去作為下個(gè)數(shù)據(jù)應(yīng)用這個(gè)函數(shù)時(shí)候的第一個(gè)參數(shù)使用,有點(diǎn)類似于遞歸操作


image

總覽:過濾

Filtering Observables
Operators that selectively emit items from a source Observable.

  • Debounce — only emit an item from an Observable if a particular timespan has passed without it emitting another item
  • Distinct — suppress duplicate items emitted by an Observable
  • ElementAt — emit only item n emitted by an Observable
  • Filter — emit only those items from an Observable that pass a predicate test
  • First — emit only the first item, or the first item that meets a condition, from an Observable
  • IgnoreElements — do not emit any items from an Observable but mirror its termination notification
  • Last — emit only the last item emitted by an Observable
  • Sample — emit the most recent item emitted by an Observable within periodic time intervals
  • Skip — suppress the first n items emitted by an Observable
  • SkipLast — suppress the last n items emitted by an Observable
  • Take — emit only the first n items emitted by an Observable
  • TakeLast — emit only the last n items emitted by an Observable

1.Filter

Filter只會返回滿足過濾條件的數(shù)據(jù).比如說,我們想讓數(shù)據(jù){a1,a2,b1,v1,c1}中以v開頭的數(shù)據(jù)排除掉,或者我們經(jīng)常用到的AppInfo中數(shù)據(jù)為null的過濾掉,我們可以使用filter輕松實(shí)現(xiàn)。

image

如上圖,我們應(yīng)該filter(東西 == 圓)

2.take/takelast

當(dāng)我們不需要整個(gè)序列時(shí),而是只想取開頭或結(jié)尾的幾個(gè)元素,我們可以用take()或takeLast()。


image
```
//太簡單了,我就不寫demo了,take(3)從前面取3個(gè),takeLast(3)從后面取3個(gè)
 Observable.from(apps).take(3)
```

3.Distinct 有且僅有一次

我們可以對我們的序列使用distinct()函數(shù)去掉重復(fù)的。就像takeLast()一樣,distinct()作用于一個(gè)完整的序列,然后得到重復(fù)的過濾項(xiàng),它需要記錄每一個(gè)發(fā)射的值。如果你在處理一大堆序列或者大的數(shù)據(jù)記得關(guān)注內(nèi)存使用情況。
Distinct操作符的用處就是用來去重,非常好理解。如下圖所示,所有重復(fù)的數(shù)據(jù)都會被過濾掉。還有一個(gè)操作符distinctUntilChanged,是用來過濾掉連續(xù)的重復(fù)數(shù)據(jù)。


image

image

4.First and last

first()方法和last()方法很容易弄明白。它們從Observable中只發(fā)射第一個(gè)元素或者最后一個(gè)元素。連圖我都不上了。
firstOrDefault()和lastOrDefault().這兩個(gè)函數(shù)當(dāng)可觀測序列完成時(shí)不再發(fā)射任何值時(shí)用得上。在這種場景下,如果Observable不再發(fā)射任何值時(shí)我們可以指定發(fā)射一個(gè)默認(rèn)的值

5.Skip and SkipLast

skip()和skipLast()函數(shù)與take()和takeLast()相對應(yīng)。它們用整數(shù)N作參數(shù),從本質(zhì)上來說,它們不讓Observable發(fā)射前N個(gè)或者后N個(gè)值。如果我們知道一個(gè)序列以沒有太多用的“可控”元素開頭或結(jié)尾時(shí)我們可以使用它。


image
image

6.ElementAt

如果我們只想要可觀測序列發(fā)射的第五個(gè)元素該怎么辦?elementAt()函數(shù)僅從一個(gè)序列中發(fā)射第n個(gè)元素然后就完成了。+

如果我們想查找第五個(gè)元素但是可觀測序列只有三個(gè)元素可供發(fā)射時(shí)該怎么辦?我們可以使用elementAtOrDefault()。下圖展示了如何通過使用elementAt(2)從一個(gè)序列中選擇第三個(gè)元素以及如何創(chuàng)建一個(gè)只發(fā)射指定元素的新的Observable。


image
image

7.Sample

在Observable后面加一個(gè)sample(),我們將創(chuàng)建一個(gè)新的可觀測序列,它將在一個(gè)指定的時(shí)間間隔里由Observable發(fā)射最近一次的數(shù)值:
如果我們想讓它定時(shí)發(fā)射第一個(gè)元素而不是最近的一個(gè)元素,我們可以使用throttleFirst()。


image

image
image

8.Debounce

debounce()函數(shù)過濾掉由Observable發(fā)射的速率過快的數(shù)據(jù);如果在一個(gè)指定的時(shí)間間隔過去了仍舊沒有發(fā)射一個(gè),那么它將發(fā)射最后的那個(gè)。
下圖展示了多久從Observable發(fā)射一次新的數(shù)據(jù),debounce()函數(shù)開啟一個(gè)內(nèi)部定時(shí)器,如果在這個(gè)時(shí)間間隔內(nèi)沒有新的數(shù)據(jù)發(fā)射,則新的Observable發(fā)射出最后一個(gè)數(shù)據(jù):


image

Combining Observables

1.Zip

Zip操作符將多個(gè)Observable發(fā)射的數(shù)據(jù)按順序組合起來,每個(gè)數(shù)據(jù)只能組合一次,而且都是有序的。最終組合的數(shù)據(jù)的數(shù)量由發(fā)射數(shù)據(jù)最少的Observable來決定。


image

2.Merege

Merge操作符將多個(gè)Observable發(fā)射的數(shù)據(jù)整合起來發(fā)射,就如同是一個(gè)Observable發(fā)射的數(shù)據(jù)一樣。但是其發(fā)射的數(shù)據(jù)有可能是交錯的,如果想要沒有交錯,可以使用concat操作符。當(dāng)某一個(gè)Observable發(fā)出onError的時(shí)候,merge的過程會被停止并將錯誤分發(fā)給Subscriber,如果不想讓錯誤終止merge的過程,可以使用MeregeDelayError操作符,會將錯誤在merge結(jié)束后再分發(fā)。


image

1.使用Scheduler進(jìn)行線程的切換

在不指定線程的情況下, RxJava 遵循的是線程不變的原則,即:在哪個(gè)線程調(diào)用訂閱方法 subscribe(),就在哪個(gè)線程生產(chǎn)事件;在哪個(gè)線程生產(chǎn)事件,就在哪個(gè)線程消費(fèi)事件。如果需要切換線程,就需要用到 Scheduler (調(diào)度器)。

幾個(gè)常用的Api:
  • Schedulers.immediate(): 直接在當(dāng)前線程運(yùn)行,相當(dāng)于不指定線程。這是默認(rèn)的 Scheduler。
  • Schedulers.newThread(): 總是啟用新線程,并在新線程執(zhí)行操作。
  • Schedulers.io(): I/O 操作(讀寫文件、讀寫數(shù)據(jù)庫、網(wǎng)絡(luò)信息交互等)所使用的 Scheduler。行為模式和 newThread() 差不多,區(qū)別在于 io() 的內(nèi)部實(shí)現(xiàn)是是用一個(gè)無數(shù)量上限的線程池,可以重用空閑的線程,因此多數(shù)情況下 io() 比 newThread() 更有效率。不要把計(jì)算工作放在 io() 中,可以避免創(chuàng)建不必要的線程。
  • Schedulers.computation(): 計(jì)算所使用的 Scheduler。這個(gè)計(jì)算指的是 CPU 密集型計(jì)算,即不會被 I/O 等操作限制性能的操作,例如圖形的計(jì)算。這個(gè) Scheduler 使用的固定的線程池,大小為 CPU 核數(shù)。不要把 I/O 操作放在 computation() 中,否則 I/O 操作的等待時(shí)間會浪費(fèi) CPU。
  • Android 還有一個(gè)專用的 AndroidSchedulers.mainThread(),它指定的操作將在 Android 主線程運(yùn)行。

有了這幾個(gè) Scheduler ,就可以使用 subscribeOn()observeOn() 兩個(gè)方法來對線程進(jìn)行控制了。

  • subscribeOn(): 指定 subscribe() 所發(fā)生的線程,即 Observable.OnSubscribe 被激活時(shí)所處的線程?;蛘呓凶鍪录a(chǎn)生的線程。

  • observeOn(): 指定 Subscriber 所運(yùn)行在的線程?;蛘呓凶鍪录M(fèi)的線程。

精簡代碼:(教簡單就不放全部代碼了)

```
 mSubscription = getObservable()//創(chuàng)建一個(gè)被觀察者
   .subscribeOn(Schedulers.io())//指定訂閱事件發(fā)生的線程
   .doOnSubscribe(new Action0() {//事件發(fā)生之前做什么事
//onStart() 也可以用作流程開始前的初始化。但是 onStart() 由于在 subscribe() 發(fā)生時(shí)就被調(diào)用了,因此不能指定線程,而是只能執(zhí)行在 subscribe() 被調(diào)用時(shí)的線程。
//doOnSubscribe同樣是在 subscribe() 調(diào)用后而且在事件發(fā)送前執(zhí)行,但區(qū)別在于它可以指定線程。
//默認(rèn)情況下, doOnSubscribe() 執(zhí)行在 subscribe() 發(fā)生的線程;而如果在 doOnSubscribe() 之后有 subscribeOn() 的話,它將執(zhí)行在離它最近的 subscribeOn() 所指定的線程。
                            @Override
                            public void call() {
                                mProgressBar.setVisibility(View.VISIBLE);
                                _log("按鈕被點(diǎn)擊了");
                            }
                        })
                        .observeOn(AndroidSchedulers.mainThread())//指定事件消費(fèi)在那個(gè)線程里面
                        .subscribe(getSubscriber());//訂閱觀察者
```

### 2.使用Buffer收集數(shù)據(jù)流后在發(fā)射

Buffer顧名思義,就相當(dāng)一個(gè)緩沖區(qū),我們可以指定一個(gè)時(shí)間或者數(shù)量來收集這些數(shù)據(jù),滿足了這個(gè)事件或者數(shù)量之后,在統(tǒng)一發(fā)射出去。在這里我們舉個(gè)例子,來學(xué)習(xí)Buffer,同時(shí)更深入的理解一個(gè)更重要的操作符map

//觀察在兩秒的時(shí)間內(nèi),按鈕被點(diǎn)擊的次數(shù)
 public Subscription getClickNumInTime(){
 //這里需要引入compile 'com.jakewharton.rxbinding:rxbinding:0.4.0'依賴,這樣我們可以觀測操作這些控件的數(shù)據(jù)流
    return RxView
    //將點(diǎn)擊事件包裝成數(shù)據(jù)(創(chuàng)建一個(gè)可觀測的序列)流發(fā)射出去
           .clicks(tapButton)
           //這里我們可以很清楚,很明了的看到,我們將點(diǎn)擊事件map成了Integer類型的數(shù)據(jù)
           .map(new Func1<Void, Integer>() {
               @Override
               public Integer call(Void aVoid) {
                   _log("得到了一個(gè)點(diǎn)擊");
                   return 1;
               }
           })
           //通過Buffer指定2秒鐘發(fā)射一次收集的數(shù)據(jù)
            .buffer(2, TimeUnit.SECONDS)
            //Buffer指定的線程是computation線程,我們修改UI要改變線程
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<List<Integer>>() {
                @Override
                public void onCompleted() {
                    _log("你永遠(yuǎn)不會走到這里,我不信這句話能打印處來");
                }

                @Override
                public void onError(Throwable e) {
                    _log("出錯了"+e.getMessage());
                }
//一旦訂閱之后,除非取消訂閱,或者徹底殺死進(jìn)程,否則會一直走下去,造成內(nèi)存泄露
                @Override
                public void onNext(List<Integer> integers) {
                    if(integers.size() > 0)
                    _log("你在兩秒內(nèi)一共點(diǎn)擊了:"+integers.size()+"次");
                }
            });
}

### 3.使用Debounce做TextSearch
比如說當(dāng)我們輸入的內(nèi)容發(fā)生改變的時(shí)候,我們需要實(shí)時(shí)的去異步請求網(wǎng)絡(luò)/數(shù)據(jù)庫來提示用戶某樣?xùn)|西也需要改變。就比如,當(dāng)我輸入金額的時(shí)候,沒當(dāng)我EditText內(nèi)容發(fā)生改變的時(shí)候我去請求服務(wù)器,將得到的京金幣展示在桌面上。有些時(shí)候,用戶連續(xù)幾個(gè)數(shù)字輸入的過快,沒必要去請求網(wǎng)絡(luò),這個(gè)時(shí)候,我們可以使用Debounce來做限流操作,一定時(shí)間內(nèi),我們只要最后一次得到的數(shù)據(jù)。

    ```
    mSubscription = RxTextView
    //將EditText的內(nèi)容改變事件轉(zhuǎn)換成一組可觀測的序列(數(shù)據(jù)流)
                .textChangeEvents(inputTxtDebounce)
                //開啟限流操作,在400毫秒內(nèi),我們只要最后一次改變的結(jié)果
                .debounce(400, TimeUnit.MILLISECONDS)
                //過濾掉空字符串,只有滿足條件的能留下
                .filter(new Func1<TextViewTextChangeEvent, Boolean>() {
                    @Override
                    public Boolean call(TextViewTextChangeEvent textViewTextChangeEvent) {
                        return !inputTxtDebounce.getText().toString().trim().isEmpty();
                    }
                })
                //和Buffer一樣,生產(chǎn)事件運(yùn)行在computation線程
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(getSubscriber());
    ```
很多時(shí)候我們都需要給訂閱事件來一個(gè)返回值,來方便我們在離開頁面的時(shí)候取消訂閱。

### 4.Retrofit+RxJava
目前最火的網(wǎng)絡(luò)請求就是Retrofit了,而目前最火的框架,二者也有一合之地,而二者的無縫結(jié)合,完全使我們的網(wǎng)絡(luò)請求隨心所欲。
Retrofit我就不多做介紹了。

    ```
    //先來看一個(gè)最簡單的。
    //首先我們用一個(gè)裝訂閱set集合,將這一個(gè)網(wǎng)絡(luò)請求的訂閱裝進(jìn)來
    mSubscriptions.add(
    //通過Retrofit創(chuàng)建網(wǎng)絡(luò)請求
    mGithubApi
        .contributors(_username.getText().toString(),_repo.getText().toString())
        //指定網(wǎng)絡(luò)請求(生產(chǎn)事件)運(yùn)行的線程
        .subscribeOn(Schedulers.io())
        //指定響應(yīng)的線程(消費(fèi)事件)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<List<Contributor>>() {
             @Override
             public void onCompleted() {
                 _adapter.add("完畢");
              }

              @Override
              public void onError(Throwable e) {
                _adapter.add("加載失敗"+e.getMessage());
              }

            @Override
             public void onNext(List<Contributor> contributors) {
             //網(wǎng)絡(luò)請求得到了一組集合,我們對這一組集合遍歷拿到我們想要的數(shù)據(jù)
                for (int i = 0; i < contributors.size(); i++) {
                    _adapter.add(format("%s 為 %s 庫做出了 %d 個(gè)貢獻(xiàn)",contributors.get(i).login,_repo.getText().toString(),contributors.get(i).contributions));
            }
        }
      })
    );
    ```
如果!我在拿到List<Contributor>響應(yīng)數(shù)據(jù)的時(shí)候,我要根據(jù)里面的作者,去搜索每一個(gè)作者的詳細(xì)信息,怎么辦?我想一想就覺得頭疼,先把這些作者收集起來,遍歷,再去挨個(gè)請求,再將得到的數(shù)據(jù)和剛才的數(shù)據(jù)整合起來,顯示到桌面?
不,RxJava的優(yōu)點(diǎn)就是,你的需求越復(fù)雜,我的邏輯越簡單?。。。。。?
    ```
    mSubscriptions.add(
    //先創(chuàng)建網(wǎng)絡(luò)請求
    mGithubApi.contributors(_username.getText().toString(),_repo.getText().toString())
    //我們先將得到的數(shù)據(jù)<List<Contributor>,遍歷發(fā)射并觀測其中的每一個(gè)元素
    .flatMap(new Func1<List<Contributor>, Observable<Contributor>>() {
        @Override
        public Observable<Contributor> call(List<Contributor> contributors) {
            return Observable.from(contributors);
        }
    })
    //這里,重點(diǎn)來了,先說一下Pair,他就是一個(gè)包含了兩個(gè)對象的容器。在這里我們用它來裝User和Contributor
    .flatMap(new Func1<Contributor, Observable<Pair<User,Contributor>>>() {
        @Override
        public Observable<Pair<User,Contributor>> call(Contributor contributor) {
        //在這里,我們在根據(jù)longin參數(shù)再去請求user的詳細(xì)信息
        Observable<User> userObservable = mGithubApi.user(contributor.login)
            //過濾掉非空數(shù)據(jù)
            .filter(new Func1<User, Boolean>() {
                @Override
                public Boolean call(User user) {
                    return !isEmpty(user.name) && !isEmpty(user.email);
                }
            });
            //到了整合的時(shí)候了。我們將user的詳細(xì)信息的數(shù)據(jù)流,同時(shí)將contributor這個(gè)值再次包裝成數(shù)據(jù)流發(fā)整合之后
            //怎么整合呢,這時(shí)候就要看Func2這個(gè)函數(shù)了
            return Observable.zip(userObservable, Observable.just(contributor), new Func2<User, Contributor, Pair<User, Contributor>>() {
                @Override
                public Pair<User, Contributor> call(User user, Contributor contributor) {
                //它將我們的user用戶信息和contributor整合在Pair容器中返回回去
                     return new Pair<User, Contributor>(user,contributor);
                }
            });
        }
    })
    //下面的不用多說了,我只想說,這個(gè)代碼寫的真的很精彩
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<Pair<User, Contributor>>() {
                            @Override
                            public void onCompleted() {
                                _adapter.add("完畢");
                            }

                            @Override
                            public void onError(Throwable e) {
                                _adapter.add("加載失敗");
                            }

                            @Override
                            public void onNext(Pair<User, Contributor> pair) {
                                User user = pair.first;
                                Contributor contributor = pair.second;

                                _adapter.add(format("%s(%s) 為 %s 庫做出了 %d 個(gè)貢獻(xiàn)",
                                        user.name,
                                        user.email,
                                        _repo.getText().toString()
                                        ,contributor.contributions));

                                _adapter.notifyDataSetChanged();
                            }
                        })
                );

5.使用combineLatest做登錄注冊模塊

CombineLatest操作符可以將2~9個(gè)Observable發(fā)射的數(shù)據(jù)組裝起來然后再發(fā)射出來。不過還有兩個(gè)前提:
1.所有的Observable都發(fā)射過數(shù)據(jù)。(用戶操作APP可以感覺到,只有當(dāng)我三個(gè)EditText全部輸入過內(nèi)容,才會觸發(fā)combineLatest的發(fā)射事件)
2.滿足條件1的時(shí)候任何一個(gè)Observable發(fā)射一個(gè)數(shù)據(jù),就將所有Observable最新發(fā)射的數(shù)據(jù)按照提供的函數(shù)組裝起來發(fā)射出去。(每當(dāng)我任何一個(gè)EditText發(fā)生改變的時(shí)候都會重新走一遍combineLatest的發(fā)射事件)
這里我們看著例子,來解釋這些話語


image

這個(gè)也是挺常用的,他可以將我們需要的數(shù)據(jù)流整合在一起。比如:
注冊的時(shí)候所有輸入信息(郵箱、密碼、電話號碼等)合法才點(diǎn)亮注冊按鈕。

```
//這里我們分別將3個(gè)EditText的內(nèi)容變化的事件轉(zhuǎn)換成數(shù)據(jù)流
_emailChangeObservable = RxTextView.textChanges(_email).skip(1);
_passwordChangeObservable = RxTextView.textChanges(_password).skip(1);
_numberChangeObservable = RxTextView.textChanges(_number).skip(1);

private void _combineLatestEvents() {
//我們將三個(gè)數(shù)據(jù)流整合起來,并加入整合的規(guī)則(這里可以將規(guī)則看成返回值Boolean,滿足規(guī)則我返回True,否則返回false)
    _subscription = Observable.combineLatest(_emailChangeObservable,
            _passwordChangeObservable, _numberChangeObservable, new Func3<CharSequence, CharSequence, CharSequence, Boolean>() {
                @Override
                public Boolean call(CharSequence email, CharSequence psw, CharSequence num) {
                    boolean emailB = "123456".equals(email.toString());
                    if(!emailB){
                        _email.setError("必須是123456");
                    }
                    boolean pswB = "654321".equals(psw.toString());
                    if(!pswB){
                        _password.setError("必須是654321");
                    }
                    boolean numB = "521".equals(num.toString());
                    if(!numB){
                        _number.setError("必須是521");
                    }
                    return emailB && pswB && numB;
                }
            })
            .subscribe(new Subscriber<Boolean>() {
                @Override
                public void onCompleted() {

                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onNext(Boolean aBoolean) {
                    if (aBoolean) {
                        _btnValidIndicator.setBackgroundColor(Color.BLUE);
                    } else {
                        _btnValidIndicator.setBackgroundColor(Color.parseColor("#888888"));
                    }
                }
            });
}

### 6.使用timer做定時(shí)操作。當(dāng)有“x秒后執(zhí)行y操作”類似的需求的時(shí)候想到使用timer
我們一般做APP會用到定時(shí)器,當(dāng)用到定時(shí)器的。比如說再按一次退出程序,我們可以這么做。我們通過Timer計(jì)時(shí),如果退出了的話會造成不到兩秒的內(nèi)存泄露,當(dāng)然也可以先取消訂閱在finish()頁面。這里使用Timer來記錄用戶點(diǎn)擊的次數(shù),如果時(shí)間不足兩秒,復(fù)原count

    `    int count = 1 ;
    onBackPressed(){
    if(count != 2){
        Toast.makeText(this, "再按一次退出應(yīng)用", Toast.LENGTH_SHORT).show();
    }else{ finish();
    }
    count = 2;
    Observable.timer(2, TimeUnit.SECONDS)
        .subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                count = 1;
            }
        });
    }`

### 7.使用interval做周期性操作。當(dāng)有“每隔xx秒后執(zhí)行yy操作”類似的需求的時(shí)候,想到使用interval.
這也是我們經(jīng)常用到的,就比如首頁的輪播圖就可以通過Interval來實(shí)現(xiàn),我們每3秒讓ViewPager當(dāng)前顯示的頁面+1即可。具體代碼可以見我的另一篇簡書。

### 8.防抖點(diǎn)擊
這里我使用的Fragmention也做了防抖點(diǎn)擊,而且思想特別好,簡單。他的做法是當(dāng)我開啟一個(gè)Fragmention的時(shí)候,我禁用掉Activity全屏的觸摸事件,在開啟之后在恢復(fù)。
這里呢,我們使用RxJava的限流操作符,在1秒內(nèi)只允許流出來一個(gè)點(diǎn)擊事件。

    ```
    RxView.clicks(button)  
              .throttleFirst(1, TimeUnit.SECONDS)  
              .subscribe(new Observer<Object>() {  
                  @Override  
                  public void onCompleted() {  
                        log.d ("completed");  
                  }  
  
                  @Override  
                  public void onError(Throwable e) {  
                        log.e("error");  
                  }  
  
                  @Override  
                  public void onNext(Object o) {  
                       log.d("button clicked");  
                  }  
              });  
    ```

RxJava的使用場景很多,遇到自己沒見到過得操作符,可以看看圖片和文檔介紹,很詳細(xì),加油。

最后感謝awesome-Rxjava;你想要就的都在這里=>https://github.com/lzyzsd/Awesome-RxJava

附上本文的Demo:https://github.com/SshiGguang/Kits
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本篇文章介主要紹RxJava中操作符是以函數(shù)作為基本單位,與響應(yīng)式編程作為結(jié)合使用的,對什么是操作、操作符都有哪些...
    嘎啦果安卓獸閱讀 2,898評論 0 10
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,573評論 25 708
  • 作者寄語 很久之前就想寫一個(gè)專題,專寫Android開發(fā)框架,專題的名字叫 XXX 從入門到放棄 ,沉淀了這么久,...
    戴定康閱讀 7,670評論 13 85
  • 參考:給 Android 開發(fā)者的 RxJava 詳解-扔物線深入淺出RxJava 基礎(chǔ) "a library f...
    Vincen1024閱讀 548評論 0 1
  • 如果這個(gè)世界都在蒼老 我也可以寫一些明亮的句子 氤氳柔軟、美好、希望 就算把謊言藏在糖果里 我依然在一千次重復(fù)中 ...
    客秋一閱讀 182評論 0 0