RxJava2 系列-3:使用 Subject

imgtrip-com-2559b11dd61c0946ae8868b27395dcce13569748.jpg

在这篇文章中,我们会先分析一下 RxJava2 中的 Subject ;然后,我们会使用 Subject 制作一个类似于 EventBus 的全局的通信工具。

在了解本篇文章的内容之前,你需要先了解 RxJava2 中的一些基本的用法,比如 Observable 以及背压的概念,你可以参考我的其他两篇文章来获取这部分内容:《RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结》《RxJava2 系列 (2):背压和Flowable》

1、Subject

1.1 Subject 的两个特性

Subject 可以同时代表 Observer 和 Observable,允许从数据源中多次发送结果给多个观察者。除了 onSubscribe(), onNext(), onError() 和 onComplete() 之外,所有的方法都是线程安全的。此外,你还可以使用 toSerialized() 方法,也就是转换成串行的,将这些方法设置成线程安全的。

如果你已经了解了 Observable 和 Observer ,那么也许直接看 Subject 的源码定义会更容易理解:

public abstract class Subject<T> extends Observable<T> implements Observer<T> {

    // ...
}

从上面看出,Subject 同时继承了 Observable 和 Observer 两个接口,说明它既是被观察的对象,同时又是观察对象,也就是可以生产、可以消费、也可以自己生产自己消费。所以,我们可以项下面这样来使用它。这里我们用到的是该接口的一个实现 PublishSubject :

public static void main(String...args) {
    PublishSubject<Integer> subject = PublishSubject.create();
    subject.subscribe(System.out::println);

    Executor executor = Executors.newFixedThreadPool(5);
    Disposable disposable = Observable.range(1, 5).subscribe(i ->
            executor.execute(() -> {
                try {
                    Thread.sleep(i * 200);
                    subject.onNext(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }));
}

根据程序的执行结果,程序在第200, 400, 600, 800, 1000毫秒依次输出了1到5的数字。

在这里,我们用 PublishSubject 创建了一个主题并对其监听,然后在线程当中又通知该主题内容变化,整个过程我们都只操作了 PublishSubject 一个对象。显然,使用 Subject 我们可以达到对一个指定类型的值的结果进行监听的目的——我们把值改变之后对应的逻辑写在 subscribe() 方法中,然后每次调用 onNext() 等方法通知结果之后就可以自动调用 subscribe() 方法进行更新操作。

同时,因为 Subject 实现了 Observer 接口,并且在 Observable 等的 subscribe() 方法中存在一个以 Observer 作为参数的方法(如下),所以,Subject 也是可以作为消费者来对事件进行消费的。

public final void subscribe(Observer<? super T> observer) 

以上就是 Subject 的两个主要的特性。

1.2 Subject 的实现类

在 RxJava2 ,Subject 有几个默认的实现,下面我们对它们之间的区别做简单的说明:

  1. AsyncSubject:只有当 Subject 调用 onComplete 方法时,才会将 Subject 中的最后一个事件传递给所有的 Observer。
  2. BehaviorSubject:该类有创建时需要一个默认参数,该默认参数会在 Subject 未发送过其他的事件时,向注册的 Observer 发送;新注册的 Observer 不会收到之前发送的事件,这点和 PublishSubject 一致。
  3. PublishSubject:不会改变事件的发送顺序;在已经发送了一部分事件之后注册的 Observer 不会收到之前发送的事件。
  4. ReplaySubject:无论什么时候注册 Observer 都可以接收到任何时候通过该 Observable 发射的事件。
  5. UnicastSubject:只允许一个 Observer 进行监听,在该 Observer 注册之前会将发射的所有的事件放进一个队列中,并在 Observer 注册的时候一起通知给它。

对比 PublishSubject 和 ReplaySubject,它们的区别在于新注册的 Observer 是否能够收到在它注册之前发送的事件。这个类似于 EventBus 中的 StickyEvent 即黏性事件,为了说明这一点,我们准备了下面两段代码:

private static void testPublishSubject() throws InterruptedException {
    PublishSubject<Integer> subject = PublishSubject.create();
    subject.subscribe(i -> System.out.print("(1: " + i + ") "));

    Executor executor = Executors.newFixedThreadPool(5);
    Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {
        try {
            Thread.sleep(i * 200);
            subject.onNext(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }));

    Thread.sleep(500);
    subject.subscribe(i -> System.out.print("(2: " + i + ") "));

    Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());
}

private static void testReplaySubject() throws InterruptedException {
    ReplaySubject<Integer> subject = ReplaySubject.create();
    subject.subscribe(i -> System.out.print("(1: " + i + ") "));

    Executor executor = Executors.newFixedThreadPool(5);
    Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {
        try {
            Thread.sleep(i * 200);
            subject.onNext(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }));

    Thread.sleep(500);
    subject.subscribe(i -> System.out.print("(2: " + i + ") "));

    Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());
}

它们的输出结果依次是

PublishSubject的结果:(1: 1) (1: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)
ReplaySubject的结果: (1: 1) (1: 2) (2: 1) (2: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)

从上面的结果对比中,我们可以看出前者与后者的区别在于新注册的 Observer 并没有收到在它注册之前发送的事件。试验的结果与上面的叙述是一致的。

其他的测试代码这不一并给出了,详细的代码可以参考Github - Java Advanced

2、用 RxJava 打造 EventBus

2.1 打造 EventBus

清楚了 Subject 的概念之后,让我们来做一个实践——用 RxJava 打造 EventBus。

我们先考虑用一个全局的 PublishSubject 来解决这个问题,当然,这意味着我们发送的事件不是黏性事件。不过,没关系,只要这种实现方式搞懂了,用 ReplaySubject 做一个发送黏性事件的 EventBus 也非难事。

考虑一下,如果要实现这个功能我们需要做哪些准备:

  1. 我们需要发送事件并能够正确地接收到事件。要实现这个目的并不难,因为 Subject 本身就具有发送和接收两个能力,作为全局的之后就具有了全局的注册和通知的能力。因此,不论你在什么位置发送了事件,任何订阅的地方都能收到该事件。
  2. 首先,我们要在合适的位置对事件进行监听,并在合适的位置取消事件的监听。如果我们没有在适当的时机释放事件,会不会造成内存泄漏呢?这还是有可能的。所以,我们需要对注册监听的观察者进行记录,并提供注册和取消注册的方法,给它们在指定的生命周期中进行调用。

好了,首先是全局的 Subject 的问题,我们可以实现一个静态的或者单例的 Subject。这里我们选择使用后者,所以,我们需要一个单例的方式来使用 Subject:

public class RxBus {

private static volatile RxBus rxBus;

private final Subject<Object> subject = PublishSubject.create().toSerialized();

public static RxBus getRxBus() {
    if (rxBus == null) {
        synchronized (RxBus.class) {
            if(rxBus == null) {
                rxBus = new RxBus();
            }
        }
    }
    return rxBus;
}

}

这里我们应用了 DCL 的单例模式提供一个单例的 RxBus,对应一个唯一的 Subject. 这里我们用到了 Subject 的toSerialized(),我们上面已经提到过它的作用,就是用来保证 onNext() 等方法的线程安全性。

另外,因为 Observalbe 本身是不支持背压的,所以,我们还需要将该 Observable 转换成 Flowable 来实现背压的效果:

public <T> Flowable<T> getObservable(Class<T> type){
    return subject.toFlowable(BackpressureStrategy.BUFFER).ofType(type);
}

这里我们用到的背压的策略是BackpressureStrategy.BUFFER,它会缓存发射结果,直到有消费者订阅了它。而这里的ofType()方法的作用是用来过滤发射的事件的类型,只有指定类型的事件会被发布。

然后,我们需要记录订阅者的信息以便在适当的时机取消订阅,这里我们用一个Map<String, CompositeDisposable>类型的哈希表来解决。这里的CompositeDisposable用来存储 Disposable,从而达到一个订阅者对应多个 Disposable 的目的。CompositeDisposable是一个 Disposable 的容器,声称可以达到 O(1) 的增、删的复杂度。这里的做法目的是使用注册观察之后的 Disposable 的 dispose() 方法来取消订阅。所以,我们可以得到下面的这段代码:

public void addSubscription(Object o, Disposable disposable) {
    String key = o.getClass().getName();
    if (disposableMap.get(key) != null) {
        disposableMap.get(key).add(disposable);
    } else {
        CompositeDisposable disposables = new CompositeDisposable();
        disposables.add(disposable);
        disposableMap.put(key, disposables);
    }
}

public void unSubscribe(Object o) {
    String key = o.getClass().getName();
    if (!disposableMap.containsKey(key)) {
        return;
    }
    if (disposableMap.get(key) != null) {
        disposableMap.get(key).dispose();
    }
    disposableMap.remove(key);
}

最后,对外提供一下 Subject 的订阅和发布方法,整个 EventBus 就制作完成了:

public void post(Object o){
    subject.onNext(o);
}

public <T> Disposable doSubscribe(Class<T> type, Consumer<T> next, Consumer<Throwable> error){
    return getObservable(type)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(next,error);
}

2.2 测试效果

我们只需要在最顶层的 Activity 基类中加入如下的代码。这样,我们就不需要在各个 Activity 中取消注册了。然后,就可以使用这些顶层的方法来进行操作了。

protected void postEvent(Object object) {
    RxBus.getRxBus().post(object);
}

protected <M> void addSubscription(Class<M> eventType, Consumer<M> action) {
    Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, LogUtils::d);
    RxBus.getRxBus().addSubscription(this, disposable);
}

protected <M> void addSubscription(Class<M> eventType, Consumer<M> action, Consumer<Throwable> error) {
    Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, error);
    RxBus.getRxBus().addSubscription(this, disposable);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    RxBus.getRxBus().unSubscribe(this);
}

在第一个 Activity 中我们对指定的类型的结果进行监听:

addSubscription(RxMessage.class, rxMessage -> ToastUtils.makeToast(rxMessage.message));

然后,我们在另一个 Activity 中发布事件:

postEvent(new RxMessage("Hello world!"));

这样当第二个 Activity 中调用指定的发送事件的方法之后,第一个 Activity 就可以接收到发射的事件了。

总结

好了,以上就是 Subject 的使用,如果要用一个词来形容它的话,那么只能是“自给自足”了。就是说,它同时做了 Observable 和 Observer 的工作,既可以发射事件又可以对事件进行消费,可谓身兼数职。它在那种想要对某个值进行监听并处理的情形特别有用。因为它不需要你写多个冗余的类,只要它一个就完成了其他两个类来完成的任务,因而代码更加简洁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 发现 关注 消息 RxSwift入坑解读-你所需要知道的各种概念 沸沸腾关注 2016.11.27 19:11*字...
    枫叶1234阅读 2,713评论 0 2
  • 注:本系列文章主要用于博主个人学习记录,本文末尾附上了一些较好的文章提供学习。转载请附 原文链接RxJava学习系...
    黑丫山上小旋风阅读 2,079评论 1 5
  • 我真的很痛苦 我规划好了一切 都一直事与愿违 我觉得可能你是唯一懂我的人 可是你也不给我机会倾诉 对啊 我们什么关...
    舒科舒科舒科阅读 321评论 0 0
  • 这颗禾树长在我家院子外边,树干有两个人合抱那么大。 自小生长在这颗树下,大禾树留给我许多的记忆。 第一是喜鹊。 禾...
    蹈海阅读 3,543评论 0 2