RxJava Один наблюдаемый, несколько подписчиков

RxJava Один наблюдаемый, несколько подписчиков

1. обзор

Поведение по умолчанию нескольких подписчиков не всегда желательно. В этой статье мы расскажем, как изменить это поведение и правильно обрабатывать несколько подписчиков.

Но сначала давайте посмотрим на поведение нескольких подписчиков по умолчанию.

2. Поведение по умолчанию

Допустим, у нас есть следующиеObservable:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        subscriber.onNext(gettingValue(1));
        subscriber.onNext(gettingValue(2));

        subscriber.add(Subscriptions.create(() -> {
            LOGGER.info("Clear resources");
        }));
    });
}

Это испускает два элемента, как толькоSubscribers подписывается.

В нашем примере у нас есть дваSubscribers:

LOGGER.info("Subscribing");

Subscription s1 = obs.subscribe(i -> LOGGER.info("subscriber#1 is printing " + i));
Subscription s2 = obs.subscribe(i -> LOGGER.info("subscriber#2 is printing " + i));

s1.unsubscribe();
s2.unsubscribe();

Представьте, что получение каждого элемента является дорогостоящей операцией - это может включать, например, интенсивные вычисления или открытие URL-соединения.

Для простоты мы просто вернем число:

private static Integer gettingValue(int i) {
    LOGGER.info("Getting " + i);
    return i;
}

Вот вывод:

Subscribing
Getting 1
subscriber#1 is printing 1
Getting 2
subscriber#1 is printing 2
Getting 1
subscriber#2 is printing 1
Getting 2
subscriber#2 is printing 2
Clear resources
Clear resources

Как видимgetting each element as well as clearing the resources is performed twice by default - один раз на каждыйSubscriber. Это не то, что мы хотим. КлассConnectableObservable помогает решить проблему.

3. ConnectableObservableс

КлассConnectableObservable позволяет разделить подписку с несколькими подписчиками и не выполнять базовые операции несколько раз.

Но сначала давайте создадимConnectableObservable.

3.1. publish()с

Методpublish() создаетConnectableObservable изObservable:

ConnectableObservable obs = Observable.create(subscriber -> {
    subscriber.onNext(gettingValue(1));
    subscriber.onNext(gettingValue(2));
    subscriber.add(Subscriptions.create(() -> {
        LOGGER.info("Clear resources");
    }));
}).publish();

Но пока это ничего не делает. Что заставляет его работать, так это методconnect().

3.2. connect()с

Until ConnectableObservable‘s connect() method isn’t called Observable‘s onSubcribe() callback isn’t triggered, даже если есть подписчики.

Продемонстрируем это:

LOGGER.info("Subscribing");
obs.subscribe(i -> LOGGER.info("subscriber #1 is printing " + i));
obs.subscribe(i -> LOGGER.info("subscriber #2 is printing " + i));
Thread.sleep(1000);
LOGGER.info("Connecting");
Subscription s = obs.connect();
s.unsubscribe();

Мы подписываемся и ждем секунду перед подключением. Выход:

Subscribing
Connecting
Getting 1
subscriber #1 is printing 1
subscriber #2 is printing 1
Getting 2
subscriber #1 is printing 2
subscriber #2 is printing 2
Clear resources

Как мы можем видеть:

    • Получение элементов происходит только один раз, как мы хотели

    • Клиринговые ресурсы также встречаются только один раз

    • Получение элементов начинается через секунду после подписки.

    • Подписка больше не вызывает выдачу элементов. Толькоconnect() делает это

Эта задержка может быть полезной - иногда нам нужно предоставить всем подписчикам одинаковую последовательность элементов, даже если один из них подписывается раньше другого.

3.3. Последовательный взгляд на наблюдаемые -connect() послеsubscribe()

Этот вариант использования нельзя продемонстрировать на наших предыдущихObservable, так как он работает холодно, и оба подписчика в любом случае получают всю последовательность элементов.

Вместо этого представьте, что испускаемый элемент не зависит от момента подписки, например, события, генерируемые при щелчках мыши. А теперь представьте, что второйSubscriber подписывается через секунду после первого.

ПервыйSubscriber получит все элементы, испускаемые в этом примере, тогда как второйSubscriber получит только некоторые элементы.

С другой стороны, использование методаconnect() в нужном месте может дать обоим подписчикам одинаковое представление о последовательностиObservable.

Пример HotObservable

Давайте создадим горячийObservable. При щелчке мышью поJFrame он будет излучать элементы.

Каждый элемент будет х-координатой клика:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        frame.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                subscriber.onNext(e.getX());
            }
        });
        subscriber.add(Subscriptions.create(() {
            LOGGER.info("Clear resources");
            for (MouseListener listener : frame.getListeners(MouseListener.class)) {
                frame.removeMouseListener(listener);
            }
        }));
    });
}

Поведение HotObservable по умолчанию

Теперь, если мы подпишем дваSubscribers один за другим со вторым интервалом, запустим программу и начнем щелкать, мы увидим, что первыйSubscriber получит больше элементов:

public static void defaultBehaviour() throws InterruptedException {
    Observable obs = getObservable();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
subscribing #1
subscriber#1 is printing x-coordinate 280
subscriber#1 is printing x-coordinate 242
subscribing #2
subscriber#1 is printing x-coordinate 343
subscriber#2 is printing x-coordinate 343
unsubscribe#1
clearing resources
unsubscribe#2
clearing resources

connect() Черезsubscribe()

Чтобы оба подписчика получали одинаковую последовательность, мы преобразуем этотObservable вConnectableObservable и вызовемconnect() после подписки какSubscribers:

public static void subscribeBeforeConnect() throws InterruptedException {

    ConnectableObservable obs = getObservable().publish();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i ->  LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe connected");
    s.unsubscribe();
}

Теперь они получат ту же последовательность:

subscribing #1
subscribing #2
connecting:
subscriber#1 is printing x-coordinate 317
subscriber#2 is printing x-coordinate 317
subscriber#1 is printing x-coordinate 364
subscriber#2 is printing x-coordinate 364
unsubscribe connected
clearing resources

Итак, дело в том, чтобы дождаться момента, когда все абоненты будут готовы, а затем позвонитьconnect().

В приложении Spring мы можем подписать все компоненты, например, во время запуска приложения и вызватьconnect() вonApplicationEvent().

Но вернемся к нашему примеру; обратите внимание, что все щелчки перед методомconnect() пропущены. Если мы не хотим пропускать элементы, а наоборот обрабатывать их, мы можем поместитьconnect() раньше в код и заставитьObservable генерировать события при отсутствииSubscriber.

3.4. Принудительная подписка при отсутствииSubscriber -connect() доsubscribe()

Чтобы продемонстрировать это, давайте исправим наш пример:

public static void connectBeforeSubscribe() throws InterruptedException {
    ConnectableObservable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish();
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    obs.subscribe((i) -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    obs.subscribe((i) -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    s.unsubscribe();
}

Шаги относительно просты:

  • Сначала мы подключаем

  • Затем ждем одну секунду и подписываемся на первыеSubscriber

  • Наконец, ждем еще секунду и подписываемся на второйSubscriber

Обратите внимание, что мы добавили операторdoOnNext(). Здесь мы могли бы хранить элементы в базе данных, например, но в нашем коде мы просто печатаем «save…».

Если мы запустим код и начнем щелкать, то увидим, что элементы генерируются и обрабатываются сразу после вызоваconnect():

connecting:
saving 306
saving 248
subscribing #1
saving 377
subscriber#1 is printing x-coordinate 377
saving 295
subscriber#1 is printing x-coordinate 295
saving 206
subscriber#1 is printing x-coordinate 206
subscribing #2
saving 347
subscriber#1 is printing x-coordinate 347
subscriber#2 is printing x-coordinate 347
clearing resources

Если бы не было подписчиков, элементы все равно были бы обработаны.

So the connect() method starts emitting and processing elements regardless of whether someone is subscribed, как если бы был искусственныйSubscriber с пустым действием, которое потребляло элементы.

И если какие-то реальныеSubscribers подписываются, этот искусственный посредник просто распространяет на них элементы.

Чтобы отписаться от искусственныхSubscriber, выполняем:

s.unsubscribe();

Куда:

Subscription s = obs.connect();

3.5. autoConnect()с

This method implies that connect() isn’t called before or after subscriptions but automatically when the first Subscriber subscribes.

Используя этот метод, мы не можем вызватьconnect() сами, поскольку возвращаемый объект - это обычныйObservable, у которого нет этого метода, но используется базовыйConnectableObservable:

public static void autoConnectAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
    .doOnNext(x -> LOGGER.info("saving " + x)).publish().autoConnect();

    LOGGER.info("autoconnect()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription s1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription s2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe 1");
    s1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe 2");
    s2.unsubscribe();
}

Обратите внимание, что мы также не можем отказаться от подписки на искусственныеSubscriber. Мы можем отписаться от всех реальныхSubscribers, но искусственныйSubscriber по-прежнему будет обрабатывать события.

Чтобы понять это, давайте посмотрим, что происходит в конце после того, как последний подписчик отказался от подписки:

subscribing #1
saving 296
subscriber#1 is printing x-coordinate 296
saving 329
subscriber#1 is printing x-coordinate 329
subscribing #2
saving 226
subscriber#1 is printing x-coordinate 226
subscriber#2 is printing x-coordinate 226
unsubscribe 1
saving 268
subscriber#2 is printing x-coordinate 268
saving 234
subscriber#2 is printing x-coordinate 234
unsubscribe 2
saving 278
saving 268

Как мы видим, очистки ресурсов не происходит, и сохранение элементов сdoOnNext() продолжается после второго отказа от подписки. Это означает, что искусственныйSubscriber не отказывается от подписки, а продолжает потреблять элементы.

3.6. refCount()с

refCount() похож наautoConnect() в том, что подключение также происходит автоматически, как только первыйSubscriber подписывается.

В отличие отautoconnect() отключение также происходит автоматически, когда последнийSubscriber отписывается:

public static void refCountAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish().refCount();

    LOGGER.info("refcount()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
refcount()
subscribing #1
saving 265
subscriber#1 is printing x-coordinate 265
saving 338
subscriber#1 is printing x-coordinate 338
subscribing #2
saving 203
subscriber#1 is printing x-coordinate 203
subscriber#2 is printing x-coordinate 203
unsubscribe#1
saving 294
subscriber#2 is printing x-coordinate 294
unsubscribe#2
clearing resources

4. Заключение

КлассConnectableObservable помогает без особых усилий обрабатывать несколько подписчиков.

Его методы выглядят схожими, но в значительной степени изменяют поведение подписчиков из-за тонкостей реализации, что означает, что даже порядок методов имеет значение.

Полный исходный код всех примеров, использованных в этой статье, можно найти в папкеGitHub project.