使用 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 文档。
使用 RxJava 的正确姿势的更多相关文章
- 判断是否为gif/png图片的正确姿势
判断是否为gif/png图片的正确姿势 1.在能取到图片后缀的前提下 1 2 3 4 5 6 7 8 9 //假设这是一个网络获取的URL NSString *path = @"http:/ ...
- 在Linux(ubuntu server)上面安装NodeJS的正确姿势
上一篇文章,我介绍了 在Windows中安装NodeJS的正确姿势,这一篇,我们继续来看一下在Linux上面安装和配置NodeJS. 为了保持一致,这里也列举三个方法 第一个方法:通过官网下载安装 h ...
- 程序员取悦女朋友的正确姿势---Tips(iOS美容篇)
前言 女孩子都喜欢用美图工具进行图片美容,近来无事时,特意为某人写了个自定义图片滤镜生成器,安装到手机即可完成自定义滤镜渲染照片.app独一无二,虽简亦繁. JH定律:魔镜:最漂亮的女人是你老婆魔镜: ...
- ios监听ScrollView/TableView滚动的正确姿势
主要介绍 监测tableView垂直滚动的舒畅姿势 监测scrollView/collectionView横向滚动的正确姿势 1.监测tableView垂直滚动的舒畅姿势 通常我们用KVO或者在scr ...
- 玩转 Ceph 的正确姿势
玩转 Ceph 的正确姿势 本文先介绍 Ceph, 然后会聊到一些正确使用 Ceph 的姿势:在集群规模小的时候,Ceph 怎么玩都没问题:但集群大了(到PB级别),这些准则可是保证集群健康运行的不二 ...
- 解锁redis锁的正确姿势
解锁redis锁的正确姿势 redis是php的好朋友,在php写业务过程中,有时候会使用到锁的概念,同时只能有一个人可以操作某个行为.这个时候我们就要用到锁.锁的方式有好几种,php不能在内存中用锁 ...
- jquery选中radio或checkbox的正确姿势
jquery选中radio或checkbox的正确姿势 Intro 前几天突然遇到一个问题,没有任何征兆的..,jquery 选中radio button单选框时,一直没有办法选中,后来查了许多资料, ...
- 程序员节应该写博客之.NET下使用HTTP请求的正确姿势
程序员节应该写博客之.NET下使用HTTP请求的正确姿势 一.前言 去年9月份的时候我看到过外国朋友关于.NET Framework下HttpClient缺陷的分析后对HttpClient有了一定的了 ...
- 使用 win10 的正确姿势
17年9月初,写了第一篇<使用 win10 的正确姿势>,而现在半年多过去,觉得文章得更新一些了,索性直接来个第二版吧. -----2018.3.24 写 一. 重新定义桌面 我的桌面: ...
随机推荐
- Codeforces Round #232 (Div. 2) C
C. On Number of Decompositions into Multipliers time limit per test 1 second memory limit per test 2 ...
- CF578D. LCS Again
n<=100000个字符的小写字母串,问用前m<=26个小写字母能拼出多少个和原串lcs=n-1的字符串. 首先把字符串划分成若干个连续相同的段,如aaa|bb|c|dd,然后题目即要求从 ...
- Hero HDU4310 贪心
When playing DotA with god-like rivals and pig-like team members, you have to face an embarrassing s ...
- MySQL慢日志切割邮件发送脚本
#!/bin/bashtime=`date -d yesterday +"%Y-%m-%d"`slowlog='/usr/local/percona/data/slow.log'# ...
- 二维数组的查找,刷题成功——剑指Offer
今天又做了一道题目,通过啦,欧耶! https://www.nowcoder.net/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqI ...
- 新浪微博开放平台之OAuth2.0认证
1.先到开放平台创建一个移动应用.获得key和secret,接着到"应用信息"里面填写授权回调页的网址,该网址能够随意,可是必须是能訪问的. 2.通过新浪提供的api:https: ...
- GCC编译uboot出现(.ARM.exidx+0x0): undefined reference to `__aeabi_unwind_cpp_pr0'错误的解决的方法
/opt/arm-2010.09/bin/../lib/gcc/arm-none-linux-gnueabi/4.5.1/armv4t/libgcc.a(_bswapsi2.o):(.ARM.exid ...
- C#之out和ref区别
out与ref的区别总结:1.两者都是通过引用来传递.2.两者都按地址传递的,使用后都将改变原来参数的数值.3.属性不是变量,因此不能作为 out或ref 参数传递.4.若要使用 ref 或 out, ...
- linux查找nginx所在目录
ps -ef |grep nginx
- 【iOS开发-32】iOS程序真机调试须要购买调试证书怎么办?
一.情况 我们在开发iOS程序的时候,一般都是在模拟器上执行查看效果的. 可是,当开完完毕.须要在真机上调试怎么办? 二.官方解决的方法 苹果有为个人和企业开发人员提供调试证书和公布证书.个人版99美 ...