Вышло следующее поколение RxJava; RxJava 2. Если вы работаете над проектом, который в настоящее время использует RxJava 1, теперь у вас есть возможность перейти на новую версию. Но следует ли вам немедленно начать миграцию или лучше подождать и взять что-то из бэклога вашего проекта?

Чтобы принять решение, вам нужно подумать о рентабельности инвестиций (ROI); если время, потраченное на портирование, окупится в краткосрочной и долгосрочной перспективе.

Преимущества миграции

Совместимость с реактивными потоками

Одним из архитектурных изменений в RxJava 2 является поддержка Reactive Streams. Для этого RxJava пришлось переписать с нуля.

Reactive Streams обеспечивает общее понимание и API того, как должна работать реактивная библиотека.

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

Примером может служить библиотека Reactor 3, очень похожая на RxJava. Если вы Android-разработчик, вы вряд ли сталкивались с ним, потому что он работает только на Java 8+.

Тем не менее, преобразовать реактивную последовательность между этими двумя библиотеками так же просто:

Зеленый код — RxJava 2, красный — Reactor 3.

Жаль, что мы не можем использовать его на Android, так как Reactor 3 имеет повышение производительности от 10% до 50% по сравнению с RxJava 2.

Насколько мне известно, RxJava 2 в настоящее время является единственной библиотекой, которая работает на Android и поддерживает Reactive Streams. Это означает, что на данный момент ваш ROI от использования RxJava 2 для Reactive Streams довольно низок.

Противодавление — наблюдаемое/текучее

В RxJava 2 появился новый реактивный тип Flowable, который очень похож на Observable из RxJava 1, но с ключевым отличием; в RxJava 2 Flowable — это тип, поддерживающий противодавление.

Поясним, что означает «поддерживающее противодавление».

Люди, которые плохо знакомы с RxJava 2, часто слышат «Flowable поддерживает противодавление» и спрашивают: «Означает ли поддержка противодавления, что у меня никогда не будет MissingBackpressureException?». Ответ - нет.

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

Текучий

В случае с Flowables вы должны указать, как он будет себя вести — для этого у вас есть несколько стратегий. Это включает в себя:

  • Буфер — те события, которые потребитель не может начать обрабатывать немедленно, буферизуются и воспроизводятся, когда потребитель заканчивает работу с предыдущими событиями.
  • Отбросить — когда потребитель слишком медленный, он будет игнорировать все элементы, и как только он будет готов к следующему элементу, он возьмет самый последний созданный.
  • Ошибка — потребитель выдаст MissingBackpressureException

С практической точки зрения, вы, вероятно, столкнетесь с противодействием в наших приложениях? Мне тоже было интересно, поэтому я написал Flowable, в котором использовался датчик акселерометра. Показания, которые я напечатал на экране:

[Акселерометр с использованием Flowables]

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

Наблюдаемый

Observable не поддерживает противодавление. Это означает, что они никогда не будут генерировать исключение MissingBackpressureException. Если потребитель не может немедленно обработать события, они будут помещены в буфер и воспроизведены позже.

Итак, когда вы должны использовать Flowable и когда вы должны использовать Observable?

Я использую Flowable, когда особенно вероятны проблемы с обратным давлением, и неправильное обращение с ними может привести к проблемам. В случае с акселерометром я бы все равно использовал Flowable — если бы я не торопился с показаниями, а не просто распечатывал их, возникало бы противодавление.

Observable следует использовать, когда проблема маловероятна. Если пользователь нажимает какую-то кнопку слишком много раз, мы можем жить с буферизацией.

Одно предостережение: если вы будете использовать Observable в месте, где буферизуется огромное количество событий, все приложение рухнет.

Мое эмпирическое правило: при создании Observable подумайте, ведет ли себя источник событий так:

  • Пользователь нажимает на кнопку — максимум несколько событий в секунду — используйте Observable
  • Датчик освещенности или акселерометр — десятки событий в секунду — используйте Flowable

Помните, что даже если вам потребуется очень много времени для обработки нажатия кнопки, у вас возникнут проблемы с обратным давлением!

Представление

Производительность RxJava 2 лучше, чем в предыдущей версии.

Всегда приятно иметь более производительную библиотеку. Однако вы получите заметную отдачу только в том случае, если узким местом вашей производительности является RxJava.

Сколько раз вы смотрели на код и думали: «Скорость этого flatMap чертовски низкая!»?

В приложении для Android вычисления обычно не являются проблемой. В большинстве случаев узким местом является рендеринг пользовательского интерфейса.

Мы не теряем кадры, потому что слишком много всего происходит в планировщике вычислений. Мы теряем кадры, потому что макет слишком сложный, мы забыли поставить доступ к файлам в фоновом потоке или создаем растровые изображения в onDraw.

Проблемы миграции

До свидания, нули

В последние годы все больше и больше растет антагонизм против нулей. Это не должно быть сюрпризом — даже изобретатель Null Reference назвал это «ошибкой на миллиард долларов».

В RxJava 1 вы могли обойтись без нулевых значений. В новой версии вообще нельзя будет использовать null: использование null в качестве значения в потоке запрещено. Если вы используете нули в своем проекте, будьте готовы к большому количеству переделок.

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

Лимит ловкости

Вы когда-нибудь пытались объяснить функциональному программисту, что на Android существует ограничение на количество функций, которые мы можем написать? Попробуй! Их реакция бесценна!

К сожалению, у нас есть этот барьер в 65000 методов, который мы не хотим превышать. RxJava 1 имеет около 5500 методов, что довольно много. Теперь RxJava 2 имеет более 9200 методов. Увеличение количества методов на 4000 может быть приемлемым из-за функциональности, которую они привносят, но поскольку вполне вероятно, что вы будете переносить свое приложение шаг за шагом, у вас будут обе библиотеки одновременно во время миграции.

Всего это почти 15000 методов, что составляет 22% от лимита Dex!

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

Вы уже превысили лимит? Если да, то для вас это не проблема.

Но если вы почти достигли предела Dex, миграция может вас опрокинуть.

Написание операторов

Существующего набора операторов в RxJava не всегда может быть достаточно. Вам может понадобиться какое-то пользовательское поведение. В таких случаях у вас может возникнуть соблазн написать собственный оператор.

Теперь написать оператор специально для 2.x в 10 раз сложнее, чем для 1.x.

- Давид Карнок

В RxJava 1 это было непросто. Нужно было подумать о многопоточном доступе и поддержке обратного давления.

В RxJava 2 все становится серьезно. Во-первых, изменился способ создания операторов; раньше вы бы сделали это с помощью печально известного метода create. Теперь в RxJava 2, помимо многопоточного доступа, обратного давления, отмены и многих других, вы можете рассмотреть возможность использования функций 4-го поколения, таких как Operator Fusion, которые повысят производительность вашего оператора, но в то же время уравняют его. более сложный.

Стоит ли писать собственные операторы?

Если вы не пишете оператор для включения в RxJava 2 или другие реактивные библиотеки, я бы посоветовал вам найти другое решение.

Во-первых, проверьте, может ли она быть действительно удовлетворена комбинацией существующих операторов. Тогда вы можете подумать о написании трансформера, это может быть достаточно хорошим решением для вас. Они не будут такими настраиваемыми, как операторы, но их намного проще написать. Другим преимуществом операторов может быть производительность, но опять же, на Android есть большая вероятность, что она будет потрачена впустую, поскольку нашим узким местом обычно является пользовательский интерфейс.

Если вы все еще думаете о его написании, просто сравните один из самых простых (map) операторов с одним из самых сложных (flatMap) и подумайте, готовы ли вы принять вызов.

Вывод

Итак, это основные аргументы за и против перехода на RxJava 2. Всегда вам решать, стоит ли требуемая работа по переходу.

В настоящее время совершенно нормально оставаться с RxJava 1, пока она все еще поддерживается; поддержка этого. В ближайшем будущем, когда RxJava 1 устареет, у вас появится гораздо более веский аргумент для перехода на следующую версию.

Если ваш проект будет длиться более одного года, вам следует пересмотреть вопрос о миграции, в противном случае лучшим вариантом может быть сохранение RxJava 1.

Если вам интересно, как вы могли бы подойти к миграции, следите за обновлениями для следующего поста.