Rxjava2入門教程五:Flowable背壓支持——幾乎可以說是對Flowable最全面而詳細的講解(轉)

如需下載源碼,請訪問

https://github.com/fengchuanfang/Rxjava2Tutorial

文章原創,轉載請注明出處:

Rxjava2入門教程五:Flowable背壓支持——幾乎可以說是對Flowable最全面而詳細的講解

通過前面四節的學習,我們已經了解了Rxjava2的基礎內容,掌握了Observer與Observable這對最典型的觀察者與可觀察對象的組合。

1、創建一個可觀察對象Observable發射數據流

2、通過操作符Operator加工處理數據流

3、通過線程調度器Scheduler指定操作數據流所在的線程

4、創建一個觀察者Observer接收響應數據流

在之后的章節中,我們一起了解一下Rxjava2的高級內容

背壓(backpressure)

通過上節的學習,我們了解到數據流發射,處理,響應可能在各自的線程中獨立進行,上游在發射數據的時候,不知道下游是否處理完,也不會等下游處理完之后再發射。

這樣,如果上游發射的很快而下游處理的很慢,會怎樣呢?

將會產生很多下游沒來得及處理的數據,這些數據既不會丟失,也不會被垃圾回收機制回收,而是存放在一個異步緩存池中,如果緩存池中的數據一直得不到處理,越積越多,最后就會造成內存溢出,這便是Rxjava中的背壓問題。

例如,運行以下代碼:

demo1.jpg

創建一個可觀察對象Obervable在Schedulers.newThread()()的線程中不斷發送數據,而觀察者Observer在Schedulers.newThread()的另一個線程中每隔5秒接收一條數據,運行后,查看內存使用如下:

backpressure.gif

由于上下游分別在各自的線程中獨立處理數據(如果上下游在同一線程中,下游對數據的處理會堵塞上游數據的發送,上游發送一條數據后會等下游處理完之后再發送下一條),而上游發送數據速度遠大于下游接收數據的速度,造成上下游流速不均,導致數據累計,最后引起內存溢出。

Flowable

Flowable是為了解決背壓(backpressure)問題,而在Observable的基礎上優化后的產物,與Observable不是同一組觀察者模式下的成員,Flowable是Publisher與Subscriber這一組觀察者模式中Publisher的典型實現,Observable是ObservableSource/Observer這一組觀察者模式中ObservableSource的典型實現;

所以在使用Flowable的時候,可觀察對象不再是Observable,而是Flowable;觀察者不再是Observer,而是Subscriber。Flowable與Subscriber之間依然通過subscribe()進行關聯。

有些朋友可能會想,既然Flowable是在Observable的基礎上優化后的產物,Observable能解決的問題Flowable都能進行解決,何不拋棄Observable而只用Flowable呢。其實,這是萬萬不可的,他們各有自己的優勢和不足。

由于基于Flowable發射的數據流,以及對數據加工處理的各操作符都添加了背壓支持,附加了額外的邏輯,其運行效率要比Observable低得多。

因為只有上下游運行在各自的線程中,且上游發射數據速度大于下游接收處理數據的速度時,才會產生背壓問題。

所以,如果能夠確定上下游在同一個線程中工作,或者上下游工作在不同的線程中,而下游處理數據的速度高于上游發射數據的速度,則不會產生背壓問題,就沒有必要使用Flowable,以免影響性能。

通過Flowable發射處理數據流的基礎代碼如下:

demo2.jpg

執行結果如下:

System.out: 發射----> 1System.out: 發射----> 2System.out: 發射----> 3System.out: 發射----> 完成System.out: 接收----> 1System.out: 接收----> 2System.out: 接收----> 3System.out: 接收----> 完成

我們發現運行結果與Observerable沒有區別,但是的代碼中,除了為上下游指定各自的運行線程外,還有三點不同

一、create方法中多了一個BackpressureStrategy類型的參數。

二、onSubscribe回調的參數不是Disposable而是Subscription,多了行代碼:

s.request(Long.MAX_VALUE);

三、Flowable發射數據時,使用的發射器是FlowableEmitter而不是ObservableEmitter

BackpressureStrategy背壓策略

在Flowable的基礎創建方法create中多了一個BackpressureStrategy類型的參數,

BackpressureStrategy是個枚舉,源碼如下:

publicenumBackpressureStrategy {? ERROR,BUFFER,DROP,LATEST,MISSING}

其作用是什么呢?

Flowable的異步緩存池不同于Observable,Observable的異步緩存池沒有大小限制,可以無限制向里添加數據,直至OOM,而Flowable的異步緩存池有個固定容量,其大小為128。

BackpressureStrategy的作用便是用來設置Flowable通過異步緩存池存儲數據的策略。

ERROR

在此策略下,如果放入Flowable的異步緩存池中的數據超限了,則會拋出MissingBackpressureException異常。

運行如下代碼:

demo3.jpg

Flowable發射129條數據,Subscriber在睡10秒之后再開始接收,運行后會發現控制臺打印如下異常:

W/System.err:io.reactivex.exceptions.MissingBackpressureException:create:couldnotemit value due to lack of requestsW/System.err:at io.reactivex.internal.operators.flowable.FlowableCreate$ErrorAsyncEmitter.onOverflow(FlowableCreate.java:411)W/System.err:at io.reactivex.internal.operators.flowable.FlowableCreate$NoOverflowBaseAsyncEmitter.onNext(FlowableCreate.java:377)W/System.err:at net.fbi.rxjava2.RxJava2Demo$6.subscribe(RxJava2Demo.java:103)W/System.err:at io.reactivex.internal.operators.flowable.FlowableCreate.subscribeActual(FlowableCreate.java:72)W/System.err:at io.reactivex.Flowable.subscribe(Flowable.java:12218)W/System.err:at io.reactivex.internal.operators.flowable.FlowableSubscribeOn$SubscribeOnSubscriber.run(FlowableSubscribeOn.java:82)W/System.err:at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:59)W/System.err:at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:51)W/System.err:at java.util.concurrent.FutureTask.run(FutureTask.java:237)W/System.err:at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:154)W/System.err:at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:269)W/System.err:at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)W/System.err:at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)W/System.err:at java.lang.Thread.run(Thread.java:818)

如果將Flowable發射數據的條數改為128,則不會出現此異常。

DROP

在此策略下,如果Flowable的異步緩存池滿了,會丟掉將要放入緩存池中的數據。

運行如下代碼:

demo4.jpg

在上面代碼中通過創建Flowable發射500條數據,每隔100毫秒發射一次,并記錄開始發射和結束發射的時間,下游每隔300毫秒接收一次數據,運行后,控制臺打印日志如下:

GIF111.gif

通過日志

1.jpg

我們可以發現Subscriber在接收完第128條數據后,再次接收的時候已經到了288,而這之間的60條數據正是因為緩存池滿了而被丟棄掉了。

那么問題來了,當Flowable在發射第129條數據的時候,Subscriber已經接收了42條數據了,第129條數據為什么沒有放入緩存池中呢?日志如下:

2.jpg

那是因為緩存池中數據的清理,并不是Subscriber接收一條,便清理一條,而是每累積到95條清理一次。也就是Subscriber接收到第96條數據時,緩存池才開始清理數據,之后Flowable發射的數據才得以放入。

3.jpg

查看日志可以發現,Subscriber接收到第96條數據后,Flowable發射第288條數據。而第128到288之間的數據,正好處于緩存池存滿的狀態,而被丟棄,所以Subscriber在接收完第128條數據之后,接收到的是第288條數據,而不是第129條。

LATEST

與Drop策略一樣,如果緩存池滿了,會丟掉將要放入緩存池中的數據,不同的是,不管緩存池的狀態如何,LATEST都會將最后一條數據強行放入緩存池中。

將上述代碼中的DROP策略改為LATEST:

demo5.jpg

運行后日志對比如下:

DROP:

DROP.jpg

LATEST:

LATEST.jpg

latest策略下Subscriber在接收完成之前,接收的數據是Flowable發射的最后一條數據,而Drop策略下不是。

BUFFER

此策略下,Flowable的異步緩存池同Observable的一樣,沒有固定大小,可以無限制向里添加數據,不會拋出MissingBackpressureException異常,但會導致OOM。

運行如下代碼:

demo6.jpg

查看內存使用:

GIF222.gif

會發現和使用Observalbe時一樣,都會導致內存劇增,最后導致OOM,不同的是使用Flowable內存增長的速度要慢得多,那是因為基于Flowable發射的數據流,以及對數據加工處理的各操作符都添加了背壓支持,附加了額外的邏輯,其運行效率要比Observable低得多。

MISSING

此策略表示,通過Create方法創建的Flowable沒有指定背壓策略,不會對通過OnNext發射的數據做緩存或丟棄處理,需要下游通過背壓操作符(onBackpressureBuffer()/onBackpressureDrop()/onBackpressureLatest())指定背壓策略。

onBackpressureXXX背壓操作符

Flowable除了通過create創建的時候指定背壓策略,也可以在通過其它創建操作符just,fromArray等創建后通過背壓操作符指定背壓策略。

onBackpressureBuffer()對應BackpressureStrategy.BUFFER

onBackpressureDrop()對應BackpressureStrategy.DROP

onBackpressureLatest()對應BackpressureStrategy.LATEST

例如代碼

demo7.jpg

等同于,代碼:

demo8.jpg

Subscription

Subscription與Disposable均是觀察者與可觀察對象建立訂閱狀態后回調回來的參數,如同通過Disposable的dispose()方法可以取消Observer與Oberverable的訂閱關系一樣,通過Subscription的cancel()方法也可以取消Subscriber與Flowable的訂閱關系。

不同的是接口Subscription中多了一個方法request(long n),如上面代碼中的:

s.request(Long.MAX_VALUE);

此方法的作用是什么呢,去掉這個方法會有什么影響呢?

運行如下代碼:

demo9.jpg

運行結果如下:

System.out: 發射----> 1System.out: 發射----> 2System.out: 發射----> 3System.out: 發射----> 完成

我們發現Flowable照常發送數據,而Subsriber不再接收數據。

這是因為Flowable在設計的時候,采用了一種新的思路——響應式拉取方式,來設置下游對數據的請求數量,上游可以根據下游的需求量,按需發送數據。

如果不顯示調用request則默認下游的需求量為零,所以運行上面的代碼后,上游Flowable發射的數據不會交給下游Subscriber處理。

運行如下代碼:

demo10.jpg

運行結果如下:

System.out: 發射----> 1System.out: 發射----> 2System.out: 發射----> 3System.out: 發射----> 完成System.out: 接收----> 1System.out: 接收----> 2

我們發現通過s.request(2);設置Subscriber的數據請求量為2條,超出其請求范圍之外的數據則沒有接收。

多次調用request會產生怎樣的結果呢?

運行如下代碼:

demo11.jpg

通過Flowable發射10條數據,在onSubscribe(Subscription s) 方法中調用兩次request,運行結果如下:

AB417C9CAC5A4BD98375240B5A5C1D6A.jpg

我們發現Subscriber總共接收了7條數據,是兩次需求累加后的數量。

通過日志我們發現,上游并沒有根據下游的實際需求,發送數據,而是能發送多少,就發送多少,不管下游是否需要。

而且超出下游需求之外的數據,仍然放到了異步緩存池中。這點我們可以通過以下代碼來驗證:

demo12.jpg

通過Flowable發射130條數據,通過s.request(1)設置下游的數據請求量為1條,設置緩存策略為BackpressureStrategy.ERROR,如果異步緩存池超限,會導致MissingBackpressureException異常。

運行之后,日志如下:

MissingBackpressureException.jpg

久違的異常出現了,所以超出下游需求之外的數據,仍然放到了異步緩存池中,并導致緩存池溢出。

那么上游如何才能按照下游的請求數量發送數據呢,

雖然通過request可以設置下游的請求數量,但是上游并沒有獲取到這個數量,如何獲取呢?

這便需要用到Flowable與Observable的第三點區別,Flowable特有的發射器FlowableEmitter

FlowableEmitter

flowable的發射器FlowableEmitter與observable的發射器ObservableEmitter均繼承自Emitter(Emitter在教程二中已經說過了)

比較兩者源碼可以發現;

publicinterfaceObservableEmitterextendsEmitter{voidsetDisposable(Disposable d);voidsetCancellable(Cancellable c);booleanisDisposed();ObservableEmitterserialize();}

publicinterfaceFlowableEmitterextendsEmitter{voidsetDisposable(Disposable s);voidsetCancellable(Cancellable c);longrequested();booleanisCancelled();FlowableEmitterserialize();}

接口FlowableEmitter中多了一個方法

longrequested();

我們可以通過這個方法來獲取當前未完成的請求數量,

運行下面的代碼,這次我們要先喪失一下原則,雖然我們之前說過同步狀態下不使用Flowable,但是這次我們需要先看一下同步狀態下情況。

demo13.jpg

打印日志如下:

4.jpg

通過日志我們發現, 通過e.requested()獲取到的是一個動態的值,會隨著下游已經接收的數據的數量而遞減。

在上面的代碼中,我們沒有指定上下游的線程,上下游運行在同一線程中。

這與我們之前提到的,同步狀態下不使用Flowable相違背。那是因為異步情況下e.requested()的值太復雜,必須通過同步情況過渡一下才能說得明白。

我們在上面代碼的基礎上,給上下游指定獨立的線程,代碼如下

demo14.jpg

運行后日志如下:

log5.jpg

雖然我們指定了下游的數據請求量為3,但是我們在上游獲取未完成請求數量的時候,并不是3,而是128。難道上游有個最小未完成請求數量?只要下游設置的數據請求量小于128,上游獲取到的都是128?

帶著這個疑問,我們試一下當下游的數據請求量為500,大于128時的情況。

demo15.jpg

運行日志如下;

log6.jpg

結果還是128.

其實不論下游通過s.request();設置多少請求量,我們在上游獲取到的初始未完成請求數量都是128。

這是為啥呢?

還記得之前我們說過,Flowable有一個異步緩存池,上游發射的數據,先放到異步緩存池中,再由異步緩存池交給下游。所以上游在發射數據時,首先需要考慮的不是下游的數據請求量,而是緩存池中能不能放得下,否則在緩存池滿的情況下依然會導致數據遺失或者背壓異常。如果緩存池可以放得下,那就發送,至于是否超出了下游的數據需求量,可以在緩存池向下游傳遞數據時,再作判斷,如果未超出,則將緩存池中的數據傳遞給下游,如果超出了,則不傳遞。

如果下游對數據的需求量超過緩存池的大小,而上游能獲取到的最大需求量是128,上游對超出128的需求量是怎么獲取到的呢?

帶著這個疑問,我們運行一下,下面的代碼,上游發送150個數據,下游也需要150個數據。

demo16.jpg

截取部分日志如下:

log7.jpg

我們發現通過e.requested()獲取到的上游當前未完成請求數量并不是一直遞減的,在遞減到33時,又回升到了128.而回升的時機正好是在下游接收了96條數據之后。我們之前說過,異步緩存池中的數據并不是向下游發射一條便清理一條,而是每等累積到95條時,清理一次。通過e.requested()獲取到的值,正是在異步緩存池清理數據時,回升的。也就是,異步緩存池每次清理后,有剩余的空間時,都會導致上游未完成請求數量的回升,這樣既不會引發背壓異常,也不會導致數據遺失。

上游在發送數據的時候并不需要考慮下游需不需要,而只需要考慮異步緩存池中是否放得下,放得下便發,放不下便暫停。所以,通過e.requested()獲取到的值,并不是下游真正的數據請求數量,而是異步緩存池中可放入數據的數量。數據放入緩存池中后,再由緩存池按照下游的數據請求量向下傳遞,待到傳遞完的數據累積到95條之后,將其清除,騰出空間存放新的數據。如果下游處理數據緩慢,則緩存池向下游傳遞數據的速度也相應變慢,進而沒有傳遞完的數據可清除,也就沒有足夠的空間存放新的數據,上游通過e.requested()獲取的值也就變成了0,如果此時,再發送數據的話,則會根據BackpressureStrategy背壓策略的不同,拋出MissingBackpressureException異常,或者丟掉這條數據。

所以上游只需要在e.requested()等于0時,暫停發射數據,便可解決背壓問題。

最終方案

下面我們回到最初的問題

運行下面代碼:

demo17.jpg

由于下游處理數據的速度(Thread.sleep(50))趕不上上游發射數據的速度,則會導致背壓問題。

運行后查看內存使用如下:

GIF333.gif

內存暴增,很快就會OOM

下面,對其通過Flowable做些改進,讓其既不會產生背壓問題,也不會引起異常或者數據丟失。

代碼如下:

demo18.jpg

下游處理數據的速度Thread.sleep(50)趕不上上游發射數據的速度,

不同的是,我們在下游onNext(Integer integer)方法中,每接收一條數據增加一條請求量,

mSubscription.request(1)

在上游添加代碼

if(e.requested()==0)continue;

讓上游按需發送數據。

運行后查看內存:

GIF999.gif

內存一直相當的平靜,而且上游嚴格按照下游的需求量發送數據,不會產生MissingBackpressureException異常,或者丟失數據。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容