RxJava實戰技巧大全

這幾天一直看《激盪三十年》,感覺非常不錯。這篇文章本身並不想寫,總覺得沒什麼含量,後面寫着寫着,發現其中的有些點還是非常有意思的,因此這篇文章的重點在rxjava使用場景說明及rxjava內存優化上。rxjava的使用場景更多的取決於我們所面臨的業務以及當前rxjava的一些操作符是否提供了對業務的支持,而對於內存優化上,則是要求我們儘可能快的解除訂閱關係。

另外本文對RxBinding,RxLifecycle的介紹並未做過多的深入,原因在於這兩個庫無論是從實現還是從使用角度來說都是比較簡單的,我們完全可以自行書寫類似的庫。

話不多說,步入正題。


引入RxBinding

在引入RxBinding之前首先要弄明吧RxBinding是什麼?。

所謂的RxBinding是用來爲界面元素綁定事件的,比如爲Buttong設置點擊事件等。瀏覽其源碼,不難發現其實現原理也是通過包裝原有事件實現的。

爲什麼要引入RxBinding?使用RxJava一定要引入RxBinding麼?

首先很確定的說使用RxJava不要求你一定要使用RxBinding,大部分情況下沒必要用。這裏之所以要談RxBinding一方面是完善我們的知識體系,看看響應式編程的思想是如何應用在android界面元素上,另一方面是看看RxBinding能夠有效的解決什麼問題?

RxBinding提供了和RxJava一致的api體驗,更重要的是它更好的符合RxJava做法:通過將事件轉化爲Observable對象,最終可以利用RxJava一系列操作符對其處理,最典型的使用場景是界面防抖動,這點我們在rxjava真實應用場景中做詳細的介紹。

關於如何使用RxBinding,直接參見RxBinding項目說明即可:https://github.com/JakeWharton/RxBinding


rxjava真實應用場景

在上一篇文章中爲解決異常問題,我們引入了rxjava的支持。接下來我們來看看rxjava在實際工程中的顯著應用。

場景零:線程切換

rxjava引入讓使得線程切換更加的容易,幾行代碼就可以搞定。RxAnroid的引入更是讓我們非常容易的能夠切換到UI線程。可以說,引入RxJava,就放棄古老而沉重的AsyncTask吧(初學者還是要學AsyncTask的)。最典型的就是從網絡中獲取數據,然後在更新界面,很顯然獲取數據操作需要發生在子線程,更新UI操作發聲明在主線程。這裏我們以模擬從數據庫中獲取聯繫人操作爲例:

private void getConcactFromDB() {
        Observable.create(new Observable.OnSubscribe<List<String>>() {
            @Override
            public void call(Subscriber<? super List<String>> subscriber) {
                //模擬從數據庫中獲取數據
                ArrayList<String> list = new ArrayList<>();
                for (int i = 0; i < 100; i++) {
                    list.add("user name:" + i);
                }
                //模擬耗時操作
                SystemClock.sleep(5000);

                subscriber.onNext(list);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<String>>() {
                    @Override
                    public void call(List<String> list) {
                        Log.d("MainActivity", "更新界面:" + list.size());
                    }
                });
    }

場景一:接口依賴(flatmap)

目前大部分服務端接口設計都是通過用戶名和密碼登錄獲取access token,後面其他api的請求都是藉助該token。對於需要註冊功能的產品來說,我們經常面對這樣的問題:使用用戶名和密碼登錄成功後,保存服務器返回的access token,再調用服務端接口獲取用戶的詳情信息。不難發現,這裏獲取用戶詳情的請求依賴登錄請求.我們先來看傳統方法是如何解決這問題:

private void handleLogin2(LoginPost post) {
        ApiFactory.getBaseApi().login(post).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber<Result<Token>>(this){
                    @Override
                    public void onNext(Result<Token> tokenResult) {
                        if (tokenResult.isOk()) {
                            Token data = tokenResult.getData();
                            String token = data.getToken();
                           //保存token操作 ApiFactory.getUserApi().getUserProfile(token).subscribeOn(Schedulers.io())
                                    .observeOn(AndroidSchedulers.mainThread())
                                    .subscribe(new BaseSubscriber<Result<User>>(LoginActivity.this){
                                        @Override
                                        public void onNext(Result<User> userResult) {
                                            //處理用戶信息
                                        }

                                        @Override
                                        public void onError(Throwable e) {
                                            //處理錯誤
                                        }
                                    });
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        //處理錯誤
                    }
                });
    }

開始時,我們大部分人會寫出類似以上的代碼。當然,這實現了我們想要的邏輯,但當你仔細思考的時候,會發現幾個問題:

  1. 回調嵌套看起來令人疑惑。由於我們在大多數情況下是線性思維,那麼此時當你看到onNext(Result<Token> tokenResult)中又去嵌套處理獲取用戶信息的接口你的思維不得不跳躍一下。
  2. 登錄功能的異常處理點被分隔了,使我們不得不寫出冗餘的代碼。
  3. 多次線程開銷好像可以被進一步優化。

實際上這三個問題的根本原因在於我們在實現登錄功能的時候是以方法作爲最小單位,而不是以登錄邏輯爲最小單位,因此看起不是那麼的連貫。現在來看看我們應該怎麼樣讓上面的代碼具有連貫性:

private void handleLogin(LoginPost post) {
        ApiFactory.getBaseApi().login(post).flatMap(new Func1<Result<Token>, Observable<Result<User>>>() {

            @Override
            public Observable<Result<User>> call(Result<Token> tokenResult) {
                if (tokenResult.isOk()) {//獲取token成功
                    Token data = tokenResult.getData();
                    String token = data.getToken();
                    //保存token操作
                    return ApiFactory.getUserApi().getUserProfile(token);//獲取用戶信息
                } else {//獲取token,直接觸發onError()方法
                    return Observable.error(new ApiException(tokenResult.getCode(), tokenResult.getMsg()));
                }
            }
        }).subscribeOn(Schedulers.io())
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call() {
                        showWaitDialog();
                    }
                }).subscribeOn(AndroidSchedulers.mainThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber<Result<User>>(this) {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onNext(Result<User> userResult) {
                        //處理用戶信息

                    }

                    @Override
                    public void onError(Throwable e) {
                       、//處理錯誤
                    }
                });


    }

通過flatmap操作符,不但解決了接口依賴問題,而且使得代碼邏輯相比之前更具有連貫性。另外,這裏引入的BaseSubscriber在我們Retrofit響應數據及異常處理策略做過說明了,不明白的同學可以自行查閱。

場景二:接口合併(merge)

很多情況下,一個界面中需要的數據來自多個數據源(請求),而只有當所有的請求的響應數據都拿到之後才能渲染界面。

接口結果同類型

當前數據源來自多個渠道,拿到的結果屬於同一類型的,比如有些數據需要從本地數據讀取,而另一些數據則從網絡中獲取,但無論哪個數據源今最後返回的數據類型是一樣的,比如:

 private Observable<ArrayList<String>> getDataFromNet() {
        ArrayList<String> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add("data from net:" + i);
        }

        return Observable.just(list);
    }

    private Observable<ArrayList<String>> getDataFromDisk() {
        ArrayList<String> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add("data from disk:" + i);
        }

        return Observable.just(list);
    }

上面的兩個方法分別從磁盤和網絡中獲取數據,且最後的數據類型都是ArrayList<String>,現在我們來合併這兩個接口:

   private void getData() {
        Observable.merge(getDataFromDisk(), getDataFromNet()).subscribe(new Subscriber<ArrayList<String>>() {
            @Override
            public void onCompleted() {
                //更新界面
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(ArrayList<String> list) {
                for (String s : list) {
                    Log.d("MainActivity", s);
                }
            }
        });

接口結果不同類型

有些情況下,不同數據源返回的結果類型不一致,那該如何解決呢?比如當前存在兩個接口:

@GET("dict/locations")
Observable<Result<ArrayList<String>>> getLocationList();

@GET("user")
Observable<Result<User>> getUserInfo(@Query("id") String id);

只有當這兩個請求都完成後才能更新UI,那我們該怎麼做呢?同樣還是使用merge操作符,關鍵在於如何區分響應:

    private void getData(String uid) {
        Observable<Result<ArrayList<String>>> locationOb = ApiFactory.getUserApi().getLocationList();
        Observable<Result<User>> userOb = ApiFactory.getUserApi().getUserInfo(uid);

        Observable.merge(locationOb,userOb).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<Result<? extends Object>>() {
                    @Override
                    public void onCompleted() {
                        //更新UI
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onNext(Result<? extends Object> result) {
                        Object data = result.getData();
                        if(data instanceof User ){
                            //處理用戶數據
                        } else if (data instanceof ArrayList) {
                            //處理位置列表
                        }
                    }
                });

    }

場景三:構建多級緩存(concat)

緩存機制想必是衆所周知。這裏我們就以常見的三級緩存機制爲例:首先從內存中獲取數據,如果內存中不存在,則從硬盤中獲取數據,如果硬盤中不存在數據,則從網絡中獲取數據。現在看看RxJava是如何幫我們解決這個問題:

 //獲取數據
 private void getData(String url) {

        Observable.concat(getDataInMemory(url),getDataInDisk(url),getDataInNet(url)).takeFirst(new Func1<Bitmap, Boolean>() {
            @Override
            public Boolean call(Bitmap bitmap) {
                return bitmap!=null;
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Bitmap>() {
            @Override
            public void call(Bitmap bitmap) {
                //處理圖片
            }
        });
    }

    //從內存中獲取
    private Observable<Bitmap> getDataInMemory(final String url) {
        final Map<String, Bitmap> memoryCache = new HashMap<>();
        //模擬內存中的數據
        //...

        return Observable.create(new Observable.OnSubscribe<Bitmap>() {

            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                if (memoryCache.containsKey(url)) {
                    subscriber.onNext(memoryCache.get(url));
                }
                subscriber.onCompleted();
            }
        });
    }

    //從硬盤中獲取
    private Observable<Bitmap> getDataInDisk(final String url) {
        final Map<String, Bitmap> diskCache = new HashMap<>();
        //模擬內存中的數據
        //...

        return Observable.create(new Observable.OnSubscribe<Bitmap>() {

            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                if (diskCache.containsKey(url)) {
                    subscriber.onNext(diskCache.get(url));
                }
                subscriber.onCompleted();
            }
        });

    }

    //從網絡中獲取
    private Observable<Bitmap> getDataInNet(final String url) {
        return Observable.create(new Observable.OnSubscribe<Bitmap>(){

            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                Bitmap bitmap=null;
                //從網絡獲取圖片bitmap

                subscriber.onNext(bitmap);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io());
    }

rxjava爲我們提供的concat操作符可以很容的的實現多級緩存機制。這裏需要記住在getData()方法中不要忘記使用takeFirst()。concat操作符接受多個Observable,並按其順序串聯,
在訂閱的時候會返回所有Observable的數據(按順序依次返回)。換言之,如果在getData()中不實用takeFirst(),將會並行的從內存,硬盤及網絡中檢索數據,這顯然不是我們想要的。takeFirst操作符可以從返回的數據中取出第一個,並中斷數據檢索的過程。我們知道,檢索速度:內存>硬盤>網絡,這就意味着當我們從內存中獲取到數據的時候就不會再從硬盤中獲取數據,反之,則從硬盤中獲取數據;當我們從硬盤中獲取到數據的時候就不會再從網絡中獲取到數據了,反之,則從網絡中獲取。

這樣就實現了我們的最終目標。

場景四:定時任務(timer)

在一些情況下我們需要執行定時任務,傳統的做法上有兩種方式可選擇:Timer和SchelchExector。但是在引入rxjava之後,我們有了第三種選擇:

    private void startTimerTask() {
        Observable.timer(2, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start execute task:" + Thread.currentThread().getName());
            }
        });
    }

場景五:週期任務(interval)

當然rxjava通過interval提供了週期任務的支持:

 private void startIntervalTask() {
        Observable.interval(5, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

場景六:數據過濾(filter)

在處理集合時,我們經常需要過濾操作,這時候使用filte操作符就非常有用,用個簡單示例:

   private void dataFilter() {
        final HashSet<String> hashSet = new HashSet<>();
        hashSet.add("1");
        hashSet.add("2");

        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("");

        Observable.from(list).filter(new Func1<String, Boolean>() {
            @Override
            public Boolean call(String s) {
                return !TextUtils.isEmpty("") && !hashSet.contains(s);
            }
        }).subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                Log.d("MainActivity", "result: " + s);
            }
        });

    }

場景七:界面防抖動(throttleFirst)

所謂的界面防抖動就是用於處理快速點擊某控件導致重複打開界面的操作,比如點擊某個button可以打開一個Activity,正常情況下,我們一旦點擊了該Button便會等待該Activity。在應用響應比較慢,用戶以爲無響應而多次點擊Button或者惡意快速點擊的情況下,會重複打開同一個Activity,當用戶想要退出該Activity的時候體驗會非常差。

通過rxjava提供的throttleFirst操作符我們能夠很容易防止按鈕在單位時間內被重複點擊的問題:

RxView.clicks(mBtnTest2).throttleFirst(1L, TimeUnit.SECONDS).subscribe(new Action1<Void>() {
        @Override
        public void call(Void aVoid) {
            Toast.makeText(MainActivity.this, "button2 clicked", Toast.LENGTH_SHORT).show();
        }
    });

場景八:老接口適配(just)

當你在爲老項目添加rxjava支持的時候,難免需要將一些方法返回類型轉爲Observable.通過just操作符不需要對原方法進行任何修改便可實現:

private int oldMethod(int x, int y) {
        return x+y;
    }

private void addTest() {
    Observable.just(oldMethod(4, 9)).subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer result) {
            Log.d("MainActivity", "result:" + result);
        }
    });
    }

場景十:響應式界面

界面元素更新

在信息填充界面時,我們經常會遇到只有填寫完必要的信息之後,提交按鈕才能被點擊的情況。比如在登錄界面時,只有我們填寫完用戶名和密碼之後,登錄按鈕才能被點擊。通過藉助rxjava提供的combineLatest操作符我們可以容易的實現這種響應式界面

  EditText mEditUsername = (EditText) findViewById(R.id.editText3);
        EditText mEditPwd = (EditText) findViewById(R.id.editText4);
        final Button mBtnLogin = (Button) findViewById(R.id.button2);
        mBtnLogin.setEnabled(false);

        Observable<CharSequence> usernameOb = RxTextView.textChanges(mEditUsername);
        Observable<CharSequence> pwdOb = RxTextView.textChanges(mEditPwd);

        Observable.combineLatest(usernameOb, pwdOb, new Func2<CharSequence, CharSequence, Boolean>() {
            @Override
            public Boolean call(CharSequence username, CharSequence pwd) {

                return !TextUtils.isEmpty(username) && !TextUtils.isEmpty(pwd);
            }
        }).subscribe(new Action1<Boolean>() {
            @Override
            public void call(Boolean isLogin) {
                mBtnLogin.setEnabled(isLogin);
            }
        });

RxJava內存優化

內存優化

藉助rxjava提供的線程調度器Scheduler我們可以很容的實現線程切換,目前Scheduler提供了一下幾種調度策略:

  • Schedulers.immediate():默認的調度策略,不指定線程,也就是運行在當前線程
  • Schedulers。newThread():運行在一個新創建的線程當中,相當於new Thread()操作。
  • Schedulers.io():採用了線程池機制,內部維護了一個不限制線程數量的線程池,用於IO密集型操作。
  • Schedulers.computation():同樣採用了線程池機制,只不過線程池中線程的數量取決與CPU的核數,以便實現最大性能。通常用於CPU密集型操作,比如圖形處理。

通過上面的介紹,我們基本能做出以下使用規則:對於網絡請求及讀寫大量本地數據等操作,既可以採用Schedulers.newThread()也可以採用Schedulers.io(),但是優先採用Schedulers.io(),對於計算量比較大的,當然是採用Schedulers.computation()。
這樣,我們既能達到較好的性能,又儘可能的減少內存佔用。

內存泄漏

儘管rxjava非常簡單易用,但是隨着訂閱的增多內存開銷也會隨之增大,尤其是在配合使用網絡請求的時候,稍不注意就容易造成內存泄漏。早期我也犯過多次這種錯誤。

當我們不需要的時候,主動取消訂閱。比如在下面的代碼中,我們開啓一個週期任務用來不斷的輸出信息,那麼我們需要在該Activity被銷燬的時候調用mSubscription.unsubscribe()來主動的解除訂閱關係防止內存泄漏。

public class MainActivity extends AppCompatActivity {

    private Subscription mSubscription;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button mBtnTest1 = (Button) findViewById(R.id.btn_test1);

        mBtnTest1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSubscription = startIntervalTask();
            }
        });
    }

    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override

            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //主動解除訂閱關係
        if (mSubscription != null && !mSubscription.isUnsubscribed()) {
            mSubscription.unsubscribe();
        }
    }
}

看完上面簡單的示例,想必你也明白rxjava所造成的內存泄漏往往和組件的生命週期相關。也就是我們要重點關注那些在在組件銷燬之後,訂閱關係卻仍然存在的情況。大部分情況下,當我們的視圖銷燬之後,訂閱關係就沒有必要存在了,所以需要我們主動取消訂閱即可。

存在一種特殊情況:當我們進入某個界面後,往往會發出網絡請求,在返回數據後首先需要緩存數據,然後在更新界面視圖。這種情況下當然不能在視圖銷燬後立刻解除訂閱關係。那麼這裏需要注意的是更新UI之前需要自行判斷當前視圖是否存在,存在則更新,不存在就沒有必要更新了。

在我們的工程中,往往存在很多個視圖(Activity,Fragment等),如果在每個視圖當中都要手動的解除訂閱關係是件很繁瑣的事情。這裏有兩種方式:一是在基類當中,比如BaseActivity,BaseFragment中統一取消訂閱,另外一種方式就是使用RxLifeCycle這個庫。

補充:RxJava中也提供了CompositeSubscription用來批量的管理訂閱關係,其內部通過Set<Subscription>實現,在需要取消多個訂閱關係的時候使用它也是非常方便

使用RxLifecycle

RxLifeCycle可以幫助我們在組件生命週期的某個階段或者指定事件發生時自動取消訂閱,其項目地址在:https://github.com/trello/RxLifecycle,如果你不介意引入新的庫,建議使用。

compile 'com.trello:rxlifecycle:0.3.1'
compile 'com.trello:rxlifecycle-components:0.3.1'

RxLifeCycle主要提供了兩個方法bindToLifecycle()和bindUntilEvent(),分別用來綁定生命週期和事件。

綁定生命週期(bindToLifecycle())

綁定生命週期的做法本質上是通過監聽組件(Activity,Fragment)生命週期的變化來自動解除訂閱關係。用法如下:

//1. 需要繼承對應的RxAppCompatActivity
public class MainActivity extends RxAppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //2. 在onResume中綁定,將在對應的onPause()方法中解除訂閱關係
        //在onStart()中綁定,將在對應的onStop()中解除訂閱關係
        //在onCreate()中綁定,將在對應的onDestory()中解除訂閱關係
        startIntervalTask();
    }

    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).compose(this.<Long>bindToLifecycle()).doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.d("MainActivity", "解除訂閱");
            }
        }).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

}

綁定生命週期的方式適應於在一開始就能夠確定開始點,即能明確的知道訂閱關係發生在哪個階段(onCreate()?onResume(),onStart()),這樣才能在恰當的方式解除訂閱關係。比如在onResume()中聯網獲取用戶數據,那麼需要在onPause()中解除。

綁定事件(bindUntilEvent())

RxLifecycle中將組件生命週期的各個階段轉化爲相對應的事件,因此綁定事件的方式和綁定生命週期的方式並無太大區別。和使用bindToLifecycle()不一樣的是,綁定事件的方式只關心何時解除訂閱關係。因爲在很多情況下,我們所做的操作並不一定是在onResume()開始,在onPause()結束,此時顯然不能用綁定生命週期的方法。來看看綁定事件如何使用:

//1. 需要繼承對應的RxAppCompatActivity
public class MainActivity extends RxAppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button mBtnTest1 = (Button) findViewById(R.id.btn_test1);

              mBtnTest1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //
                startIntervalTask();
    }


    //ActivityEvent提供了相關的事件
    //當pause發生時,自動解除訂閱關係
    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).compose(this.bindUntilEvent(ActivityEvent.PAUSE)).doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.d("MainActivity", "解除訂閱");
            }
        }).subscribe(new Action1<Object>() {
            @Override
            public void call(Object o) {
                Log.d("MainActivity", "start task");
            }
        });
    }

}

總結

這裏介紹了有關rxjava一些實際應用場景。儘管rxjava看起來非常容易 使用,但其內存使用問題需要我們重點關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章