給初學者的RxJava2.0教程(五)

Season發表於2016-12-13

Outline

[TOC]

前言

大家喜聞樂見的Backpressure來啦.

這一節中我們將來學習Backpressure. 我看好多吃瓜群眾早已坐不住了, 別急, 我們先來回顧一下上一節講的Zip.

正題

上一節中我們說到Zip可以將多個上游傳送的事件組合起來傳送給下游, 那大家有沒有想過一個問題, 如果其中一個水管A傳送事件特別快, 而另一個水管B 傳送事件特別慢, 那就可能出現這種情況, 發得快的水管A 已經傳送了1000個事件了, 而發的慢的水管B 才發一個出來, 組合了一個之後水管A 還剩999個事件, 這些事件需要繼續等待水管B 傳送事件出來組合, 那麼這麼多的事件是放在哪裡的呢? 總有一個地方儲存吧? 沒錯, Zip給我們的每一根水管都弄了一個水缸 , 用來儲存這些事件, 用通俗易懂的圖片來表示就是:

給初學者的RxJava2.0教程(五)
zip2.png

如圖中所示, 其中藍色的框框就是zip給我們的水缸! 它將每根水管發出的事件儲存起來, 等兩個水缸都有事件了之後就分別從水缸中取出一個事件來組合, 當其中一個水缸是空的時候就處於等待的狀態.

題外話: 大家來分析一下這個水缸有什麼特點呢? 它是按順序儲存的, 先進來的事件先取出來, 這個特點是不是很熟悉呀? 沒錯, 這就是我們熟知的佇列, 這個水缸在Zip內部的實現就是用的佇列, 感興趣的可以翻看原始碼檢視.

好了回到正題上來, 這個水缸有大小限制嗎? 要是一直往裡存會怎樣? 我們來看個例子:

Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() {    
    @Override                                                                          
    public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {       
        for (int i = 0; ; i++) {   //無限迴圈發事件                                                    
            emitter.onNext(i);                                                         
        }                                                                              
    }                                                                                  
}).subscribeOn(Schedulers.io());    

Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() {      
    @Override                                                                          
    public void subscribe(ObservableEmitter<String> emitter) throws Exception {        
        emitter.onNext("A");                                                           
    }                                                                                  
}).subscribeOn(Schedulers.io());    

Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() {                 
    @Override                                                                          
    public String apply(Integer integer, String s) throws Exception {                  
        return integer + s;                                                            
    }                                                                                  
}).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<String>() {                               
    @Override                                                                          
    public void accept(String s) throws Exception {                                    
        Log.d(TAG, s);                                                                 
    }                                                                                  
}, new Consumer<Throwable>() {                                                         
    @Override                                                                          
    public void accept(Throwable throwable) throws Exception {                         
        Log.w(TAG, throwable);                                                         
    }                                                                                  
});複製程式碼

在這個例子中, 我們分別建立了兩根水管, 第一根水管用機器指令的執行速度來無限迴圈傳送事件, 第二根水管隨便傳送點什麼, 由於我們沒有傳送Complete事件, 因此第一根水管會一直髮事件到它對應的水缸裡去, 我們來看看執行結果是什麼樣.

執行結果GIF圖:

給初學者的RxJava2.0教程(五)
zip2.gif

我勒個草, 記憶體佔用以斜率為1的直線迅速上漲, 幾秒鐘就300多M , 最終報出了OOM:

zlc.season.rxjava2demo W/art: Throwing OutOfMemoryError "Failed to allocate a 28 byte allocation with
4194304 free bytes and 8MB until OOM; 
zlc.season.rxjava2demo W/art: "main" prio=5 tid=1 Runnable      
zlc.season.rxjava2demo W/art:   | group="main" sCount=0 dsCount=0 obj=0x75188710 self=0x7fc0efe7ba00   
zlc.season.rxjava2demo W/art:   | sysTid=32686 nice=0 cgrp=default sched=0/0 handle=0x7fc0f37dc200    
zlc.season.rxjava2demo W/art:   | state=R schedstat=( 0 0 0 ) utm=948 stm=120 core=1 HZ=100         
zlc.season.rxjava2demo W/art:   | stack=0x7fff971e8000-0x7fff971ea000 stackSize=8MB         
zlc.season.rxjava2demo W/art:   | held mutexes= "mutator lock"(shared held)    
zlc.season.rxjava2demo W/art:     at java.lang.Integer.valueOf(Integer.java:742)複製程式碼

出現這種情況肯定是我們不想看見的, 這裡就可以引出我們的Backpressure了, 所謂的Backpressure其實就是為了控制流量, 水缸儲存的能力畢竟有限, 因此我們還得從源頭去解決問題, 既然你發那麼快, 資料量那麼大, 那我就想辦法不讓你發那麼快唄.

那麼這個源頭到底在哪裡, 究竟什麼時候會出現這種情況, 這裡只是說的Zip這一個例子, 其他的地方會出現嗎? 帶著這個問題我們來探究一下.

我們讓事情變得簡單一點, 從一個單一的Observable說起.

來看段程式碼:

Observable.create(new ObservableOnSubscribe<Integer>() {                         
    @Override                                                                    
    public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { 
        for (int i = 0; ; i++) {   //無限迴圈發事件                                              
            emitter.onNext(i);                                                   
        }                                                                        
    }                                                                            
}).subscribe(new Consumer<Integer>() {                                           
    @Override                                                                    
    public void accept(Integer integer) throws Exception {                       
        Thread.sleep(2000);                                                      
        Log.d(TAG, "" + integer);                                                
    }                                                                            
});複製程式碼

這段程式碼很簡單, 上游同樣無限迴圈的傳送事件, 在下游每次接收事件前延時2秒. 上下游工作在同一個執行緒裡, 來看下執行結果:

給初學者的RxJava2.0教程(五)
peace.gif

哎臥槽, 怎麼如此平靜, 感覺像是走錯了片場.

為什麼呢, 因為上下游工作在同一個執行緒呀騷年們! 這個時候上游每次呼叫emitter.onNext(i)其實就相當於直接呼叫了Consumer中的:

   public void accept(Integer integer) throws Exception {                       
        Thread.sleep(2000);                                                      
        Log.d(TAG, "" + integer);                                                
   }     複製程式碼

所以這個時候其實就是上游每延時2秒傳送一次. 最終的結果也說明了這一切.

那我們加個執行緒呢, 改成這樣:

Observable.create(new ObservableOnSubscribe<Integer>() {                            
    @Override                                                                       
    public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {    
        for (int i = 0; ; i++) {    //無限迴圈發事件                                                     
            emitter.onNext(i);                                                      
        }                                                                           
    }                                                                               
}).subscribeOn(Schedulers.io())                                                    
        .observeOn(AndroidSchedulers.mainThread())                                  
        .subscribe(new Consumer<Integer>() {                                        
            @Override                                                               
            public void accept(Integer integer) throws Exception {                  
                Thread.sleep(2000);                                                 
                Log.d(TAG, "" + integer);                                           
            }                                                                       
        });複製程式碼

這個時候把上游切換到了IO執行緒中去, 下游到主執行緒去接收, 來看看執行結果呢:

給初學者的RxJava2.0教程(五)
violence.gif

可以看到, 給上游加了個執行緒之後, 它就像脫韁的野馬一樣, 記憶體又爆掉了.

為什麼不加執行緒和加上執行緒區別這麼大呢, 這就涉及了同步非同步的知識了.

當上下游工作在同一個執行緒中時, 這時候是一個同步的訂閱關係, 也就是說上游每傳送一個事件必須等到下游接收處理完了以後才能接著傳送下一個事件.

當上下游工作在不同的執行緒中時, 這時候是一個非同步的訂閱關係, 這個時候上游傳送資料不需要等待下游接收, 為什麼呢, 因為兩個執行緒並不能直接進行通訊, 因此上游傳送的事件並不能直接到下游裡去, 這個時候就需要一個田螺姑娘來幫助它們倆, 這個田螺姑娘就是我們剛才說的水缸 ! 上游把事件傳送到水缸裡去, 下游從水缸裡取出事件來處理, 因此, 當上遊發事件的速度太快, 下游取事件的速度太慢, 水缸就會迅速裝滿, 然後溢位來, 最後就OOM了.

這兩種情況用圖片來表示如下:

同步:

給初學者的RxJava2.0教程(五)
同步.png

非同步:

給初學者的RxJava2.0教程(五)
非同步.png

從圖中我們可以看出, 同步和非同步的區別僅僅在於是否有水缸.

相信通過這個例子大家對執行緒之間的通訊也有了比較清楚的認知和理解.

源頭找到了, 只要有水缸, 就會出現上下游傳送事件速度不平衡的情況, 因此當我們以後遇到這種情況時, 仔細思考一下水缸在哪裡, 找到水缸, 你就找到了解決問題的辦法.

既然源頭找到了, 那麼下一節我們就要來學習如何去解決了. 下節見.

相關文章