
Привет, Хабр. Меня зовут Владислав Пурьев. Я занимаюсь разработкой экосистемных и комплексных сервисов в VK. В предыдущей статье мы познакомились с теоретическими основами языка описания интерфейсов (IDL).
Эта статья посвящена платформенному механизму в Android — Android Interface Definition Language (AIDL), в основе которого лежат оригинальные идеи IDL.
Мы проведём подробный теоретический разбор того, как удалённый вызов процедур (RPC) и IDL реализованы в системе Android и как AIDL вообще работает.
На момент публикации материалы в этой статье актуальные, а полные версии кода можно найти в вашей IDE, в проектеAOSP или на сайте Android Code Search.
Когда AIDL нужен разработчику
Существует два основные ситуации, когда разработчику требуется использовать AIDL:
Разработка устройства на Android.
Разработка главного приложения, которое может предоставлять некоторые функции другим приложениям. Оно должно быть установлено на устройстве пользователя, потому что другое приложение не сможет предоставить нужную функциональность без главного приложения.
В любом случае они требуют мощи AIDL только для одной цели — взаимодействия между разными процессами.
Большинство частей системы Android и приложения являются изолированными. Каждое приложение имеет свою собственную виртуальную машину и ресурсы. Но иногда требуются дополнительные данные или функции, которые не относятся к вашей программе.
Когда одному процессу необходимы функции другого внешнего процесса, выполняется межпроцессное взаимодействие, или IPC (Interprocess Communication). Подробнее об этом понятии можно прочитать здесь. Например, когда вы разрабатываете или используете функцию «Поделиться», варианты в диалоге не являются вашими приложениями, но вы можете использовать их и передать им свои ресурсы.
Можно использовать AIDL, чтобы приложения взаимодействовали с другими приложениями или сервисами, то есть реализовать клиент-серверную модель внутри Android. Потому что приложения запускаются на разных виртуальных машинах.
Пример
Рассмотрим простой пример IPC между двумя приложениями, реализованного с помощью AIDL. Для этого разработаем два небольших приложения.
Первое приложение — сервер, который предоставляет некоторую функцию клиентской стороне. Для начала требуется создать файл с расширением .aidl (подробнее здесь). Для добавления файла в Android Studio проще всего открыть меню (нажав ПКМ или через вкладку File) и в варианте New выбрать AIDL, после этого — AIDL File. В нём будет написано следующее:
// ICustomRemoteService.aidl
package com.report.aidlserver;
// Declare any non-default types here with import statements
interface ICustomRemoteService {
String getProcessId(String someInput);
}Выглядит как обычный интерфейс на Java. Но его нельзя использовать в Java или Kotlin напрямую.
Этот интерфейс должен быть реализован новым Android-сервисом. Так как приложения обычно разрабатываются с помощью Java или Kotlin, они не могут реализовать интерфейс в файле .aidl напрямую. И теперь настала очередь одной из основных идей IDL, а именно компиляторов. Чтобы они заработали, необходимо собрать проект.
С определённой версии Gradle-плагина для генерации интерфейсов из файлов .aidl требуется явно включить работу с AIDL в файле build.gradle / build.gradle.kts. На момент написания статьи это можно сделать в блоке
buildFeatures { }с помощью кодаaidl = true. Иначе сборка проекта не приведёт к генерации нужного файла.
После сборки будет создан новый файл ICustomRemoteService.java. Он появится в секции проекта по адресу build/generated/aidl_source_output_dir. Этот файл покажу чуть позже. Теперь возможно реализовать интерфейс, который в нём находится. Класс сервиса простой, и его создание не сильно отличается от создания связанного сервиса (bound service):
class CustomService : Service() {
private val binder: ICustomRemoteService.Stub = object : ICustomRemoteService.Stub() {
override fun getProcessId(someInput: String): String =
"$someInput${Process.myPid()}"
}
override fun onBind(intent: Intent?): IBinder? =
binder
}Этого кода достаточно, чтобы два Android-приложения начали взаимодействовать. Основная идея заключается в том, чтобы просто возвращать клиентскому приложению значение ID удалённого процесса, заключённого в специальную строку.
Посмотрим на клиентскую часть. Клиентское приложение — это отдельная программа, которая не является частью серверной стороны. Для начала должен быть создан файл .aidl с точно таким же кодом и названием пакета. Это важное условие, так как требуется подключиться к удалённому сервису, который находится в другом приложении, — то есть у вас нет доступа к классу сервиса, как это бывает при работе с локальными сервисами.
Определение пакета с точно таким же названием, что и на серверной стороне, необходимо для выполнения посылки явного intent (explicit intent), чтобы привязать сервис. Это специфика системы Android: файл .aidl для клиентского приложения должен быть таким же, что и для серверного. Даже название пакета должно быть идентичным.
Затем нужно не забыть включить aidl в файле build.gradle / build.gradle.kts, собрать проект и привязать сервис в классе Activity.
class MainActivity : AppCompatActivity() {
private var customService: ICustomRemoteService? = null
private val customServiceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
// Прочий код
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
customService = ICustomRemoteService.Stub.asInterface(service)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
// Прочий код
}
override fun onStart() {
super.onStart()
bindCustomService()
}
private fun bindCustomService() {
val intent = Intent("com.report.aidlserver.CustomService")
intent.setPackage("com.report.aidlserver")
bindService(intent, customServiceConnection, Context.BIND_AUTO_CREATE)
}
}Начнём чтение этого кода с конца: привязка выполняется без указания имени класса сервиса, только с использованием названия пакета. Не обязательно указывать аргумент action для экземпляра intent. Но если есть несколько сервисов, использующих один и тот же AIDL, стоит указать дополнительные детали для правильного взаимодействия. В любом случае необходимоопределить intent-фильтр в приложении-сервере (подробнее об этом здесь). МетодbindService() вызывается точно так же, как и для локального сервиса.
В колбэке onServiceConnected() можно увидеть, что экземпляр сервиса создаётся с помощью вызова метода asInterface() у класса Stub, который находится в ICustomRemoteService. В этот метод передаётся параметр, называемый service, имеющий тип IBinder.
Теперь расскажу подробнее о том, что генерируется без участия человека, — интерфейсе ICustomRemoteService.
Что генерирует компилятор?
Сейчас мы увидим, что AIDL полностью основан на идеях концепций IDL и RPC. Дальше много кода — и мы будем его исследовать строка за строкой, начиная сверху.
public interface ICustomRemoteService extends android.os.IInterface
{
// Остальной код будет рассмотрен далее
}Сгенерированный файл — это просто интерфейс, который расширяет другой интерфейс — IInterface, расположенный в пакете android.os:
/**
* Base class for Binder interfaces. When defining a new interface,
* you must derive it from IInterface.
*/
public interface IInterface
{
/**
* Retrieve the Binder object associated with this interface.
* You must use this instead of a plain cast, so that proxy objects
* can return the correct result.
*/
public IBinder asBinder();
}Это базовый интерфейс для наследников класса Binder, он содержит только один метод и больше ничего. Вернёмся к сгенерированному файлу.
public interface ICustomRemoteService extends android.os.IInterface
{
/** Default implementation for ICustomRemoteService. */
public static class Default implements com.report.aidlserver.ICustomRemoteService
{
@Override public java.lang.String getProcessId(java.lang.String someInput) throws android.os.RemoteException
{
return null;
}
@Override
public android.os.IBinder asBinder() {
return null;
}
}
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.report.aidlserver.ICustomRemoteService
{
// Этот код будет показан позже
}
// Прочий код
}Здесь можно увидеть статический вложенный класс, который называется Default. И этот класс реализует свой внешний интерфейс ICustomRemoteService, но переопределённые методы ничего не делают и возвращают null. Пока что в сгенерированном коде нет чего-то экстраординарного. За исключением следующего вложенного класса — Stub.
Класс Stub
Он тоже реализует интерфейс ICustomRemoteService и, самое главное, является наследником класса Binder.
Binder — значимый и сложный инструмент (больше информации о нём можно найти здесь и внутри исходного кода AOSP). Пока же достаточно знать только три вещи:
Binderреализует интерфейс IBinder;в Android весь IPC проходит через
Binder;Binderявляется узким каналом — о нём я подробно рассказал здесь.
Класс Stub расширяет могущественный класс Binder и реализует интерфейс AIDL, который представлен в сгенерированном файле ICustomRemoteService.java.
// Прочий код в файле ICustomRemoteService.java
public static abstract class Stub extends android.os.Binder implements com.report.aidlserver.ICustomRemoteService
{
/** Construct the stub and attach it to the interface. */
@SuppressWarnings("this-escape")
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.report.aidlserver.ICustomRemoteService interface,
* generating a proxy if needed.
*/
public static com.report.aidlserver.ICustomRemoteService asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.report.aidlserver.ICustomRemoteService))) {
return ((com.report.aidlserver.ICustomRemoteService)iin);
}
return new com.report.aidlserver.ICustomRemoteService.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
// Этот код будет рассмотрен далее
}
// Прочий кодНа верху класса располагается конструктор. Метод attachInterface() используется для установки значения интерфейса и дескриптора Binder. Так как ICustomRemoteService является расширением IInterface, то допустимо передать экземпляр класса Stubв этот метод. Внутрь также передаётся некоторая константа DESCRIPTOR, которая в самом классе Stubотсутствует, хотя в предыдущих версиях она находилась именно в нём. Эта константа используется для уникальной идентификации объекта Binder. В настоящий момент имеет значение com.report.aidlserver.ICustomRemoteService и располагается во внешнем интерфейсе ICustomRemoteService. Этот конструктор и вызывается внутри класса CustomService (код выше).
Далее следует метод asInterface(). Он является целиком сгенерированным членом класса. Благодаря ему концепция RPC и реализована на Android.
Метод вызывается в MainActivity клиентского приложения (код выше). И клиентская сторона понятия не имеет, с какой реализацией работает, а знает только об интерфейсе.
Метод asInterface() получает экземпляр реализации IBinder (удалённый сервис). Значение DESCRIPTOR передаётся в метод queryLocalInterface(), который возвращает экземпляр IInterface. Это значение и получает вызывающая сторона. В противном случае создаётся и возвращается объект Proxy (о классе Proxy будет рассказано дальше). Подчеркну, что метод asInterface()вернёт экземпляр Proxy для удалённого вызова и наследника Stub (см. тело блока if выше) для локального вызова.
Метод asBinder() используется для получения правильной реализации IBinder. Он необходим для корректной работы с экземплярами Proxy и должен быть реализован во всех расширениях IInterface. Перейдём к следующей части класса Stub.
// Прочий код в файле ICustomRemoteService.java
public static abstract class Stub extends android.os.Binder implements com.report.aidlserver.ICustomRemoteService
{
// Этот код был рассмотрен выше
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION {
data.enforceInterface(descriptor);
}
if (code == INTERFACE_TRANSACTION) {
reply.writeString(descriptor);
return true;
}
switch (code)
{
case TRANSACTION_getProcessId:
{
java.lang.String _arg0;
_arg0 = data.readString();
java.lang.String _result = this.getProcessId(_arg0);
reply.writeNoException();
reply.writeString(_result);
break;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
return true;
}
// Этот код будет рассмотрен далее
}
// Прочий кодВыше я показал переопределённый метод onTransact() класса Binder. Сначала идёт блок if, который проверяет значение параметра code. Если он лежит в интервале от 1 до FFFFFF (значения констант из интерфейса IBinder в блоке if), то вызывается метод enforceInterface(). Этот метод в свою очередь вызывает нативный метод nativeEnforceInterface(), передавая дескриптор объекта IBinder — переменную descriptor в самом начале onTransact() — и значение поля mNativePtr, имеющего тип long. Сам метод enforceInterface()вызывается у параметра data, который является экземпляром класса Parcel.
С классом Parcel тоже не всё просто. Он используется для маршалинга и демаршалинга данных в реализации RPC/IPC на Android. Чтобы больше узнать об этом классе, можно обратиться к официальной документации и исходному коду.
Далее следует блок if, в котором значение дескриптора записывается в экземпляр класса Parcel(на этот раз параметр reply) в случае, если параметр codeимеет значение INTERFACE_TRANSACTION.
Затем идёт наиболее интересный и важный участок кода с блоком switch, реагирующий на значения параметра code. Обратите внимание, что TRANSACTION_getProcessId — это сгенерированная константа, первая часть которой написана заглавными буквами. А вторая часть — это имя метода интерфейса.
Количество подобных констант равно количеству методов в интерфейсе, но в файлах .aidl невозможно использовать перегруженные функции. Название константы зависит от имени метода. Сейчас есть только один метод и, следовательно, константа тоже одна.
В случае вызова со стороны клиентского приложения маршалинг данных выполняется за разработчика.
Сначала определяется первый аргумент удалённого метода. И чем больше аргументов у метода, тем больше переменных с именем
argN(где N — индекс аргумента) будет сгенерированоЗатем этот аргумент получает значение от метода readString() класса
Parcel. А что произойдёт, если удалённой функции требуется несколько параметров одного типа? Об этом не стоит беспокоиться —Parcelвызывает необходимые нативные методы, и все значения будут извлечены в нужный момент (больше о параметрах можно узнать в официальной документации)После инициализации всех аргументов вызывается удалённый метод (
getProcessId()) и выполняется его основная работаПолученный результат передаётся в метод writeString() параметра
reply, и затем блокswitchпрерывается
Метод onTransact() родительского класса вызывается только в ветке default блока switch. В конце переопределённая функция onTransact() возвращает true.
Теперь изучим класс Proxy.
Класс Proxy
public static abstract class Stub extends android.os.Binder implements com.report.aidlserver.ICustomRemoteService
{
// Этот код был рассмотрен выше
private static class Proxy implements com.report.aidlserver.ICustomRemoteService
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public java.lang.String getProcessId(java.lang.String someInput) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.lang.String _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(someInput);
boolean _status = mRemote.transact(Stub.TRANSACTION_getProcessId, _data, _reply, 0);
_reply.readException();
_result = _reply.readString();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
// Прочий код класса Stub
}Этот класс является приватным. У него есть одно поле mRemote, которое инициализируется в конструкторе. Следует запомнить следующую последовательность: клиентский код получает экземпляр IBinderвнутри переопределённого метода onServiceConnected(), этот объект IBinderпередаётся в метод asInterface(), затем, так как всё происходит на клиентской стороне, экземпляр класса Proxyсоздаётся и возвращается.

Таким образом, Proxyполучает реализацию IBinderиз узкого канала. То есть Binder работает с системным нативным кодом.
Следующие два метода довольно просты и не нуждаются в объяснении, а вот третий является тем, что в итоге и использует клиент.
Сначала создаются два экземпляра
Parcel: один — для входного значения, другой — для значения, которое возвращается из удалённого методаЗатем объявляется переменная
result. Тип переменнойresultсоответствует типу возвращаемого значения метода. Если метод объявлен без возвращаемого значения, то есть сvoidна Java, то переменная_resultне генерируетсяЭтот метод выбрасывает RemoteException, в связи с чем основная работа проходит внутри блока
try. Первым делом метод writeInterfaceToken() получает значение константыDESCRIPTOR. Внутри себя метод вызывает специальную функцию nativeWriteInterfaceToken()После этого входные параметры записываются в
Parcel(в коде выше — вызов writeString()) и вызывается метод transact() экземпляраIBinder, в который передаются все необходимые аргументы. И именно этот вызов может выбросить RemoteException. В результате, поскольку вызывается реализацияBinderинтерфейсаIBinder, метод transact() внутри вызываетonTransact(). МетодonTransact()представлен выше, в разделе «Класс Stub»Затем вызывается метод readException()у переменной
_replyВ самом конце переменная
resultполучает значение изreplyпутём вызова метода readString()
Подождите, что-то здесь не так! Как в Parcel попадает результат, если функция transact() возвращает значение типа boolean?
Переменная _reply типа Parcel передаётся в метод onTransact() (так как метод transact() внутри вызывает onTransact()), где и получает необходимые данные. Но этот метод является удалённым и данное взаимодействие — это не то же самое, что просто передать объект в метод другого класса на том же языке программирования. На самом деле всё довольно просто, если принимать некоторые вещи в нативном коде Android такими, какие они есть. Класс Proxyне использует экземпляр Binder, он использует BinderProxy. Посмотрим на его метод transact().
public final class BinderProxy implements IBinder {
// Предыдущий код BinderProxy
/**
* Perform a binder transaction on a proxy.
*
* @param code The action to perform. This should
* be a number between {@link #FIRST_CALL_TRANSACTION} and
* {@link #LAST_CALL_TRANSACTION}.
* @param data Marshalled data to send to the target. Must not be null.
* If you are not sending any data, you must create an empty Parcel
* that is given here.
* @param reply Marshalled data to be received from the target. May be
* null if you are not interested in the return value.
* @param flags Additional operation flags. Either 0 for a normal
* RPC, or {@link #FLAG_ONEWAY} for a one-way RPC.
*
* @return
* @throws RemoteException
*/
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
// Этот код не важен
try {
final boolean result = transactNative(code, data, reply, flags);
// Этот код не важен
return result;
} finally {
AppOpsManager.resumeNotedAppOpsCollection(prevCollection);
if (transactListener != null) {
transactListener.onTransactEnded(session);
}
if (tracingEnabled) {
Trace.traceEnd(Trace.TRACE_TAG_ALWAYS);
}
}
}
// Остальной код класса BinderProxy
}Самое интересное здесь — это метод transactNative(). После того как метод получает параметр reply, вся остальная работа переходит в нативный код, и затем остальные операции выполняются на удалённой стороне.
В конце реализации удалённого метода в классе Proxy(в нашем случае это метод getProcessId()) вызывается функция recycle() для обоих экземпляров Parcel. После этого объект не должен использоваться. Более подробно о классе Parcel здесь.
И в самом конце результат возвращается вызывающей стороне. Пойдём дальше.
Окончание сгенерированного интерфейса
public interface ICustomRemoteService extends android.os.IInterface
{
// Прочий код
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.report.aidlserver.ICustomRemoteService
{
// Этот код был рассмотрен выше
private static class Proxy implements com.report.aidlserver.ICustomRemoteService
{
// Этот код был рассмотрен выше
}
static final int TRANSACTION_getProcessId = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
}
/** @hide */
public static final java.lang.String DESCRIPTOR = "com.report.aidlserver.ICustomRemoteService";
public java.lang.String getProcessId(java.lang.String someInput) throws android.os.RemoteException;
}В самом низу класса Stub находится сгенерированная константа TRANSACTION_getProcessId, которая используется в переопределённом методе onTransact() (код выше). Она имеет значение "0x00000001" (то есть значение android.os.IBinder.FIRST_CALL_TRANSACTION + 0).
Если бы в файле .aidl было больше одного метода, тогда следующая константа имела бы значение, равное FIRST_CALL_TRANSACTION, увеличенной на 1, то есть "0x00000002". Соответственно, третья константа имела бы значение "0x00000003" и так далее.
Уже в конце внешнего интерфейса находится константа DESCRIPTORи ещё один сгенерированный метод getProcessId(). Правда, теперь он является исключительно функцией, объявленной в интерфейсе, а до этого было сгенерировано много «неинтерфейсного» кода.
Как можно заметить, в одном файле присутствуют две заглушки: Stub — для локальных вызовов и Proxy — для удалённых. Класс Stub реализует интерфейс ICustomRemoteService, но не реализует его метод, потому что Stub является абстрактным классом.
Полную схему взаимодействий в AIDL можно представить следующим образом:

Если интересно, то можно сравнить схему выше с этой, которая описывает RPC от Microsoft. Небольшое отличие объясняется разницей в технологиях, но концепция и техники одинаковы.
Что делает AIDL и зачем ему Binder и Parcel
Итак, у нас есть два основных персонажа для связи между двумя приложениями. Первый из них — Binder, основной двигатель IPC в Android.
Второй — это Parcel, специальная форма аргументов для удалённого вызова процедур в Android. Parcel — это обработанные (marshalled) данные. Объект с этими данными — экземпляр Parcel — может быть передан между двумя разными приложениями или процессами через ядро системы Android.
Binder выполняет различные операции с Parcel в зависимости от действий, которые требуются удалённой стороне от другого приложения. Окей, а при чём здесь AIDL?
Именно для IPC сам AIDL не имеет никакого значения. Действительно, он не делает ничего для создания связи между процессами, только вызывает различные методы у экземпляров Binder и Parcel.
AIDL — это просто удобный механизм для разработчиков. В этой статье большое количество кода только для удалённого сервиса с одним методом. А о чём беспокоился разработчик во всём процессе? Только о том, что указано в разделе «Пример». Остальное было разработано специальным компилятором Android IDL. Действительно, только представьте себе — более сотни строчек кода только для одного метода. А если бы методов было 20? Каждый раз надо создать Parcel, занести в него данные, вызвать Binder, позаботиться об исключениях и прочее, а потом повторить то же самое столько раз, сколько методов в интерфейсе. И не забывайте, что у вас два приложения — клиентское и серверное.
Разработка заглушек уровня исходного кода из предыдущей статьи не так уж сложна, но это утомляет. AIDL генерирует рутинный код за разработчика и автоматизирует действия для реализации RPC и IPC, скрывая конкретные низкоуровневые детали.
Компиляторы AIDL находятся в коде AOSP в директории директория_с_aosp/system/tools/aidl.

Эта директория содержит файлы, которые называются aidl_to_cpp.cpp и aidl_to_java.cpp, потому что файлы .aidl можно использовать для генерации кода как на Java, так и на C++.
Заключение
Мы разобрали интерфейс с одним методом и Java-файл, который по нему сгенерировал компилятор. AIDL не реализует IPC самостоятельно. Parcel упаковывает аргументы и результат, Binder используется для передачи, а компилятор AIDL разрабатывает код, который их связывает: Proxy для клиента и Stub для сервера. Вам остаётся описать интерфейс в .aidl, расширить Stub в сервисе и привязать сервис в клиенте. Следите, чтобы файлы .aidl в обоих приложениях совпадали.
Хотите копнуть глубже? Читайте материалы по ссылкам ниже.
Полезные ссылки
Android Сode Search — исходный код системы Android, включая нативный код
Android Interface Definition Language (AIDL) — официальная документация
Deep Dive into Android IPC/Binder Framework — большое исследование IPC в Android и механизма Binder в частности
Связанные сервисы (Bound services) — официальная документация по связанным сервисам
Parcel — официальная документация о Parcel
Android Open Source Project — мои исследования начались отсюда

Комментарии 0