Правила использования AIDL API

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

AIDL можно использовать для определения API, когда приложениям нужно взаимодействовать друг с другом в фоновом процессе или с системой.

Для интерфейсов HAL используется стабильный AIDL с @VintfStability, который позволяет обновлять клиентские и серверные приложения независимо друг от друга. Для этого требуется обратная совместимость и структурированные данные.

Подробнее о разработке интерфейсов программирования в приложениях с помощью AIDL можно узнать в статье Язык описания интерфейсов Android (AIDL). Примеры использования AIDL на практике можно найти в статьях AIDL для HAL и Стабильный AIDL.

Управление версиями

Каждый обратно совместимый снимок AIDL API соответствует версии. Чтобы сделать снимок, выполните команду m <module-name>-freeze-api. При каждом выпуске клиента или сервера API (например, в составе основного пакета обновлений) необходимо сделать снимок и создать новую версию. Для API, обеспечивающих взаимодействие между системой и поставщиком, это должно происходить при ежегодном обновлении платформы.

Если интерфейс заморожен (сохранен в каталоге aidl_api с указанием версии), его нельзя изменять. Вы можете редактировать только каталог current. Вы можете безопасно добавлять методы в конец интерфейса, поля в конец parcelable, перечислители в перечисление и члены в объединение.

Клиенты, вызывающие новые методы на старых серверах, получают ошибку UNKNOWN_TRANSACTION, которую клиент должен корректно обработать.

Подробнее о том, какие изменения разрешены, можно узнать в статье Версии интерфейсов.

Зависимости сборки

Модули Android не могут зависеть от нескольких разных версий библиотек, сгенерированных из aidl_interface. Разные версии библиотек определяют одни и те же типы в одних и тех же пространствах имен. Система сборки aidl для Android выявляет эту проблему и выдает ошибку для каждого графа зависимостей, который заканчивается несовпадающими версиями библиотек.

Это может затруднить обновление одной версии общего интерфейса, когда модуль содержит много зависимостей со своими собственными зависимостями.

Разработчики могут использовать aidl_interface_defaults, чтобы объявлять зависимости общего интерфейса от других интерфейсов, чтобы не обновлять их все по отдельности.

Для организации зависимостей в сгенерированных библиотеках рекомендуем использовать модули *_defaults (например, rust_defaults, cc_defaults, java_defaults). Обычно по умолчанию используется версия интерфейсов latest, а также предыдущие версии, если они все ещё используются.

Разработчики могут использовать aidl_interface_defaults, чтобы объявить зависимости общего интерфейса от других интерфейсов. Это позволит не обновлять их по отдельности.

Рекомендации по разработке API

Общие

1. Сохраняйте все документы

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

2. Корпус

Используйте стиль UpperCamelCase для типов и стиль lowerCamelCase для методов, полей и аргументов. Например, MyParcelable для типа Parcelable и anArgument для аргумента. Аббревиатуры считаются за одно слово (NFC -> Nfc).

[-Wconst-name] Значения перечислений и константы должны быть ENUM_VALUE и CONSTANT_NAME

3. Не требуйте глобальных знаний

API не должны предполагать, что разработчики обладают глобальными знаниями всей базы кода или конкретными знаниями в определенной области. При работе с идентификаторами, относящимися к определенному домену (например, названиями устройств, идентификаторами или псевдонимами):

  • Укажите, откуда берутся эти идентификаторы и в каком они формате, если это важно для обеих сторон интерфейса.
  • Также можно использовать идентификаторы, относящиеся к определенному интерфейсу (например, объекты Binder или специальные токены), и поручить одной из сторон управлять сопоставлением с базовыми значениями. Это позволяет избежать конфликтов и не требует от пользователей понимания деталей реализации за пределами их области.

4. Все данные структурированы и имеют обратную совместимость.

Неструктурированные данные, такие как string, byte[] и общая память, должны иметь стабильный формат содержимого или быть непрозрачными для одной из сторон интерфейса.

Например, строковый аргумент, используемый в качестве сообщения об ошибке для результата, может быть получен и зарегистрирован для отладки, но его нельзя анализировать и интерпретировать, поскольку формат и содержимое могут быть несовместимы с предыдущими версиями. Если другой стороне интерфейса нужно знать, какая ошибка произошла во время выполнения, используйте перечисление, константу или ServiceSpecificException.

Кроме того, не сериализуйте объекты в byte[] или общую память, если они не стабильны и не поддерживают обратную совместимость. В некоторых случаях вы можете использовать аннотацию @FixedSize для совместного использования посылок и объединений в общей памяти и очередях быстрых сообщений.

Интерфейсы

1. Название

[-Winterface-name] Название интерфейса должно начинаться с символа I, например IFoo.

2. Избегайте больших интерфейсов с объектами на основе идентификаторов

Если с определенным API связано много вызовов, лучше использовать вложенные интерфейсы. Это дает следующие преимущества:

  • Упрощает понимание кода клиента или сервера
  • Упрощает жизненный цикл объектов.
  • Использует преимущества связывателей, которые невозможно подделать.

Не рекомендуется: один большой интерфейс с объектами на основе идентификаторов

interface IManager {
   int getFooId();
   void beginFoo(int id); // clients in other processes can guess an ID
   void opFoo(int id);
   void recycleFoo(int id); // ownership not handled by type
}

Рекомендуется: отдельные интерфейсы

interface IManager {
    IFoo getFoo();
}

interface IFoo {
    void begin(); // clients in other processes can't guess a binder
    void op();
}

3. Не смешивайте односторонние и двусторонние методы

[-Wmixed-oneway] Не смешивайте односторонние и двусторонние методы, поскольку это усложняет понимание модели потоков для клиентов и серверов. В частности, при чтении клиентского кода определенного интерфейса необходимо для каждого метода выяснять, будет ли он блокировать выполнение других операций.

4. Не возвращайте коды статуса

Методы не должны возвращать коды статуса, поскольку все методы AIDL имеют неявный код статуса возврата. Ознакомьтесь с ServiceSpecificException или EX_SERVICE_SPECIFIC. По соглашению эти значения определяются как константы в интерфейсе AIDL. Если вместе с ошибкой требуется указать пользовательскую задержку или уникальные данные об ошибке, то только в этом случае пользовательский объект ответа должен представлять ошибку. Подробную информацию можно найти в разделе Обработка ошибок.

5. Массивы как выходные параметры считаются вредными

[-Wout-array] Методы с выходными параметрами массива, такие как void foo(out String[] ret), обычно неэффективны, поскольку размер выходного массива должен быть объявлен и выделен клиентом в Java, поэтому размер выходного массива не может быть выбран сервером. Это нежелательное поведение происходит из-за того, как массивы работают в Java (их нельзя перераспределить). Вместо этого используйте API, например String[] foo().

6. Избегайте параметров inout

[-Winout-parameter] Это может запутать клиентов, поскольку даже параметры in выглядят как параметры out.

7. Избегайте параметров out и inout @nullable, не являющихся массивами

[-Wout-nullable] Поскольку серверная часть Java не обрабатывает аннотацию @nullable, а другие серверные части обрабатывают, out/inout @nullable T может привести к непоследовательному поведению в разных серверных частях. Например, серверные части, написанные не на Java, могут задать для выходного параметра @nullable значение null (в C++ это std::nullopt), но клиентская часть на Java не сможет прочитать это значение как null.

8. Используйте уникальные запросы и ответы

Сгруппируйте все необходимые параметры в один вход parcelable. Создавайте отдельные объекты Parcelable для запросов и ответов для каждого метода интерфейса вместо передачи примитивных типов (например, используйте ComputeResponse compute(in ComputeRequest request) вместо передачи отдельных переменных). Это позволяет добавлять новые аргументы позже, не меняя сигнатуру функции. Этот подход рекомендуется использовать, если в будущем планируется добавить больше параметров или если в методе уже больше четырех параметров.

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

Если метод не был создан с использованием этого шаблона, вы можете переключиться на него, создав новый метод с запросом и ответом parcelable и объявив старый метод устаревшим. Пример:

void foo(int a, int b, int c); // original version, but deprecated in favor of the next version
void fooV2(in MyArg arg); // new version having int a, b, c, and d.

Структурированные объекты Parcelable

1. Когда использовать

Используйте структурированные объекты Parcelable, если нужно отправить несколько типов данных.

Или если у вас один тип данных, но вы ожидаете, что в будущем вам понадобится его расширить. Например, не используйте String username. Используйте расширяемый объект Parcelable, например следующий:

parcelable User {
    String username;
}

В будущем вы сможете расширить его следующим образом:

parcelable User {
    String username;
    int id;
}

2. Явно укажите значения по умолчанию

[-Wexplicit-default, -Wenum-explicit-default] Указывайте явные значения по умолчанию для полей. Когда в Parcelable добавляются новые поля, старые клиенты и серверы их игнорируют, а новые клиенты и серверы автоматически заполняют их значениями по умолчанию.

3. Используйте ParcelableHolder для расширений поставщиков

Если вы определили объект parcelable AOSP, который должны расширить разработчики устройств, встройте в свой объект экземпляр ParcelableHolder. Это позволяет расширять код без конфликтов слияния. Это похоже на расширения прикрепленного интерфейса, но позволяет разработчикам включать собственные parcelable вместе с существующими parcelable без создания собственного интерфейса и типов.

4. Структуры данных

  • Используйте массивы или List сериализуемых объектов для представления карт, поскольку AIDL не поддерживает типы Map, которые безопасно преобразуются во всех нативных бэкендах (например, FeatureToScoreEntry[]).
  • Для повторяющихся полей используйте массивы объектов parcelable, а не массивы примитивов, чтобы в будущем не потребовались параллельные массивы.
  • Используйте объекты parcelable со строгой типизацией вместо сериализованных строк или JSON через IPC.
  • Используйте перечисления вместо логических значений для состояний, чтобы обеспечить возможность дальнейшего расширения. Для битовых масок используйте типы const int, а не enum, чтобы избежать громоздкого приведения типов в некоторых бэкендах.

Неструктурированные объекты Parcelable

1. Когда использовать

Неструктурированные объекты Parcelable доступны в Java с помощью @JavaOnlyStableParcelable и в NDK с помощью @NdkOnlyStableParcelable. Обычно это старые и существующие объекты Parcelable, которые нельзя структурировать.

Константы и перечисления

1. Для битовых полей следует использовать константные поля

Для битовых полей следует использовать постоянные поля (например, const int FOO = 3; в интерфейсе).

2. Перечисления должны быть закрытыми наборами.

Перечисления должны быть закрытыми наборами. Примечание. Добавлять элементы enum может только владелец интерфейса. Если поставщикам или производителям оригинального оборудования нужно расширить эти поля, им потребуется альтернативный механизм. По возможности следует использовать функции, предоставляемые поставщиком. Однако в некоторых случаях специальные значения поставщиков могут быть разрешены (хотя у поставщиков должен быть механизм для управления версиями, возможно, AIDL, чтобы они не могли конфликтовать друг с другом, и эти значения не должны быть доступны сторонним приложениям).

3. Не используйте такие значения, как "NUM_ELEMENTS".

Поскольку перечисления имеют версии, следует избегать значений, указывающих на количество элементов. В C++ эту проблему можно решить с помощью enum_range<>. Для Rust используйте enum_values(). В Java решения пока нет.

Не рекомендуется: использовать числовые значения

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. Избегайте лишних префиксов и суффиксов

[-Wredundant-name] Избегайте избыточных или повторяющихся префиксов и суффиксов в константах и перечислителях.

Не рекомендуется: использование избыточного префикса

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Рекомендуется: прямое указание перечисления

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] Использование FileDescriptor в качестве аргумента или возвращаемого значения метода интерфейса AIDL крайне не рекомендуется. Если AIDL реализован на Java, это может привести к утечке дескрипторов файлов, если не принять меры предосторожности. Если вы приняли FileDescriptor, вам нужно закрыть его вручную, когда оно больше не используется.

Для нативных бэкендов это безопасно, потому что FileDescriptor сопоставляется с unique_fd, который закрывается автоматически. Но независимо от того, какой язык вы используете, лучше не использовать FileDescriptor, поскольку это ограничит вашу свободу при выборе языка в будущем.

Вместо этого используйте ParcelFileDescriptor, который закрывается автоматически.

Единицы измерения переменных

Убедитесь, что в название включены единицы измерения переменных, чтобы они были понятны без обращения к документации.

Примеры

long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good

double energy; // Bad
double energyMilliJoules; // Good

int frequency; // Bad
int frequencyHz; // Good

Временные метки должны указывать на цифровой отпечаток

Временные метки (как и все единицы измерения) должны быть четко обозначены и иметь точки отсчета.

Примеры

/**
 * Time since device boot in milliseconds
 */
long timestampMs;

/**
 * UTC time received from the NTP server in units of milliseconds
 * since January 1, 1970
 */
long utcTimeMs;

Параллелизм и асинхронные операции

Для продолжительных операций используйте асинхронный интерфейс (oneway), чтобы избежать блокировки.

Если сервис не доверяет своим клиентам, все обратные вызовы, которые он получает от клиентов, должны быть интерфейсами oneway. Это не позволяет клиентам блокировать сервис на неопределенный срок.

Структурировать асинхронные API, состоящие из прямого вызова, входных аргументов и интерфейса обратного вызова для получения результатов. Рекомендации по аргументам приведены в разделе Используйте уникальные запросы и ответы.