Карта города и справочник предприятий
87,21
рейтинг
1 июля 2014 в 10:22

Разработка → Реактивное программирование под Android

Отказоустойчивость, отзывчивость, ориентированность на события и масштабируемость — четыре принципа нынче популярного реактивного программирования. Именно следуя им создаётся backend больших систем с одновременной поддержкой десятков тысяч соединений.

Отзывчивость, простота, гибкость и расширяемость кода — принципы, которые можно закрепить за реактивным UI.

Наверняка, если совместить реактивные backend и UI, то можно получить качественный продукт. Именно его мы и попытались сделать, разрабатывая 2GIS Dialer — звонилки, которая работает через API и при этом должна оставаться быстрой и удобной.




Зачем нам реактивное программирование


Рассмотрим пример:

requestDataTask = new AsyncTask<Void, Void, JSONObject>() {
            @Override
            protected JSONObject doInBackground(Void... params) {
                final String requestResult = apiService.getData();
                final JSONObject json = JsonUtils.parse(requestResult);
                lruCache.cacheJson(json);
                return json;
            }
        };

Тут всё просто, мы создаем AsyncTask, в котором:

  1. Делаем запрос к API 2ГИС.
  2. СоздаемJSONObject на основе результата запроса.
  3. Кэшируем JSONObject.
  4. Возвращаем JSONObject.

Подобный код встречается во многих проектах, он понятен, а миллионы леммингов не могут ошибаться. Но давайте копнём чуть глубже:

  1. Что делать, если где-то во время выполнения выпал Exception?
  2. doInBackground(Void...) выполняется в отдельном потоке, как нам сказать пользователю об ошибке в UI? Заводить поля для Exception?
  3. А что возвращать, если не прошел запрос? null?
  4. А если json не валидный?
  5. Что стоит делать, если не удалось кэшировать объект?

И ведь это не самый сложный пример. Представьте, что вам надо сделать ещё один запрос на основе результатов предыдущего. На AsyncTask’ах это будет callback-hell, который, как минимум, будет неустойчив к падениям, ошибкам и т.д.

Вопросов больше, чем ответов. О недостатках AsyncTask’ов можно написать целую статью, серьезно. Но есть ли варианты лучше?

Фреймворк RxJava


Оглядываясь на принципы реактивного программирования мы начали искать решение, которое обеспечит:

  • отсутствие зависаний и тормозов;
  • масштабируемость на ресурсы смартфона;
  • отсутствие крэшей;
  • ориентированность на события.

Таковым стала RxJava от ребят из Netflix — reactive extension, идея (но не реализация) которого перекочевала из reactive extension for c#.

В RxJava всё крутится вокруг Observable. Observable — это как потоки данных (ещё их можно рассматривать как монады), которые могут каким-либо образом получать и отдавать эти самые данные. Над Observable’ами можно применять операции, такие как flatmap, filter, zip, merge, cast и т.д.

Простой пример:

//Observable, который последовательно будет давать нам элементы из Iterable
final Subscription subscription = Observable.from(Arrays.asList(1, 2, 3, 4, 5))
        .subscribeOn(Schedulers.newThread()) //отдаем новый тред для работы в background
        .observeOn(AndroidSchedulers.mainThread()) //говорим, что обсервить хотим в main thread
        .subscribe(new Action1<Integer>() {
            @Override
            public void call(Integer integer) {
                //do something with result
            }
        });

Мы создаем Observable, который поочередно отдает нам числа из Iterable. Указываем, что генерация и передача данных будет происходить в отдельном треде, а обработка результата — в main thread. Подписываемся на него, и в методе подписчика производим любые манипуляции с каждым следующим результатом.

Можно сделать этот пример более интересным:

//Observable, который последовательно будет давать нам элементы из Iterable
final Subscription subscription = Observable.from(Arrays.asList(1, 2, 3, 4, 5)).
                //оператор фильтрации для отсеивания ненужных результатов
                filter(new Func1<Integer, Boolean>() {
                    @Override
                    public Boolean call(Integer integer) {
                        return integer % 2 == 0; //выражение верно только для четных чисел
                    }
                })
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        //do something with ONLY EVEN result
                    }
                });

Теперь, указав оператор filter, мы можем обрабатывать только чётные значения.

Как используют RxJava


Вернёмся к нашему первому AsyncTask и посмотрим, как бы мы решили задачу с помощью реактивного программирования.
Для начала создадим Observable с запросом:

//Observable, действия которого основанны на переданной ему Observable.OnSubscribe<String>
Observable.create(new Observable.OnSubscribe<String>() {
              @Override
              public void call(Subscriber<? super String> subscriber) {
                  //сообщить сабскрайберу о том, что есть новые данные
                  subscriber.onNext(apiService.getData());
                  //А теперь сообщаем о том, что мы закончили и данных больше нет
                  subscriber.onCompleted();
              }
          });

Тут мы создаем Observable и специфицируем его поведение. Делаем запрос и отдаем результат в onNext(...), после чего говорим Subscriber’у, что мы закончили, вызвав onCompleted().

С этим понятно: мы создали Observalble, который отвечает только за получение объекта String с API. SRP в чистом виде.
Что, если запрос не прошёл по каким-то причинам? Тогда мы можем позвать у Observable метод retry(...), который будет повторять этот самый Observable n раз, пока он не завершится успешно (читай, без Exception). Кроме того, мы можем отдать Observable’у другой Observable, если даже retry() не помог. Если backend написан криво, то лучше бы нам закрывать соединение по таймауту. И у нас есть метод timeout(...) на этот случай. Всё вместе это выглядело бы так:

final Subscription subscription =
          Observable.create(new Observable.OnSubscribe<String>() {
              @Override
              public void call(Subscriber<? super String> subscriber) {
                  subscriber.onNext(apiService.getData());
                  subscriber.onCompleted();
              }
          })
          .timeout(5, TimeUnit.SECONDS) //указываем таймаут операции в секундах
          .retry(3) // делаем 3 попытки запроса
          //назначаем обработчик в случае, если все таки мы не спасли положение
          .onErrorResumeNext(new Func1<Throwable, Observable<? extends String>>() {
              @Override
              public Observable<? extends String> call(Throwable throwable) {
                  //return new observable here, that can rescure us from error
              }
          });

И немного рефакторинга:

final Subscription subscription =
          createApiRequestObservable() //создали Observable с запросом
          .timeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) //поставили таймаут
          .retry(RETRY_COUNT_FOR_REQUEST) //поставили кол-во повторов
          .onErrorResumeNext(createRequestErrorHandler()); // назначили обработчик ошибки

Теперь займемся созданием json. Для этого результат нашего первого Observable (а там String) надо преобразовать. Используем map(...), и, если что-то вдруг пойдет не так, вернем другой, нужный нам в случае неудачи, json с помощью onErrorReturn(...).
Вот так:

final Subscription subscription =
          createApiRequestObservable()
          .timeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)
          .retry(RETRY_COUNT_FOR_REQUEST)
          .onErrorResumeNext(createRequestErrorHandler())
          //модифицируем Observable, чтобы тот преобразовывал String в JSONObject
          .map(new Func1<String, JSONObject>() {
              @Override
              public JSONObject call(String s) {
                  return JsonUtils.parse(s);
              }
          })
          //снова ставим обработчик ошибки
          //и вернем предустановленный "ошибочный" json
          .onErrorReturn(new Func1<Throwable, JSONObject>() {
              @Override
              public JSONObject call(Throwable throwable) {
                  return jsonObjectForErrors;
              }
          });

Ок, с json разобрались. Осталось кэширование. Кэширование: это не преобразование результата, а действие над ним. Для этого у Observable есть методы doOnNext(...), doOnEach(...) и т.д. Получается как-то так:

final Subscription subscription =
          createApiRequestObservable()
          .timeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)
          .retry(RETRY_COUNT_FOR_REQUEST)
          .onErrorResumeNext(createRequestErrorHandler())
          //модифицируем Observable, чтобы тот преобразовывал String в JSONObject
          .map(new Func1<String, JSONObject>() {
              @Override
              public JSONObject call(String s) {
                  return JsonUtils.parse(s);
              }
          })
          //снова ставим обработчик ошибки
          //и вернем предустановленный "ошибочный" json
          .onErrorReturn(new Func1<Throwable, JSONObject>() {
              @Override
              public JSONObject call(Throwable throwable) {
                  return jsonObjectForErrors;
              }
          })
          //процедура, вызывающаяся при каждом onNext(..) от Observable
          .doOnNext(new Action1<JSONObject>() {
              @Override
              public void call(JSONObject jsonObject) {
                  lruCache.cacheJson(jsonObject);
              }
          });

Снова немного отрефакторим код:

final Subscription subscription =
          createApiRequestObservable() //создали Observable с запросом
          .timeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) //поставили таймаут
          .retry(RETRY_COUNT_FOR_REQUEST) //поставили кол-во повторов
          .onErrorResumeNext(createRequestErrorHandler()) // назначили обработчик ошибки
          .map(createJsonMapOperator()) //модифицировали Observable, чтобы получать JSONObject
          .onErrorReturn(createJsonErrorHandler()) //возвращаем в случае ошибки то, что ожидаем
          .doOnNext(createCacheOperation()); //кэшируем JSONObject

Мы почти закончили. Как в самом первом примере с RxJava, добавим обработчик результата и укажем треды, в которых надо исполняться.
Финальная версия:

final Subscription subscription =
          createApiRequestObservable() //создали Observable с запросом
          .timeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) //поставили таймаут
          .retry(RETRY_COUNT_FOR_REQUEST) //поставили кол-во повторов
          .onErrorResumeNext(createRequestErrorHandler()) // назначили обработчик ошибки
          .map(createJsonMapOperator()) //модифицировали Observable, чтобы получать JSONObject
          .onErrorReturn(createJsonErrorHandler()) //возвращаем в случае ошибки то, что ожидаем
          .doOnNext(createCacheOperation()); //кэшируем JSONObject
          .subscribeOn(Schedulers.newThread()) //делаем запрос, преобразование, кэширование в отдельном потоке
          .observeOn(AndroidSchedulers.mainThread()) // обработка результата - в main thread
          .subscribe(subscriber); //обработчик результата

Давайте посмотрим, чего мы тут добились:

  1. Принцип отказоустойчивости в действии: результат выполнения всех операций всегда предсказуем. Мы знаем обо всех ошибках и потенциально проблемных местах, которые могут возникнуть в коде, и уже обработали их. Никаких исключений не будет.
  2. Принцип отзывчивости в действии: соединение с базой или сервером не зависнет благодаря таймауту, попытается сам восстановиться при ошибке и, что тоже важно, вернет результат сразу, до кэширования. А кэширование в doOnNext выполнится параллельно обработке результата.
  3. Принцип ориентированности на события в действии: по ходу выполнения запроса и парсинга, мы всегда реагируем на события — события успешного/неуспешного завершения запроса, событие окончания парсинга json (2 реакции: обработка в UI и обработка в бэкграунд трэде для кэширования) и т.д. Кроме того, можно несколько раз подписываться на один Observable и держать в консистентном состоянии всю систему.
  4. Код легко расширяем и почти не требует изменений. Если нам необходимо сделать логирование ошибки или сохранение стэктрейс, можно добавить метод doOnError(Throwable thr). Хотите отфильтровать результаты — добавьте оператор filter и реализуйте его поведение.

Как и недостатки AsyncTask’ов, преимущества этого подхода, на мой взгляд, можно перечислять очень долго. Последний из принципов реактивного программирования, принцип масштабируемости, продемонстрируем ниже.

RxJava в 2GIS Dialer


Живой пример:

//создаем новый Observable путем комбинирования четырех других
final Observable<AggregatedContactData> obs = Observable.combineLatest(
                  createContactsObservable(context), //Observable для получения контактов из базы
                  createPhonesObservable(context), //Observable для получения всех телефонов контактов
                  createAccountsObservable(context), //Observable для полуения аккаунтов и контактов по ним
                  createJobsObservable(context), //Observable для получения мест работы контактов
                  contactsInfoCombinator() //функция комбинироваия результатов всех Observable выше
          ).onErrorReturn(createEmptyObservable()).cache() //обработчик ошибки и оператор кэширования
          .subscribeOn(Schedulers.executor(ThreadPoolHolder.getGeneralExecutor())) //для выполнения такой задачи потребуется тред пул
          .observeOn(AndroidSchedulers.mainThread()); // обработка данных как всегда - в main thread


  1. Тут происходит сразу много интересного и посложнее описанного выше:
    Первое, что бросается в глаза, это Observable.combineLatest(...). Этот оператор ждет onNext(...) от всех переданных ему Observable’ов и применяет функцию комбинирования сразу ко всем результатам. Может показаться сложным, но картинка из вики RxJava сделает всё понятнее. Самое важное тут, что каждый из Observable, переданных в Observable.combineLatest(...) — это CursorObservable, который передает в свой onNext(...) новый курсор, как только он меняется в базе данных. Таким образом, на любое обновление любого из четырех курсоров выполняется функция комбинирования, что позволяет всегда поставлять самые свежие данные. Это и есть принцип ориентированности на события.
  2. Если что-то пошло не так, то мы исходя из своих нужд возвращаем требуемое. В данном случае Collections.emptyList();
  3. Оператор cache() очень полезен, если на этот Observable могут быть подписаны сразу несколько Subscribers. Если этот оператор применен к Observable, то новый его подписчик мгновенно получает данные, при условии, что эти данные уже были посчитаны для подписавшихся ранее. Таким образом, все желающие имеет актуальные одинаковые данные.
  4. А вот тут видно принцип масштабируемости: в subscribeOn(...) я отдаю тред пул на 4 треда, чтобы каждый из 4х моих Observable выполнялся в отдельном треде с целью максимизации скорости, всю остальную заботу берет на себя RxJava. То есть задействованы будут все 4 процессора, при наличие оных.

Как видите, потенциал у реактивного программирования огромный, а фукнционал RxJava реализует его в достаточной мере.

Проблемы, с которыми мы столкнулись


Всё, продемонстрированное выше и намного больше в том или ином виде используется у нас в дайлере. И вот с какими проблемами мы столкнулись:

  • Проблема OOM. Наивно полагать, что Android может дать много тредов для многопоточной работы. При количестве тредов больше 15, даже топовые смартфоны начинали “задумываться”, а их мелкие собратья и вовсе падали с OutOfMemoryError. Решение было простым. Ввести CachedThreadPool для этих дел и проблема решена.
  • Кэширование запросов. Речь не про оператор cache() из примера выше. Хотелось бы, чтобы следующий запрос на тот же самый url сразу брался из кэша. В RxJava такого нет. В принципе это правильно, потому что реактивность и кэш — две разные вещи. Поэтому мы написали свой кэш.

Что еще?


Мы увидели, как классно реактивно работать с многопоточностью и запросами в Android. Но это далеко не всё. Например, можно подписываться на изменение Checkable или EditText (это из коробки идет в RxJava для Android). Тут всё просто, но ужасно удобно.
Кстати, одной RxJava реактивное программирование под Java не ограничивается. Существуют и другие библиотеки, например, Bolts-Android.
Кроме этого, сейчас активно разрабатывается Reactive-Streams, который призван унифицировать работу с разными реактивными провайдерами в java.

Вывод


Понравилось ли нам? Однозначно. Реактивные приложения действительно гораздо устойчивее к багам и падениям, код становится понятным и гибким (были бы лямбды — был бы еще и красивым). Много рутинной работы перекладывается на библиотеку, которая выполняет свою работу лучше, чем нативные Android-компоненты. Это позволяет сосредоточиться на реализации вещей, которые действительно стоит обдумать.

Реактивное программирование — это немного другое мышление по сравнению с традиционной разработкой под Android. Потоки данных, функциональные операторы — эти сложные, на первый взгляд, вещи оказываются намного проще, если разобраться. Стоит немного поработать с реактивным программированием, и мозг начинает перестраиваться с объектов и состояний на монады и операторы над ними. Это такая большая, добрая, мощная частичка ФП в ООП, которая делает жизнь и код проще, а приложение лучше. Попробуйте, не пожалеете.

Ссылки, которые помогут вам разобраться с реактивным программированием или просто могут оказаться интересными:


Небольшое отступление. Если вы разделяете наши взгляды на программирование и создание продуктов, то приходите — будем рады вас видеть в команде 2GIS Dialer.
Автор: @lNevermore
2ГИС
рейтинг 87,21
Карта города и справочник предприятий

Комментарии (59)

  • 0
    Очень рад видеть, что RxJava используется в реальном проекте. Это очень мощная библиотека, которую, ИМХО, должны использовать все Java-программисты, сталкивающиеся в своих проектах с реактивностью.
    • 0
      Отказался от использования RxJava, сильно портит читабельность и лаконичность кода.
      • 0
        Какая версия Java?
        • 0
          Android разработка, 6 версия.
          • –3
            Ну да точно, там лямбд нету. Поэтому и не лаконично. Я конечно же говорил про Java 8.
            • 0
              У нас в мире Android большие проблемы с многопоточностью и обработкой результатов. Конечно есть кучу библиотек для этого, но все равно пока нет ничего лаконичного и красивого, чтобы код душу радовал.
              • 0
                Да, с красотой действительно беда, об этом я говорил. Но вот по эффективности RxJava очень хороша. Особенно в связке с Retrofit, если говорить про REST.
                • 0
                  Согласен, по эффективности все хорошо, но простота кода не менее важный фактор.
  • +3
    А почему дайлер так долго стартует, до сих пор? При старте раскручивается данный аппарат, и эта задача полностью загружает девайс, что UI потоку не хватает «места» отрендерить клавиатурку и лоадинг истории вызовов? Спасибо.
    • –4
      Возможно ребята увлеклись многопоточностью. В свое время сравнивал что лучше — 4 тяжелых потока одновременно или последовательно. Результат удивил — по скорости многопоточность выиграла, но не в 4 раза быстрее, а процентов на 20-30. С другой стороны 4 одновременных потока очень сильно нагрели смартфон. Хотя сейчас это уже не так существенно. Сам стартую по 3 потока за раз, тем более что с android query это выглядит не менее изящно чем с RxJava:
              aq.progress(this).ajax(getCallBack(this, "user.get"));
              aq.progress(this).ajax(getCallBack(this, "user.getCategories"));
              aq.progress(this).ajax(getCallBack(this, "user.getChannelsWithTags&type=channels"));
      
      • –2
        ах, да еще и кеширование из коробки^^ обработка вьюх как в груви, работа с картинками и никаких жалких аутофмемори
        public static AjaxCallback getCallBack(Object o,String method) {
                String url = SurfingServiceImpl.API_URL+"?method="+method;
                AjaxCallback<String> cb = new AjaxCallback<String>();
                cb.url(url).type(String.class).weakHandler(o, "onRefresh").fileCache(true).expire(1000 * 15);
                cb.header("Authorization", "Bearer " + SurfingbirdApplication.getInstance().getSettings().getLoginToken());
                return cb;
            }
        

        Нет уж ребят, лучше Вы к нам
      • НЛО прилетело и опубликовало эту надпись здесь
        • –2
          Цитата нужна? Поясню. Я искал узкое место в коде и возможности оптимизации. Выяснилось что 4 http запроса в 4 потока не будут быстрее в 4 раза 4 последовательных запросов. Но буквально раскаляли телефон (это был Нексус 3 по моему). По всей видимости это связано с тем что основные накладные расходы связаны с инициализацией http клиента, установкой соединения и так далее. Вобщем должно быть очевидно что реальные задачи сильно расходятся с синтетическими тестами. Кстати узкое место я тогда нашел, и им оказался дефолтный JSON парсер. Переход на SAX парсер дал прирост раз в 100 и облегчил работу GC.

          Хотя в данном конретном случае — не думаю что дело в потоках вообще. Скорее всего инициализация/работа RxJava занимает много времени. За все надо платить. Нравятся Скала, RxJava — используйте. Но за Ваш комфорт программиста заплатят пользователи своим временем. И кстати в андроидквери вы заплатите увеличением программы килобайт на 200 и не потеряете в производительности. А получите то же самое. Вобщем я останусь ретроградом и продолжу кодить на яве.
          • –2
            Выяснилось что 4 http запроса в 4 потока не будут быстрее в 4 раза 4 последовательных запросов.


            в Android SDK до сих пор не завезли честные асинхронные сетевые запросы, обязательно использовать треды?
          • НЛО прилетело и опубликовало эту надпись здесь
            • –4
              Мы сейчас в википедии? о_О Я рассказал о своем исследовании и своих выводах, вот и все. Если Вам оно в чем то непонятно или Вы с ним не согласны — проведите свое.
              • НЛО прилетело и опубликовало эту надпись здесь
                • –8
                  Нотариально Заверенный Скриншот подойдет?
                • +1
                  Алексей, учитывая вашу профессиональную личность, складывается ощущение, что вы издеваетесь над человеком. Он ведь даже не понимает, что в результатах его исследования отсутствует объект исследования.
    • +3
      Там не виснет UI, просто зеленый экран и ничего не происходит. Почему так? Потому что в дайлере все еще есть архитектурные части, которые требуют инициализации при старте приложения. Правильным решением является устранение этой необходимости, а не ProgressBar перед историей вызовов. Они никак не относятся к RxJava. Какая вообще может быть инициализация у RxJava? Ее нет. Разве что создание тред пулов и все вот это, но оно никак не трогает старт приложения.
  • 0
    Давно уже не использую AsyncTask, т.к. в аднроиде появились Loaders.
    Возникает вопрос: как эта библиотека работает в случаях смены конфигурации экрана, например?
    • +1
      github.com/evant/rxloader попробуйте это
    • +1
      Давно уже не использую Loaders, т.к. в android «появились» сервисы
    • 0
      RxJava прекрасно работает в случаях смены конфигурации экрана. Ее это просто не трогает. Observable не привязан к Context' у активити, если вы все правильно делаете. И дальше вам решать, что и как делать.
      • 0
        Можно поподробнее отсюда? Если RxJava так же, как и AsyncTask работают внутри фрагментов, то при смене ориентации дисплее они будут запущены по новой.
      • 0
        или у вас все фрагменты setRetainInstance(true)?
        • 0
          Нет, конкретно у нас просто только портрет ориентация поддерживается, вот и все. А на деле все работает хорошо, потому что внутри себя Observable все-таки никак не связан с контекстом активити, только с контекстом самого Application. А при перевороте он сохраняется. А подписываться и отписываться (для того, чтобы обрабатывать и менять UI) можно хоть-сколько раз.
          • +1
            Писать приложение в одной ориентации конечно намного проще)
  • 0
    Приятно видеть что люди стали открывать для себя событийные языки. Но пока чтоRxJava не идет ни в какое сравнение ни с SystemC, ни с VHDL, ни c Verilog.
  • 0
    Мы у себя тоже используем RxJava, очень довольны.
    > (были бы лямбды — был бы еще и красивым)
    Это решается с помощью retrolambda:

    buildscript {
    repositories {
    mavenCentral()
    }

    dependencies {
    classpath 'com.android.tools.build:gradle:0.11.+'
    classpath 'me.tatarka:gradle-retrolambda:1.3.+'
    }
    }

    apply plugin: 'retrolambda'

    retrolambda {
    jdk System.getenv(«JAVA8_HOME»)
    }

    dependencies {
    retrolambdaConfig 'net.orfjackal.retrolambda:retrolambda:1.+'

    compile 'com.netflix.rxjava:rxjava-android:0.19.+'
    compile 'com.netflix.rxjava:rxjava-async-util:0.19.+'
    }

    P.S. извиняюсь, почему то не работает :(
    • 0
      все работает. lambdas, rxjava, retrolambda, AS 0.8.1

      вот тут есть работающий пример, из которого я брал конфиг
      github.com/fs/android-base
      • 0
        Я имел ввиду " < code > " не работает)
  • +1
    Не возникало ли проблем с дебагом такого кода? Или при анализе стектрейсов, если что-то внутри Observable упало?

    PS Спасибо за интересную статью.
    • 0
      Нет, никаких проблем. Android Studio спокойно заходит во все тела всех Observable и все хорошо. Единственное, что затрудняет дебаг, так это пошаговая отладка, потому что на пути от onNext() до, например, flatMap, если произошел retry() происходит много внутренних преобразований.

      Со стектрейсами все ок, читабельно, ее умные ребята делали.
  • +2
    «Подобный код встречается во многих проектах, он понятен, а миллионы леммингов не могут ошибаться. Но давайте копнём чуть глубже:
    Что делать, если где-то во время выполнения выпал Exception?
    doInBackground(Void...) выполняется в отдельном потоке, как нам сказать пользователю об ошибке в UI? Заводить поля для Exception?
    А что возвращать, если не прошел запрос? null?
    А если json не валидный?
    Что стоит делать, если не удалось кэшировать объект?
    »

    Возвращать из doInBackground не JSONObject, а экземпляр своего класса, у которого, скажем, будет два поля: status, msg, object. Если в doInBackground ошибка/исключение, то возвращаем из него свой объект с соответствующим статусом и сообщением. Потом в onPostExecute проверяем сие поле и уведомляем UI (так как doInBackground имеет доступ к UI). Так что проблема, по-моему, выдуманная )
    • 0
      fix: конечно же, onPostExecute имеет доступ к UI, а не doInBackground
    • 0
      Зачем городить костыли, лучше использовать robospice + retrofit. Или аналогичные либы.
      • –1
        Обработка исключений — это костыли?
        • –1
          Таким способом — да.
      • +1
        robospice + retrofit, наверное, самое лучшее что можно использовать для rest запросов. Минисуют, видимо, те кто до сих пор использует AsyncTask
    • 0
      Если во время работы doInBackground возникает исключительная ситуация, выполнение асинк-таска можно закончить методом cancel(). В таком случае вместо onPostExecute вызовется onCancelled() в аргументах которого вы можете передать и причину и объект, если зохочется.
      • 0
        А если я не хочу cancel(), а хочу еще раз попробовать сделать запрос? Понятно, что ASyncTask вполне самодостаточная вещь в каокм-то смысле. Но ее функционал все же ограничен, стоит признать.
        • 0
          это был комментарий господину Suvitruf, с примером того, как реагировать на ошибки в таске, а не к статье в целом.
  • 0
    Непонятно только одно: при чем тут андроид?
    • 0
      А при том, что у нас в 2GisDialer используется RxJava. А 2GisDialer — это приложение, написанное под Android OS. Кроме того, в статье приведено сравнение подходов к многопоточности в Android с использованием ASyncTask и Observable. Также замечу, что класс ASyncTask является частью Android SDK и его нет в Java.
      • +1
        Против самого примера ничего не имею, я о другом. Если не знать что такое RxJava и читать по диагонали, то можно упустить важный факт, что RxJava — это не только «под Android». Т.е. значительно лучше было бы акцентировать внимание на том, что это для «Java в общем, и для Android в частности». Я, например, уверен, что многие люди уже из-за одного названия статью просто не открыли.
  • 0
    Pipeline с правильной подборкой базвордов, не?
  • +1
    Проблемы, с которыми мы столкнулись

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

    Вы кешируете каждый пчих в lru cache, который храниться в памяти (±32Мб — размер кеша). Вот и получаеться что чем больше потоков тем больше записи в lru cache, тем больше вызова GC после вытеснения, а вот GC умеет останавливает UI Thread и вам кажеться что это потоки тупят систему, а на самом деле вызовы GC в лучшем случае линейно зависят от количчества потоков.

    Учитывая что вставка идет JSON, то это подразумевает Network операции, это тоже память, а если где то паралельно грузяться картинки (упаси в оригинальном размере), то не удевительно что у вас просто тупит приложение из за частого вызова GC. А ограничение в 4-е потока это просто приемлемый хак, но не решение проблемы.
    • 0
      Для кеширования ответов с сервера лучше DiskLruCache использовать (если конечно он и не используеться), учитывая что в сетевые операция изначально заложены задержки.
  • 0
    final Subscription subscription =
              Observable.create(new Observable.OnSubscribe<String>() {
    


    Разве не так?

    final Observable observable =
             Observable.create(new Observable.OnSubscribe<String>() {
    


    Как раз решил разобраться с Java RX. Пример не пошел
    • 0
      Да, разумеется, это описка. Спасибо.
      Единственный метод, который возвращает не новый Observable, а Subscription — это метод subscribe()
  • +1
    Там наверное в конце пропущено }).subscribe();
    • 0
      Именно. Убрал за ненадобностью в примере, а тип ссылки не поменял.
      • 0
        Попробовал на фиде конверторе
        github.com/app-z/CurrencyConverter2

        Тогда еще вопрос задам, специалусту в RX Java
        final Subscription subscription =…
        Не надо делать unSubscribe в onDestroy? Т.е. я имею ввиду выносить наружу subscription из метода или из onCreate и делать член класса
        Я к тому что сабскрайбер сам разберется когда прервать работу если активити уничтожается?
        • 0
          > сабскрайбер сам разберется когда прервать работу если активити уничтожается

          unsubscribe надо делать вручную. RxJava ничего не знает про жизненный цикл Activity или каких-либо других сущностей Android. Немного подробнее можно прочитать здесь в разделе «Fragment and Activity life-cycle».
  • +1
    Теоретически CachedThreadPool тоже может положить приложение, если, к примеру, одновременно в пул на выполнение отправляются штук 20 немаленьких задач, таким образом создается штук 20 потоков.
    В AsyncTask, например, создается ThreadPoolExecutor c параметрами CORE_POOL_SIZE = CPU_COUNT + 1 и MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1, где CPU_COUNT — количество процессоров на устройстве. Таким образом, если на устройстве 2 процессора, то количество потоков не будет превышать 5. По идее данный вариант самый безопасный. Но и с вашим вероятность падения очень маленькая.
    А так, спасибо за статью, весьма полезная)
    • +1
      Полностью согласен с вами. Добавлю так же, что лучше создавать несколько тредпулов с меньшим количеством тредов.

      Например, тредпул с CPU_COUNT/2 для работы с сетью, такой же для выполнения рутинных задач и FixedThreadPoolExecutor на 2-3 треда для выполнения очень приоритетных задач (коих должно быть не много по задумке).

      Так проще регулировать работы обсерваблов, если их становится много, а так же позволяет приоритезировать задачи. Обычно это работает неплохо.
  • 0
    Было бы классно, если бы Вы добавили в статью решение еще одного примера.
    Есть у нас метод запроса в сеть с параметрами offset, limit. Назовем метод request(int offset, int limit). Нам нужно получить весь массив данных. То есть скорее всего придется вызвать несколько раз request с разными параметрами offset и limit, и полученные массивы соединить в один.
    Один облегчающий фактор в том, что метод request возвращает Observable (то есть работаем через RetroFit).
    Собственно как должна выглядеть вся портянка итогового Observable, чтобы при подписке к нему, мы сразу получали весь массив данных?
    Честно говоря, сколько гуглил, так и не смог найти понятного примера. Может вы с этим уже сталкивались?)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка