30 января 2013 в 19:25

Общение между потоками через ResultReceiver

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

Вместо вступления


В большинстве Android-проектов приходится организовывать общение с внешним миром, т.е. организовывать сетевое взаимодействие. Не буду повторяться почему выполнять такой долгоиграющий код в UI-потоке плохо. Это всем известно. Более того, начиная с API 11 (Honeycomb который) система бьет разработчика по рукам исключением когда тот пытается в UI-потоке делать сетевые вызовы.
Одним из вариантов общения UI-потока с параллельным потоком (в котором, к примеру, выполняется http-запрос) есть подход, основанный на применении встроенного системного класса android.os.ResultReceiver совместно с сервисом.

Немного о архитектуре подхода


Для организации отдельного потока я выбрал IntentService. Почему именно он, а не простой Service? Потому, что IntentService при поступлении к нему команды автоматически начинает выполнять метод onHandleIntent(Intent intent) в отдельном от UI потоке. Простой Service такого не позволяет ибо он выполняется в основном потоке. Организовывать запуск потока из Service'а нужно самостоятельно.
Общение между Activity и IntentService-ом будет происходить с помощью Intent'ов.

Код


Сначала исходный код, потом ниже краткие комментарии к тому, что там и как происходит.

Реализация ResultReceiver'а

public class AppResultsReceiver extends ResultReceiver {
	
	public interface Receiver {
		public void onReceiveResult(int resultCode, Bundle data);
	}
	
	private Receiver mReceiver;
	
	public AppResultsReceiver(Handler handler) {
		super(handler);
	}
	
	public void setReceiver(Receiver receiver) {
		mReceiver = receiver;
	}

	@Override
	protected void onReceiveResult(int resultCode, Bundle resultData) {
		if (mReceiver != null) {
			mReceiver.onReceiveResult(resultCode, resultData);
		}
	}
}

Здесь следует обратить внимание на коллбэк (Receiver). При получении результата в onReceiveResult() делается проверка на не-null коллбэка. Дальше в коде активити будет показано как активировать и деактивировать ресивер с помощью этого коллбэка.

IntentService

public class AppService extends IntentService {
	public AppService() {
		this("AppService");
	}
	
	public AppService(String name) {
		super(name);
	}

	@Override
	protected void onHandleIntent(Intent intent) {
		final ResultReceiver receiver = intent.getParcelableExtra(Constants.RECEIVER);
		receiver.send(Constants.STATUS_RUNNING, Bundle.EMPTY);
		final Bundle data = new Bundle();
		
		try {
			Thread.sleep(Constants.SERVICE_DELAY);
			data.putString(Constants.RECEIVER_DATA, "Sample result data");
		} catch (InterruptedException e) {
			data.putString(Constants.RECEIVER_DATA, "Error");
		}
		receiver.send(Constants.STATUS_FINISHED, data);
	}
}

onHandleIntent() будет вызван после того, как вызывающий код (UI-классы etc.) выполнит startService(). Инстанс ResultReceiver'а будет извлечен из интента и ему тут же будет отослана команда «ОК, я пошел трудиться». После выполнения полезной работы в этом методе результаты (извлеченные из JSON классы-модели, строки, что-угодно) помещается в бандл и отправляется ресиверу. Причем для индикации типа ответа используются разные коды (описаны константами). Как ResultReceiver получает и отправляет данные можно почитать в его исходниках.

Посылка команды сервису и обработка результата (Activity)

public class MainActivity extends Activity implements AppResultsReceiver.Receiver {
	private AppResultsReceiver mReceiver;
	private ProgressBar mProgress;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mProgress = (ProgressBar) findViewById(R.id.progressBar); 
	}
	
	@Override
	protected void onResume() {
		super.onResume();
		mReceiver = new AppResultsReceiver(new Handler());
		mReceiver.setReceiver(this);
	}
	
	@Override
	protected void onPause() {
		super.onPause();
		mReceiver.setReceiver(null);
	}

	public void onStartButtonClick(View anchor) {
		final Intent intent = new Intent("SOME_COMMAND_ACTION", null, this, AppService.class);
		intent.putExtra(Constants.RECEIVER, mReceiver);
		startService(intent);
	}

	@Override
	public void onReceiveResult(int resultCode, Bundle data) {
		switch (resultCode) {
			case Constants.STATUS_RUNNING :
				mProgress.setVisibility(View.VISIBLE);
				break;
			case Constants.STATUS_FINISHED :
				mProgress.setVisibility(View.INVISIBLE);
				Toast.makeText(this, "Service finished with data: " 
						+ data.getString(Constants.RECEIVER_DATA), Toast.LENGTH_SHORT).show();
				break;
		}
	}
}

Здесь все просто. Activity реализует интерфейс AppResultsReceiver.Receiver. При старте создает экземпляр ресивера, при паузе — отвязывается от прослушивания ответов от сервиса. При клике на кнопку формируется команда (интент), в него помещается ссылка на наш ResultReceiver и стартуется сервис.
При получении ответа от сервиса в методе onReceiveResult() проверяется код ответа и выполняется соответствующее действие. Вот и все.

Демо-приложение выглядит просто, оно имеет всего одну кнопку «Послать запрос».


Исходный код демо-проекта доступен на GitHub

Вместо заключения



Обработка команды в фоновом сервисе реализована до безобразия просто: поток просто ставится на паузу на некоторое время. Конечно же в реальных приложениях нужно в интенте передавать код команды (action), которую нужно выполнить, дополнительные параметры и прочее. ООП вам в руки. Также стоит помнить, что данные (например, модели), которые будучи упакованными в бандл должны быть Parcelable-объектами. Это повысит эффективность их сериализации.
Конечно же описанный подход не есть истина в последней инстанции. Мы вольны выбирать разные архитектурные подходы, средства и комбинации. Будь то AsyncTask'и, Service+Thread+BroadcastReceiver или «ручная» передача Message'ей посредством Handler'а в UI-поток. Выбирать, как говориться, вам. Но это уже совсем другая история.
Vitaly Gashock @gshock
карма
19,2
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

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

  • 0
    Хм, взяли мы запустили сервис, повернули экран — и все поломалося. Ресивер уже создался новый, а в сервисе лежит старый. Такие ситуации никак не хэндлятся у вас?
    • 0
      Вы правы, не хендлятся. В демо это осознано упрощено дабы показать как работает связка IntentService+ResultReceiver. Понятно, что в реальной жизни нужно обрабатывать перевороты экрана.
      • +1
        Тут как раз и начинается самое интересное. Чтобы его сохранять, надо пихать его в BundleonSaveInstanceState()). ResulReceiver у нас Parceable, так что проблем вроде как нет. Осатается только считать его из savedInstanceState в onCreate().

        Вот только я столкнулся с таким непредвиденным поведением: после долгого нахождения приложения в бэкграунде (свернутом сосотянии), когда активити будет разрушена для освобождения памяти для других приложений, а потом когда вы снова развернете ее — то вызовется onCreate(), но в savedInstanceState будет лежать не ваш AppResultsReceiver, а почему-то простой ResultsReceiver. И при попытке закастить его к своему классу получите ClassCastException.

        Почему так просходит — для меня загадка. Но именно из-за этого я отказался от ResultReceiver'ов, и стал использовать LocalBroadcastManager как замену. Надо сказать более удобно и менее геморно стало.
        • 0
          А разве IntentService не стопает себя после того, как отработает onHandleIntent()? Стопает. Почему бы тогда в вашей ситуации просто заново не делать startService() вместо попытки прикастить полученный инстанс из бандла?
          • 0
            Если я вызову startService() снова, то и onHandleIntent() у меня вызовется снова (после того как отработает предыдущий). Это что, тогда сколько раз я экран поверну, столько раз и и выполнится onHandleIntent()? Зачем мне такая логика?
        • 0
          Если ResultReceiver хранить в объекте Application, такую проблему можно обойти.
          • 0
            Как член класса? Не очень красивое решение.
            • 0
              Не напрямую, конечно.
              Этот подход даже статью был оформлен не так давно.

              habrahabr.ru/post/144275/
          • 0
            А если у Вас разные команды, поэтому resultreciver в разных activity будут по сути разные, сторить все в application явно не тру.
  • 0
    Забыли указать главное ограничение IntentService — полученные задачи выполняются один за одним, по очереди.
    В основном IntentService подходит для обработки событий от других компонентов системы, например, ответов от AlarmManager, броадкастов (своих или системных), реакция на события от виджетов и тд.

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

    Для работы с сетью все же придется использовать Service+Threads.
    • 0
      Таким образом, для работы с сетью, IntentService уже не подходит, конечно же если мы говорим о выполнении нескольких запросов одновременно, а обычно это так.

      В случае, когда нужно организовать синхронное общение с сетью (например, общение с REST API) то IntentService подойдет нормально.
      А вот если нужно делать параллельные асинхронные запросы — то да, согласен с вами
      • 0
        Синхронное общение с сетью на столько редко, что практически никогда. А даже если и когда, то в таком случае смысла в сервисе мало, хватит простого асинктаска.
        • 0
          ну вот бывают случаи, когда нужно засинкать локальную бд с веб сервисом, а потом оповестить об этом UI. и при это весь синк выполняется в 1-2 синхронных запроса. идеально, на мой взгляд, подходит IntentService
          • 0
            Ну то есть для работы с сетью все равно нужен будет еще один отдельный сервис?
            • 0
              ага, для работы с сетью, не имеющей отношения к основному синку данных, отдельный сервис/асинктаск/лоадер
              • 0
                Об этом собственно я и написал в самом первом комментарии, что IntentService ограничен и подходит для одиночных долго играющих запросов. Для чего то более динамичного, как например, работа с сетью, тот же rest api, уже не подходит.

                Просто не плохо было бы об этом упомянуть в самой статье, плюсы и минусы, дабы не выглядело что IntentService — это универсальное решение.
                • 0
                  в продолжение дискуссии
                  вот только наткнулся на класс MultiThreadedIntentService. Наследует обычный Service и делает из него IntentService, только уже с возможностью выполнения нескольких задач одновременно.
  • +3
    Очень хорошая статья, для понимания как правильно надо делать.

    Но если не хочется самому все это делать, есть уже готовые библиотеки.
    Например, github.com/octo-online/robospice, они как раз делают все через сервис, + поддержка кеширования и тд.
    Можно даже поковырять их код для более лучшего понимания.

    У них кстати есть интересная инфографика, показывающая возможные проблемы AsyncTask и Loader, в сравнении с подходом работы через сервис:
    Показать картинку
    image


    • –1
      Хм сейчас гляну. В совем приложении использовал асинктаски, но я отключил пересоздание активити при повротах экрана и при необходимости, должен сказать оооооочень редкой, сам хендлил необходимые изменения, связанные с изменением лейаута.
      • 0
        Да, самый простой способ избавиться от проблемы при повороте — это отключить или игнорировать его.

        Но опять же появляются новые проблемы:
        1. все изменения связанные со сменой конфигурации надо обновлять самому, а это очень много работы.
        2. возможность легко сделать утечку памяти в п1
        3. если вы не завершите асинктаск ваша активити будет жить, а значит снова memory leak. Как такое может произойти — How to Leak a Context: Handlers & Inner Classes

        Из плюсов сервиса: даже если пользователь не дождался завершения запроса, не страшно, его можно до выполнить, закешировать и показать моментально при следующем открытии.

        Как альтеративный вариант можно использовать retain fragment без UI, и уже в нем создавать AsyncTask. Таким образом можно избавится хотя бы от проблемы с изменениями конфигурации при использовании AsyncTask.
        • –2
          1. Возможно, у кого-то этото впорос стоит остро, но у меня не было задачи менять структуру лейаута при повротах. Поэтому все корректно масштабировалось само. А где лейаут у меня строился в зависимости от размера экарна я и так его просчитывал в ручную.
          2. Не верю
          3. Зачем из мухи делать слона? Будет она жить ровно столько, сколько будет жить асинктаск. Если это сетевой запрос, то максимум отвалится по таймауту, а дальше сборщик все сожрет за милую душу.

          А как потом обновлять UI из фрагмента?
          • 0
            Вообще-то поддерживать разные ориентации экранов в приложении является даже больше, чем правило хорошего тона.
            Возможно, у кого-то этото впорос стоит остро, но у меня не было задачи менять структуру лейаута при повротах. Поэтому все корректно масштабировалось само. А где лейаут у меня строился в зависимости от размера экарна я и так его просчитывал в ручную.

            Комрад имел ввиду не столько изменение UI, сколько обработку сохранения и восстановления текущего состояния активити.
            2. Не верю

            Верьте
            • 0
              Вообще-то поддерживать разные ориентации экранов в приложении является даже больше, чем правило хорошего тона.(хм почему то blockquote не пашет наверно из-за рейтинга)

              Эм я не говорил про отключение смены ориентации девайса. А про отключение пересоздании активити при смене ориентации. Все вертится и ресайзится без проблем. Пробелмы повялются, если у вас под разные ориентации разные ресурсы юзаются, или разная структура лейаута, тогда их надо самим менять.
            • 0
              >Вообще-то поддерживать разные ориентации экранов в приложении является
              >даже больше, чем правило хорошего тона.
              Видимо 99% игроделов эти правила не читали.
          • 0
            > А как потом обновлять UI из фрагмента?

            Просто слушаете оповещение и уже выполняете необходимые действия с UI.
            Например как то так:
            1. Делаем запрос на выполнение чего либо в фоне.
            2. В самом же фрагменте в отдельном треде уже проверяем актуальность данных, и либо возвращаем уже существующие, либо делаем новую фоновую задачу и по завершению оповещаем слушателей.

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

            Этот же способ можно использовать и для хранения временных данных при поворотах.
            Например, у нас есть ответ от сервера, скажем какой то список, в базу мы пока не хотим кешировать и нам нужно что бы разметка при поворотах нормально подхватывалась в зависимости от конфигурации, и статику тоже не хотим использовать. Тогда делаем отдельный ретеин фрагмент и храним все в нем.
            • 0
              Может порекамендуете какую-нибть статью или туториал по данной тематике?
              Конкретнее инетерсует, как правильно закодить UI, обновление модели в бекграунде, и обновления UI по модели
      • 0
        у асинк тасков проблема не только с поворотами. вам могут банально позвонить когда ваше приложение работает.
        а еще пользователь может сам свернуть ваше приложение и уйти по своим делам, такое тоже часто случается.
        тысячи случаев, на самом деле, андроид так непредсказуем

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