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) {
                    //處理錯誤
                }
            });
}

開始時,我們大部分人會寫出類似以上的代碼。當然,這實現了我們想要的邏輯,但當你仔細思考的時候,會發現幾個問題:回調嵌套看起來令人疑惑。由于我們在大多數情況下是線性思維,那么此時當你看到

onNext(Result<Token> tokenResult)

中又去嵌套處理獲取用戶信息的接口你的思維不得不跳躍一下。登錄功能的異常處理點被分隔了,使我們不得不寫出冗余的代碼。多次線程開銷好像可以被進一步優化。

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

 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。
場景二:接口合并(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可以幫助我們在組件生命周期的某個階段或者指定事件發生時自動取消訂閱,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");
    }
});
}

引入RxBinding
所謂的RxBinding是用來為界面元素綁定事件的,比如為Buttong設置點擊事件等。瀏覽其源碼,不難發現其實現原理也是通過包裝原有事件實現的。
為什么要引入RxBinding?使用RxJava一定要引入RxBinding么?
首先很確定的說使用RxJava不要求你一定要使用RxBinding,大部分情況下沒必要用。這里之所以要談RxBinding一方面是完善我們的知識體系,看看響應式編程的思想是如何應用在Android界面元素上,另一方面是看看RxBinding能夠有效的解決什么問題?
RxBinding提供了和RxJava一致的api體驗,更重要的是它更好的符合RxJava做法:通過將事件轉化為Observable對象,最終可以利用RxJava一系列操作符對其處理,關于如何使用RxBinding,直接參見RxBinding項目說明即可:https://github.com/JakeWharton/RxBinding

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容