如需下載源碼,請訪問
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異常,或者丟失數據。