Рекомендации по использованию API AIDL

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

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

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

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

Версионирование

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

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

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

Для получения более подробной информации о типах разрешенных изменений см. раздел «Интерфейсы версионирования» .

Создать зависимости

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

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

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

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

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

рекомендации по проектированию API

Общий

1. Документируйте всё.

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

2. Обсадная труба

Используйте верхний регистр camel case для типов и нижний регистр camel case для методов, полей и аргументов. Например, MyParcelable для типа, допускающего последовательность Parcelable, и anArgument для аргумента. Что касается аббревиатур, рассматривайте их как слова ( NFC -> Nfc ).

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

3. Избегайте требования глобальных знаний.

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

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

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

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

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

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

Интерфейсы

1. Название

[-Winterface-name] Название интерфейса должно начинаться с I like 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. Использование массивов в качестве выходных параметров считается вредным.

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

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

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

7. Избегайте параметров out и inout, допускающих значение null и не являющихся массивами.

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

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

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

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

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

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.

Структурированные пакеты

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

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

Или, например, если у вас один тип данных, но вы ожидаете, что в будущем вам потребуется его расширить. Например, не используйте 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 объектов типа Parcelable, поскольку AIDL изначально не поддерживает типы Map , которые можно безопасно преобразовать во все собственные бэкенды (например, FeatureToScoreEntry[] ).
  • Для повторяющихся полей используйте массивы объектов, parcelable , вместо массивов примитивных типов, чтобы в будущем избежать необходимости в параллельных массивах.
  • Вместо сериализованных строк или JSON через межпроцессное взаимодействие используйте строго типизированные объекты, parcelable objects).
  • Используйте перечисления (enum) вместо логических значений (boolean) для состояний, чтобы обеспечить возможность дальнейшего расширения. Для битовых масок используйте константные целочисленные типы const int вместо перечислений enum , чтобы избежать громоздкого приведения типов в некоторых бэкендах.

Неструктурированные пакеты

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

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

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

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

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

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

Перечисления должны быть замкнутыми множествами. Примечание: добавлять элементы перечисления может только владелец интерфейса. Если поставщикам или OEM-производителям необходимо расширять эти поля, необходим альтернативный механизм. По возможности следует отдавать предпочтение интеграции функциональности поставщика. Однако в некоторых случаях могут быть разрешены пользовательские значения от поставщика (хотя у поставщиков должен быть механизм версионирования, возможно, сам 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, состоящие из прямого вызова, входных аргументов и интерфейса обратного вызова. Рекомендации по использованию аргументов см. в разделе «Использование уникальных запросов и ответов» .