最近在使用 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 的正确姿势的更多相关文章

  1. 判断是否为gif/png图片的正确姿势

    判断是否为gif/png图片的正确姿势 1.在能取到图片后缀的前提下 1 2 3 4 5 6 7 8 9 //假设这是一个网络获取的URL NSString *path = @"http:/ ...

  2. 在Linux(ubuntu server)上面安装NodeJS的正确姿势

    上一篇文章,我介绍了 在Windows中安装NodeJS的正确姿势,这一篇,我们继续来看一下在Linux上面安装和配置NodeJS. 为了保持一致,这里也列举三个方法 第一个方法:通过官网下载安装 h ...

  3. 程序员取悦女朋友的正确姿势---Tips(iOS美容篇)

    前言 女孩子都喜欢用美图工具进行图片美容,近来无事时,特意为某人写了个自定义图片滤镜生成器,安装到手机即可完成自定义滤镜渲染照片.app独一无二,虽简亦繁. JH定律:魔镜:最漂亮的女人是你老婆魔镜: ...

  4. ios监听ScrollView/TableView滚动的正确姿势

    主要介绍 监测tableView垂直滚动的舒畅姿势 监测scrollView/collectionView横向滚动的正确姿势 1.监测tableView垂直滚动的舒畅姿势 通常我们用KVO或者在scr ...

  5. 玩转 Ceph 的正确姿势

    玩转 Ceph 的正确姿势 本文先介绍 Ceph, 然后会聊到一些正确使用 Ceph 的姿势:在集群规模小的时候,Ceph 怎么玩都没问题:但集群大了(到PB级别),这些准则可是保证集群健康运行的不二 ...

  6. 解锁redis锁的正确姿势

    解锁redis锁的正确姿势 redis是php的好朋友,在php写业务过程中,有时候会使用到锁的概念,同时只能有一个人可以操作某个行为.这个时候我们就要用到锁.锁的方式有好几种,php不能在内存中用锁 ...

  7. jquery选中radio或checkbox的正确姿势

    jquery选中radio或checkbox的正确姿势 Intro 前几天突然遇到一个问题,没有任何征兆的..,jquery 选中radio button单选框时,一直没有办法选中,后来查了许多资料, ...

  8. 程序员节应该写博客之.NET下使用HTTP请求的正确姿势

    程序员节应该写博客之.NET下使用HTTP请求的正确姿势 一.前言 去年9月份的时候我看到过外国朋友关于.NET Framework下HttpClient缺陷的分析后对HttpClient有了一定的了 ...

  9. 使用 win10 的正确姿势

    17年9月初,写了第一篇<使用 win10 的正确姿势>,而现在半年多过去,觉得文章得更新一些了,索性直接来个第二版吧. -----2018.3.24 写 一. 重新定义桌面 我的桌面: ...

随机推荐

  1. RMI分布式议程服务学习

    转自:http://6221123.blog.51cto.com/6211123/1112619 这里讲述的是基于JDK1.5的RMI程序搭建,更简单的说是一个 HelloWorld RMI. 1. ...

  2. poj 2135最小费用最大流

    最小费用最大流问题是经济学和管理学中的一类典型问题.在一个网络中每段路径都有"容量"和"费用"两个限制的条件下,此类问题的研究试图寻找出:流量从A到B,如何选择 ...

  3. Codeforces 314B(倍增)

    题意:[a,b]表示将字符串a循环写b遍,[c,d]表示把字符串c循环写d遍,给定a,b,c,d,求一个最大的p,使得[[c,d],p]是[a,b]的子序列(注意不是子串,也就是不要求连续).(b,d ...

  4. 【C语言】模拟实现strcmp函数

    //模拟实现strcmp函数 //str1>str2,返回1 //str1=str2,返回0 //str1<str2,返回-1 #include <stdio.h> #incl ...

  5. springMVC和ckeditor图片上传

    springMVC和ckeditor图片上传 http://blog.csdn.net/liuchangqing123/article/details/45270977 修正一下路径问题: packa ...

  6. 2015 Multi-University Training Contest 2 1004 Delicious Apples(DP)

    pid=5303">题目链接 题意:长度为l 的环,有n棵果树,背包容量为k,告诉你k棵苹果树的id.以及每棵树上结的果子数.背包一旦装满要返回起点(id==0) 清空,问你至少走多少 ...

  7. 11082 完全二叉树的种类 O(n) 卡特兰数

    11082 完全二叉树的种类 时间限制:800MS  内存限制:1000K提交次数:0 通过次数:0 题型: 编程题   语言: G++;GCC;VC Description 构造n个(2<=n ...

  8. linux /proc/stat 文件说明

    /proc/stat 文件内容 # cat /proc/stat cpu 1411 1322 3070 1193539 2790 0 268 0 0 0 cpu0 472 658 787 297933 ...

  9. BestCoder Round #61 (div.2) C.Subtrees dfs

    Subtrees   问题描述 一棵有N个节点的完全二叉树,问有多少种子树所包含的节点数量不同. 输入描述 输入有多组数据,不超过1000组. 每组数据输入一行包含一个整数N.(1\leq N\leq ...

  10. 关于三星手机调用相机返回后activity被回收的问题

    今天遇到个问题很蛋疼啊,别的手机没问题,唯独三星机型的手机跳转到相机之后,回来activity没了.这个或许是三星内部回收机制的关系,因为相机打开之后消耗会比较大, 所以后面的进程都给暂时回收掉了,加 ...