使用 RxJava 的正确姿势
最近在使用 RxJava 时遇到了一些比较诡异的问题,排查的过程中做了点研究,发现平时大家的用法多多少少都有些问题。有些地方存在隐患,有些用法不够简练,我把这些问题简单做一下分类和总结,供大家参考。
数据源类型选择
RxJava2 中的数据源类型有5种,分别是 Observable,Flowable,Single,Maybe 和 Completable,它们的区别如下,看到有些同学只用 Observable,其实这并不是个很好的习惯。
类别 | 特点 |
---|---|
Observable | 多个数据,不支持背压 |
Flowable | 多个数据,支持背压 |
Single | 一个数据 |
Maybe | 一个或没有数据 |
Completable | 没有数据,只有结束信号 |
我们举数据库操作的例子,例如在 Repository 中有一个方法,根据一本书的序列号去查询这本书的名称: |
private String getBookName(int serialNumber) throws InterruptedException {
Thread.sleep(1000); // 模拟耗时操作
return "Pride And Prejudice";
}
尝试把它转化成一个 Rx 风格的方法,如果使用 Observable 是这样的:
public Observable<String> getBookNameObservable(int serialNumber) {
return Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) throws Exception {
String name = getBookName(serialNumber);
if (name != null) {
emitter.onNext(name);
emitter.onComplete();
} else {
emitter.onError(new NullPointerException("Not Found!"));
}
}
});
}
访问数据库确定有且只有一次返回结果,所以可以改成Single:
public Single<String> getBookNameSingle(int serialNumber) {
return Single.create(new SingleOnSubscribe<String>() {
@Override
public void subscribe(SingleEmitter<String> emitter) throws Exception {
String name = getBookName(serialNumber);
if (name != null) {
emitter.onSuccess(name);
} else {
emitter.onError(new NullPointerException("Not Found!"));
}
}
});
}
这样写的好处是:
- 语义明确,看到是 Single 类型就知道只有一个返回值
- 防止遗漏 onComplete 调用,保证流正常结束
- 写起来更简洁一些,对应的订阅者也是
- Single 和 Completable 是使用最广泛的数据源,它们的语义简单,操作符更丰富
我们项目中会有这样一些常用的场景:
- 持续监听某个组件状态(PUSH类型),监听航线执行状态,监听用户的点击事件等,这种情况会连续返回数据,一般使用 Observable 就可以了,如果有特殊情况需要支持背压可以考虑 Flowable,不过我目前还没有遇到,如果推送频率过高,使用 throttle 操作符过滤一下即可;
- 主动获取某个传感器的某个状态值(GET类型),访问数据库查找数据,发起网络请求等,获得一个返回值或者失败信息,应该使用 Single;
- 设置某个组件的状态、开关(SET类型),控制某个组件执行任务(ACTION类型),数据库增删改等,不需要返回值,只需知道是否执行完成或者失败,应该使用Completable;
- 工作流比较复杂,需要一步一步完成任务,中间还需要线程转换等,这种情况大多只需要传递上一个操作完成的结果或者信号,视情况使用 Single 或者 Completable 就足够了。
装配过程
Single.just 引发的血案
先列举我遇到过的一个问题,和调用时序相关,类似这样:
public class SingleJustTest {
private static int index = 0;
public static void main(String[] args) {
Single<Integer> single = Single.just(index);
index++;
single.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {
System.out.println("index from Single is : " + integer);
System.out.println("real index is : " + index);
}
});
}
}
原以为 integer 这个值应该和 index 相等,结果却出乎意料:
index from Single is : 0
real index is : 1
这是为什么呢?看一下 Single.just 的实现:
public static <T> Single<T> just(final T item) {
ObjectHelper.requireNonNull(item, "value is null");
return RxJavaPlugins.onAssembly(new SingleJust<T>(item));
}
SingleJust 这个类也很简单:
public final class SingleJust<T> extends Single<T> {
final T value;
public SingleJust(T value) {
this.value = value;
}
@Override
protected void subscribeActual(SingleObserver<? super T> observer) {
observer.onSubscribe(Disposables.disposed());
observer.onSuccess(value);
}
}
这个对象保存了 Single.just 方法传入的参数,并且在订阅时传给下游,所以上面这个 integer 的值并不会随着 index 值变化。
要想达到目的,有两种解决方法,使用 Single.fromCallable 或者用 Single.defer:
public class SingleJustTest {
private static int index = 0;
public static void main(String[] args) {
Single<Integer> single = Single.fromCallable(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return index;
}
});
index++;
single.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {
System.out.println("index from Single is : " + integer);
System.out.println("real index is : " + index);
}
});
}
}
public class SingleJustTest {
private static int index = 0;
public static void main(String[] args) {
Single<Integer> single = Single.defer(new Callable<SingleSource<? extends Integer>>() {
@Override
public SingleSource<? extends Integer> call() throws Exception {
return Single.just(index);
}
});
index++;
single.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {
System.out.println("index from Single is : " + integer);
System.out.println("real index is : " + index);
}
});
}
}
这两种方式都能得到正确的结果:
index from Single is : 1
real index is : 1
原理也很简单,Single.fromCallable 是通过方法调用的方式,订阅后执行传入的 Callable 取得 index 的值; Single.defer 是在订阅时才开始生成真正的数据源,所以也没有问题。
强调一下,just 操作符(包括 Single.just / Observalbe.just / Flowable.just / Maybe.just)虽然使用方便,但是存在上述问题,除非传入的参数是常量,否则应当避免使用。
正确创建数据源
下面是我们项目中经常看到的用法:
public class RxJavaTest {
public static void main(String[] args) {
getBookNameObservable(123)
.subscribeOn(Schedulers.io())
// 非 Android 环境演示线程调度需要使用 blockingSubscribe 方式订阅,否则当主线程执行完后就退出了
.blockingSubscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {
log("onSubscribe");
}
@Override
public void onNext(String s) {
log("onNext : s = " + s);
}
@Override
public void onError(Throwable e) {
log("onError " + e.getMessage());
}
@Override
public void onComplete() {
log("onComplete");
}
});
}
private static Observable<String> getBookNameObservable(int serialNumber) {
//先执行一系列运算,最后返回数据源
String bookName = getBookNameFromDB(serialNumber);
return Observable.fromCallable(new Callable<String>() {
@Override
public String call() throws Exception {
log("call");
return bookName;
}
});
}
private static String getBookNameFromDB(int serialNumber) {
try {
log("getBookNameFromDB");
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Pride And Prejudice";
}
private static void log(String msg) {
System.out.println(msg + ", in thread: " + Thread.currentThread().getName());
}
}
注意 getBookNameObservable 这个方法,先去查询数据库获得 bookName,然后返回了一个 Observable,我们执行一下:
getBookNameFromDB, in thread: main
onSubscribe, in thread: main
call, in thread: RxCachedThreadScheduler-1
onNext : s = Pride And Prejudice, in thread: main
onComplete, in thread: main
看起来好像没有什么问题,也得到了正确的结果,但是注意调用的顺序和每个方法运行的线程:getBookNameFromDB 这个方法是耗时操作,我们本意是把它放到 io 线程去执行,但是它却在 main 线程执行了,而且调用时机要早于 onSubscribe 方法。
为什么会这样呢?其实很好理解,需要分清楚上面这段代码中哪些是在装配过程中调用,哪些是在订阅之后调用的。getBookNameObservable 这个方法是在装配过程中被调用,应该返回一个未订阅的数据源,当用户订阅时才开始执行 getBookNameFromDB 获取并发射数据,但现在这种写法,getBookNameObservable 方法中 return 之前的语句都会在装配过程中被调用,这就能解释上面的 log 信息了。
总结一下这种写法会带来的问题:
- 本该在流中执行的代码会在订阅之前执行,产生时序问题
- 如果装配完成后没有马上订阅,那么订阅时接收到的数据可能已经与真实数据不同
- 线程调度不起作用
- 一旦这部分代码抛出了异常,订阅者无法在 onError 中接收到错误信息
所以虽然很多地方是这么用的,结果也没有影响,但这并不表示这种用法就是正确的,只是没有暴露问题而已。强调一下,实现返回数据源方法时,必须第一行就是return语句,所有的操作应当包含在操作流中,这样做能避免很多问题。
简便操作符
此处是要讲一些常用的操作符,能让我们把非 RxJava 代码快速转换过来。
just
前面已经提过,虽然好用,但是只有参数为常量时才能使用。
fromCallable
前面也有演示,一般 Single 操作符使用这个会很方便,直接在 call 方法中返回 onSuccess 的值即可。Observable 和 Flowable 在只有一次 onNext 的情况下也可以用。
fromAction
Completable 专用,相当于执行完成 Action 中的代码并且调用 onComplete,很方便。
flatMap
这个大家用的比较多了,多用于数据源类型转换。
andThen
Completable 因为没有返回值,所以也就没有 flatMap 操作符,可以用 andThen 来连接下一个数据源。
doOnSuccess / doAfterSuccess / doOnNext / doOnComplete
在特定位置插入操作,有些可以对发射的值做进一步处理。
暂时先讲这几个,RxJava 的操作符很丰富,不要只会用 create 和 just,可以多了解一下,有需要去查 官方 API 文档。