AIDL (Android Interface Definition Language) и коммуникация между процессами (IPC)

В данной статье мы попытаемся описать свой опыт работы с AIDL в Android IPC.
В ней содержится пример приложения с сервисом, который запущен в отдельном процессе.

Статью стоит рассматривать как:
  • пример архитектуры приложения, использующего remote Android Services и AIDL.
  • полезные примеры кода.
  • исключительно как дополнение к основной документации на Android Developers (см. ссылки в конце статьи).

Базовые понятия


Service – это компонентAndroid приложения без интерфейса пользователя, предназначенныйдля осуществления ресурсоемких и/или длительных операций.

Типы Android сервисов

  • Started (запущенные) — сервисы которые запускаются любым другим компонентом приложения (Activity, BrodcastReceiver, Service) и работают пока не остановят сами себя или кто-то не остановит их.
  • Bound (связанные) — сервис который выступает в роли сервера в клиент-серверной архитектуре. Такой сервис создается при первом соединении(запросе) от другого компонента приложения.Сервис останавливается, когда отсоединится последний клиент.
  • Сервис может быть одновременно и Started и Bound. Такой сервис способен «жить вечно» и обслуживать запросы клиентов.

Сервис может быть запущен в отдельном от Activity процессе.

Преимущества:
  • максимальный размер памяти увеличивается в 2 раза, например 32 МБ/процесс (зависит от платформы).
  • GC ведет себя менее агрессивно, если у вас есть 2 процесса со snapshot в N МБкаждый, чем 1 процесс и 2*N МБ.
  • стандартные преимущества Android сервисов: background, независимость от Activity, прочее.

Недостатки:
  • дополнительные ресурсы системы на низкоуровневую сериализацию и десериализацию.
  • необходимость контроля жизненного цикла процесса.
  • немного больше кода.

AIDL

В буквальном переводе – язык описания интерфейсов Android. Используется для описания композиции и декомпозиции Java объектов в примитивы ОС для непосредственно передачи между процесами.
AIDL файлы очень похожи на стандартные интерфейсы в java за исключением:
  • Импортировать нужно даже те aidl файлы, которые находятся в том же пакете.
  • Ключевое слово oneway в декларации void метода означает что метод будет вызван асинхронно (клиент не дожидается его выполнения).
  • Использовать можно только примитивы, String, List и Parcelable классы, объявленные в других aidl файлах.

С помощью AIDL автоматически генерируется java код для генерации stab’ов.

Архитектура приложения


Разработанное нами приложение — галерея для Android которая позволяет просматривать фотографии из карты памяти и сетей обмена фотографиями.
Основными задачами сервиса в данном приложении являются: получение метаданных (информации о альбомах, фотографиях, друзьях), мониторинг их обновлений и всего остального, что с ними связано. Сервис постоянно хранит актуальную информацию и готов в любой момент отдать ее основной Activity для отображения.
Ниже будут приведены ключевые участки кода и описан процесс создания примитивного сервиса:
Для осуществления общения между сервисом и Activity используются следующие AIDL файлы:

IDataSourceService.aidl – интерфейс сервиса:
	packagecom.umobisoft.habr.aidlexample.common;
	import com.umobisoft.habr.aidlexample.common.IDataSourceServiceListener;
	interfaceIDataSourceService{
		voidloadAlbums(in IDataSourceServiceListener listener);
		…
	}


IDataSourceServiceListener.aidl – интерфейс слушателей сообщений от сервиса:
	package com.umobisoft.habr.aidlexample.common;
	import com.umobisoft.habr.aidlexample.common.pojo.Album;

	interface IDataSourceServiceListener{
		oneway void albumItemLoaded(in Album a);
	}


Данные передаются с помощью двух классов, которые реализуют интерфейс Parcelable — Album и Photo. Декларация aidl файлов для этих классов обязательна. При конвертации из примитивов ОС в java Объекты используется класс Creator.
Для записи данных используется метод writeToParcel интерфейса Parcelable:
	@Override
	public void writeToParcel(Parcel out, int flags) {
		try{
			out.writeLong(id);	
			out.writeString(name);
			out.writeTypedList(photos);
		}catch (Exception e) {
			Log.e(TAG, "writeToParcel", e);
		}
	}


Также существует вспомогательный метод describeContents, его задача описать специальные случаи/состояния объектакоторые когут использоватся при сериализации и десириализации:

	@Override
	public int describeContents() {
		// TODO Auto-generated method stub
		return 0;
	}


Методы чтения данных оказались недостойными вынесения их в состав интерфейса Parcelable, но стандартная практика – использование Creator вместе с:

	private void readFromParcel(Parcel in) {
		try{
			id = in.readLong();
			name = in.readString();

			photos.clear();
			in.readTypedList(photos, Photo.CREATOR);
		}catch (Exception e) {
			Log.e(TAG, "readFromParcel", e);
		}
	}	


Activity запускает сервис (делая его таким образом StartedService) в методе onCreate

@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		textView = (TextView)findViewById(R.id.album_text);
		Intent serviceIntent = newIntent(this, DataSourceService.class);
		startService(serviceIntent);
		connectToService();
    }


На этом же этапе жизненного цикла она соединяется с сервисом:

	private void connectToService() {
		Intent intent = newIntent(this, DataSourceService.class);        
		this.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
	}


Процесс соединения с сервисом асинхронный, в нем учувствует реализация интерфейса ServiceConnection. Во время соединения с сервером Activity регистрируется в сервисе как слушатель сообщений, с помощью имплементации IDataSourceServiceListener.Stub:

	private ServiceConnection serviceConnection = newServiceConnection() {
		@Override
		public void onServiceConnected(ComponentName name, IBinder service) {
			Log.i(TAG, "Service connection established");
	
			serviceApi = IDataSourceService.Stub.asInterface(service);
			
			try {
				mainListener = newIDataSourceServiceListener.Stub() {
					@Override
					publicvoidalbumItemLoaded(final Album a) throwsRemoteException {
						mToastHandler.post(new Thread(){
							publicvoid run(){
								Toast.makeText(HabrahabrAIDLExampleActivity.this, a.toString(), Toast.LENGTH_LONG).show();
								textView.setText(a.toString());
							}
						});
					}
				};
				serviceApi.loadAlbums(mainListener);
			} catch (RemoteException e) {
				Log.e(TAG, "loadAlbums", e);
			}
		}

		@Override
		publicvoidonServiceDisconnected(ComponentName name) {
			Log.i(TAG, "Service connection closed");
			serviceApi = null;
			connectToService();
		}
	};


Во время работы StartedService, в случае если к-во свободной памяти уменьшатся до определенного порога, система может убить сервис без предупреждения. После этого система обязана перезапустить сервис. Таким образом, в методе onServiceDisconnected мы опять инициализируем связь с сервисом.

Надеюсь выложенные исходные тексты помогут разработчикам ознакомится с AIDL. Архив с полным примером тут.
Основное приложение доступно для ознакомления в Android Market.

Официальная документация на AndroidDevelopers:
Services: developer.android.com/guide/topics/fundamentals/services.html
AIDL: developer.android.com/guide/developing/tools/aidl.html
  • +25
  • 25.1k
  • 7
Share post

Comments 7

    +1
    круто. а есть какая возможность дергать из сервиса callback в активити? броадкастами или без них?
      +1
      почитайте внимательно код примера… там как раз callback…
      mainListener = newIDataSourceServiceListener.Stub() {...}
      

      а потом
      serviceApi.loadAlbums(mainListener);
      +1
      Прошу прощения за оффтоп, но мне кажется следующие комментарии не будут лишними:
      а) существует мемори лик (если это гугл еще не починил): DataSourceService::apiEndpoint надо создавать через статичный класс
      б) необходимо использовать RemoteCallbackList вместо List в сервисе
      в) необходимо помнить что методы объекта apiEndpoint вызывается не в главном потоке сервиса и необходима либо синхронизация, либо использовать Handler как сделано в serviceConnection::onServiceConnected
        0
        Спасибо за коментарии, возможно мы действительно очень упростили пример. Синхронизация нужна, мемори ликов не наблюдали.
        +1
        Мемори лики появятся (опять же если в вашей SDK гугл это не пофиксил еще — не следил просто за данной темой) когда сервис будет создаваться и убиваться несколько раз. Если не ошибаюсь, вот тред на данную тему: code.google.com/p/android/issues/detail?id=6426
          0
          Ура! Ура! Только вот не хватает качества спутниковых снимков некоторых городов, для того что бы можно было рисовать поверх них народную карту.
          Помнится, обещались сделать спутниковые снимки (и даже панорамы) курортов России, а в итоге, ограничились черноморским побережем. Реквестую качественные спутниковые снимки Кавказких Минеральных Вод, например, а народную карту сознательные граждане сами нарисуют.
            0
            Таак. Я прошу прощения, промахнулся мимо вкладки.

          Only users with full accounts can post comments. Log in, please.