Приветствую, коллеги. Рад видеть вас снова в третьей части «Руководства по фоновой работе в Android». Если не видели предыдущие части, вот они:


В прошлый раз мы разобрались, как работают Loaders, а сразу после этого Google взял и сообщил, что они полностью переписали LoaderManager. Видимо, мне надо позже вернуться к этой теме, но пока что буду следовать плану и делиться подробностями того, как организовать фоновую работу в Android исключительно с помощью джавовых thread pool executors, а также как EventBus может помочь в этом, и как всё это работает под капотом.

Давайте вспомним, в чём главная загвоздка: времязатратные операции вроде сетевых вызовов должны осуществляться в фоновом потоке, но публикация результата может происходить только в главном UI-потоке.

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

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

Из-за этих ограничений возник подход, опирающийся на executors. Давайте рассмотрим его. Для начала, понятно, нам надо разжиться потоками, куда мы сможем отправлять свои задачи для фоновой работы. Давайте создадим для этого класс Background:

public class Background {

   private final ExecutorService mService = new ScheduledThreadPoolExecutor(5);
   
   public Future<?> execute(Runnable runnable) {
      return mService.submit(runnable);
   }
   public <T> Future<T> submit(Callable<T> runnable) {
      return mService.submit(runnable);
   }
}


Итак, у нас есть executor, и есть метод, позволяющий запустить какой-то код асинхронно, обернув его в Runnable или Callable.

Здорово, давайте попробуем засунуть результат операции в UI-поток. Не проблема, мы знаем, что нам требуется только Handler:

public class Background {
  ...
  private final Handler mUiHandler;

  public void postOnUiThread(final Runnable runnable) {
     mUiHandler.post(runnable);
  }
}


Но подождите, мы не знаем, существует ли в этот момент вообще наш UI, и если да, как он узнает, что надо что-то изменить?

Тут на помощь и приходит подход, называемый «шина событий» или event bus. Общая идея в том, что есть некая общая шина (или даже несколько), куда публикуются события. Кто угодно может в любое время начать слушать шину, получать события, а затем прекращать слушать (звучит похоже на RxJava, да? Дождитесь следующей статьи!)

В общем, нам нужны три составляющих:

Сама шина
Источник (или источники) событий
Слушатель (или слушатели) событий

Можно отразить эту структуру такой диаграммой:


Принципиальная схема передачи событий по шине

Шина событий


Никто не требует самостоятельно реализовывать шину с нуля. Можно выбрать одну из существующих реализаций: Google Guava, Otto или EventBus от greenrobot (у последнего есть стильная поддержка отправки событий на разные потоки с помощью аннотаций).

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

public class Background {

   private final Bus mEventBus;
  
   public void postEvent(final Object event) {
      mEventBus.post(event);
   }
}


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

 mBackground.execute(new Runnable() {
   @Override
   public void run() {
      try {

         initDatabaseInternal();
         mBackground.post(new DatabaseLoadedEvent());

      } catch (Exception e) {
         Log.e("Failed to init db", e);
      }
   }
});


Так что, например, мы можем спрятать прогресс-бар и перейти к нашей MainActivity:

public class SplashActivity extends Activity {
  
    @Override
    protected void onStart() {
        super.onStart();
        eventBus.register(this);
    }

    @Override
    protected void onStop() {
       eventBus.unregister(this);
       super.onStop();
    }
  
    @Subscribe
    public void on(DatabaseLoadedEvent event) {
       progressBar.setVisibility(View.GONE);
       showMainActivity();
    }
  
}



Проблема тут нам уже хорошо известна: нельзя модифицировать UI из фонового потока, а код выше пытается сделать именно это.

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

Но перед этим давайте немного закопаемся в сорцы и посмотрим, как вызывается метод, помеченный аннотацией Subscribe, когда событие публикуется.

Заглянем в исходный код шины событий Google Guava:

public class EventBus {

  private final SubscriberRegistry subscribers = new SubscriberRegistry(this);

  public void register(Object object) {
    subscribers.register(object);
  }

  public void unregister(Object object) {
    subscribers.unregister(object);
  }
  
   public void post(Object event) {
    Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
    if (eventSubscribers.hasNext()) {
      dispatcher.dispatch(event, eventSubscribers);
    } else if (!(event instanceof DeadEvent)) {
      // the event had no subscribers and was not itself a DeadEvent
      post(new DeadEvent(this, event));
    }
  }
}


Как видим, шина событий хранит подписчиков в SubscriberRegistry и пытается передать каждое событие подписчику конкретно этого события (ключом здесь выступает название класса объекта). Список подписчиков можно представить себе в виде Map <EventType, Subscriber>.

Обращение с потоками зависит от объекта dispatcher, который по умолчанию выставлен на Dispatcher.perThreadDispatchQueue().

Что же происходит внутри dispatcher:

private static final class PerThreadQueuedDispatcher extends Dispatcher {

    private final ThreadLocal<Queue<Event>> queue =
        new ThreadLocal<Queue<Event>>() {
          @Override
          protected Queue<Event> initialValue() {
            return Queues.newArrayDeque();
          }
        };

    @Override
    void dispatch(Object event, Iterator<Subscriber> subscribers) {

      Queue<Event> queueForThread = queue.get();
      queueForThread.offer(new Event(event, subscribers));

      Event nextEvent;
      while ((nextEvent = queueForThread.poll()) != null) {
        while (nextEvent.subscribers.hasNext()) {
          nextEvent.subscribers.next().dispatchEvent(nextEvent.event);
        }
      }
    }


Главное тут: PerThreadQueuedDispatcher использует ThreadLocal для хранения очереди событий. По сути, это означает, что метод подписчика будет вызван в том же самом потоке, в котором было опубликовано событие.

И что нам с этим делать? Решение нехитрое — просто публиковать события в том потоке, в котором хотите их обрабатывать:

public void postEventOnUiThread(final Object event) {
   mUiHandler.post(new Runnable() {
      @Override
      public void run() {
         mEventBus.post(event);
      }
   });
}


Это работает, но привносит ту проблему, которую шина событий вроде как должна решать: понижение связности с помощью разделения публикации и обработки событий. С таким решением мы обязываем код, публикующий события, знать, в каком потоке клиент хотел бы обработать код.

Другим решением стало бы использование Handlers прямо в UI:

public class SplashActivity extends Activity {

  @Subscribe
  public void on(DatabaseLoadedEvent event) {
    runOnUiThread(new Runnable() {
      @Override
       public void run() {
         progressBar.setVisibility(View.GONE);
         showMainActivity();
       }
    })     
  }
}


Это тоже не выглядит полноценным решением. И в этом состоит ограничение шины событий. Как с этим можно справиться? Конечно, с помощью RxJava! Но об этом — в следующей части.
От автора: Я вхожу в программный комитет конференции Mobius, и её программа на 90% готова. Скорее смотрите, что вам приготовила конференция, и ждите новостей о финализации программы!

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


  1. usharik
    14.03.2018 14:40

    Уже давно хотел спросить, про использование библиотеки RxJava для реализации фоновой работы в Андроид. Является ли она полноценной альтернативой описаным в вашем цикле статей средством реализации фоновых процессов?


    1. dzigoro Автор
      14.03.2018 14:41

      Является. RxJava — очень мощный инструмент, и я бы сказал, что просто для организации фоновой работы слишком мощный. Но об этом в следующей статье и моем выступлении на Mobius Piter 2018.


      1. usharik
        14.03.2018 14:44

        С нетерпением жду продолжения!


  1. nekdenis
    14.03.2018 14:40
    +1

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


  1. pvloleg
    15.03.2018 16:48

    https://medium.com/google-developers/loaders-in-support-library-27-1-0-b1a1f0fee638
    всегда знал что что-то с этими лоадерами не так. чуть сложнее кейс и все сыпется.
    и если писать про ивентбас, то стоит упомянуть, думаю, что многие считают его антипатерном из-за спагети-архитектуры которую он навязывает что-ли. про трудности тестирования его