Las prácticas recomendadas que se describen aquí sirven como guía para desarrollar interfaces de AIDL de manera eficaz y con atención a la flexibilidad de la interfaz, en especial cuando se usa AIDL para definir una API estable y retrocompatible.
Se puede usar AIDL para definir una API cuando las apps necesitan interactuar entre sí en un proceso en segundo plano o con el sistema.
El AIDL estable con @VintfStability se usa para las interfaces de HAL y permite que los clientes y los servidores se actualicen de forma independiente. Esto requiere retrocompatibilidad y datos estructurados.
Para obtener más información sobre el desarrollo de interfaces de programación en apps con AIDL, consulta Lenguaje de definición de la interfaz de Android (AIDL). Para ver ejemplos prácticos del AIDL, consulta AIDL para HALs y AIDL estable.
Control de versiones
Cada instantánea compatible con versiones anteriores de una API de AIDL corresponde a una versión.
Para tomar una instantánea, ejecuta m <module-name>-freeze-api. Cada vez que se lanza un cliente o servidor de la API (por ejemplo, en una versión de Mainline), debes tomar una instantánea y crear una versión nueva. En el caso de las APIs de sistema a proveedor, esto debería ocurrir con la revisión anual de la plataforma.
Cuando una interfaz se congela (se guarda en el directorio aidl_api versionado), nunca se debe modificar. Solo puedes editar el directorio current. Puedes agregar de forma segura métodos al final de una interfaz, campos al final de un objeto parcelable, enumeradores a una enumeración y miembros a una unión.
Los clientes que llaman a métodos nuevos en servidores más antiguos reciben un error UNKNOWN_TRANSACTION, que el cliente debe controlar correctamente.
Para obtener más detalles e información sobre el tipo de cambios que se permiten, consulta Control de versiones de interfaces.
Dependencias de la compilación
Los módulos de Android no pueden depender de varias versiones diferentes de las bibliotecas generadas a partir de un aidl_interface. Las diferentes versiones de las bibliotecas definen los mismos tipos en los mismos espacios de nombres. El sistema de compilación aidl de Android identifica este problema y arroja un error con cada uno de los gráficos de dependencia que terminan en las versiones no coincidentes de las bibliotecas.
Esto puede dificultar la actualización de una versión de una interfaz común cuando un módulo contiene muchas dependencias con sus propias dependencias.
Los desarrolladores pueden usar aidl_interface_defaults para declarar las dependencias de una interfaz compartida en otras interfaces, de modo que no sea necesario actualizar todas de forma independiente.
Te recomendamos que uses módulos *_defaults (como rust_defaults, cc_defaults, java_defaults) para organizar las dependencias de las bibliotecas generadas. Es común tener un valor predeterminado para la versión latest de las interfaces, así como valores predeterminados para las versiones anteriores si aún se usan.
Los desarrolladores pueden usar aidl_interface_defaults para declarar las dependencias de una interfaz compartida en otras interfaces, de modo que no sea necesario actualizar todas de forma independiente.
Lineamientos de diseño de APIs
General
1. Documente todo
- Documenta cada método para su semántica, argumentos, uso de excepciones integradas, excepciones específicas del servicio y valor de devolución.
- Documenta cada interfaz para su semántica.
- Documenta el significado semántico de las enumeraciones y las constantes.
- Documenta todo lo que pueda no estar claro para un implementador.
- Proporciona ejemplos cuando sea pertinente.
2. Carcasa
Usa la convención de escritura upper camel case para los tipos y lower camel case para los métodos, los campos y los argumentos. Por ejemplo, MyParcelable para un tipo parcelable y anArgument para un argumento. En el caso de las siglas, considera que son una palabra (NFC -> Nfc).
[-Wconst-name] Los valores y las constantes de enumeración deben ser ENUM_VALUE y CONSTANT_NAME.
3. Evita requerir conocimiento global
Las APIs no deben suponer que los desarrolladores tienen conocimientos globales de toda la base de código o experiencia específica en un dominio. Cuando se trata de identificadores específicos del dominio (como nombres, IDs o identificadores de dispositivos):
- Sé explícito y documenta de dónde provienen estos identificadores y su formato si es importante que ambas partes de la interfaz los conozcan.
- Como alternativa, usa identificadores específicos de la interfaz (como objetos de vinculador o tokens personalizados) y haz que un lado administre la asignación a los valores subyacentes. Esto reduce las colisiones y evita que los usuarios deban comprender los detalles de implementación fuera de su área.
4. Todos los datos están estructurados y son retrocompatibles.
Los datos no estructurados, como string, byte[] y la memoria compartida, deben tener un formato estable para su contenido o ser opacos para un lado de la interfaz.
Por ejemplo, un argumento de cadena que se usa como mensaje de error para un resultado se puede recibir y registrar para la depuración, pero no se debe analizar ni interpretar porque el formato y el contenido podrían no ser compatibles con versiones anteriores. Si el otro lado de la interfaz necesita saber cuál es el error en el tiempo de ejecución, usa un enum, una constante o ServiceSpecificException.
Del mismo modo, no serialices objetos en byte[] ni en memoria compartida, a menos que sean estables y retrocompatibles. En algunos casos, puedes usar la anotación @FixedSize para compartir objetos Parcelable y uniones en la memoria compartida y las filas de mensajes rápidos.
Interfaces
1. Nombre
[-Winterface-name] El nombre de una interfaz debe comenzar con I, como IFoo.
2. Evita una interfaz grande con "objetos" basados en IDs
Prefiere las subinterfaces cuando hay muchas llamadas relacionadas con una API específica. Esto proporciona los siguientes beneficios:
- Facilita la comprensión del código del cliente o del servidor
- Simplifica el ciclo de vida de los objetos
- Aprovecha que los binders no se pueden falsificar.
No se recomienda: Una sola interfaz grande con objetos basados en IDs
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
}
Recomendación: Interfaces individuales
interface IManager {
IFoo getFoo();
}
interface IFoo {
void begin(); // clients in other processes can't guess a binder
void op();
}
3. No mezcles métodos unidireccionales con bidireccionales
[-Wmixed-oneway] No mezcles métodos unidireccionales con métodos no unidireccionales, ya que esto dificulta la comprensión del modelo de subprocesos para clientes y servidores. Específicamente, cuando lees el código del cliente de una interfaz en particular, debes buscar para cada método si ese método se bloqueará o no.
4. Evita devolver códigos de estado
Los métodos deben evitar los códigos de estado como valores de retorno, ya que todos los métodos de AIDL tienen un código de retorno de estado implícito. Consulta ServiceSpecificException o EX_SERVICE_SPECIFIC. Por convención, estos valores se definen como constantes en una interfaz de AIDL. Si se necesita un retraso personalizado o datos de error únicos junto con un error, ese es el único momento en que un objeto de respuesta personalizado debe representar un error. Para obtener información más detallada, consulta Manejo de errores.
5. Los arrays como parámetros de salida se consideran dañinos
[-Wout-array] Los métodos que tienen parámetros de salida de array, como void foo(out String[] ret), suelen ser malos porque el cliente debe declarar y asignar el tamaño del array de salida en Java, por lo que el servidor no puede elegir el tamaño de la salida del array. Este comportamiento no deseado se produce debido a la forma en que funcionan los arrays en Java (no se pueden reasignar). En su lugar, prefiere APIs como String[] foo().
6. Evita los parámetros de entrada y salida
[-Winout-parameter] Esto puede confundir a los clientes, ya que incluso los parámetros in parecen parámetros out.
7. Evita los parámetros @nullable que no sean de array y que sean de entrada y salida
[-Wout-nullable] Dado que el backend de Java no controla la anotación @nullable, mientras que otros backends sí lo hacen, out/inout @nullable T puede generar un comportamiento incoherente en los diferentes backends. Por ejemplo, los backends que no son de Java pueden establecer un parámetro @nullable externo como nulo (en C++, se establece como std::nullopt), pero el cliente de Java no puede leerlo como nulo.
8. Usa solicitudes y respuestas únicas
Agrupa todos los parámetros necesarios en un solo parcelable de entrada.
Crea objetos Parcelable de solicitud y respuesta dedicados para cada método de interfaz en lugar de pasar primitivas (por ejemplo, usa ComputeResponse compute(in ComputeRequest request) en lugar de pasar variables separadas). Esto permite agregar argumentos nuevos más adelante sin cambiar la firma de la función. Se recomienda usar este patrón cuando se espera que se agreguen más parámetros en el futuro o si un método ya tiene más de cuatro parámetros.
Los métodos que no requieren entradas o salidas adicionales no se beneficiarán con esta sugerencia. Pensar de forma explícita en cada caso y mantener la flexibilidad para los cambios futuros puede generar menos métodos obsoletos y menos complejidad para el código retrocompatible.
Si un método no se creó con este patrón, puedes cambiar a él creando un método nuevo con un objeto Parcelable de solicitud y respuesta, y marcando el método anterior como obsoleto. Por ejemplo:
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.
Objetos Parcelables estructurados
1. Cuándo debe utilizarse
Usa objetos Parcelables estructurados cuando tengas varios tipos de datos para enviar.
O bien, cuando tienes un solo tipo de datos, pero esperas que deberás extenderlo en el futuro. Por ejemplo, no uses String username. Usa un objeto Parcelable extensible, como el siguiente:
parcelable User {
String username;
}
Para que, en el futuro, puedas extenderlo de la siguiente manera:
parcelable User {
String username;
int id;
}
2. Proporciona valores predeterminados de forma explícita
[-Wexplicit-default, -Wenum-explicit-default] Proporciona valores predeterminados explícitos para los campos. Cuando se agregan campos nuevos a un objeto Parcelable, los clientes y servidores antiguos los descartan, pero los valores predeterminados se completan automáticamente para los clientes y servidores nuevos.
3. Cómo usar ParcelableHolder para las extensiones del proveedor
Si defines un parcelable de AOSP que los implementadores de dispositivos deben extender, incorpora una instancia de ParcelableHolder en tu objeto. Esto actúa como un punto de extensión sin crear conflictos de combinación. Esto es similar a las extensiones de interfaz adjuntas, pero permite que los implementadores incluyan su propio parcelable junto con el parcelable existente sin crear su propia interfaz y tipos.
4. Estructuras de datos
- Usa arrays o
Listde objetos Parcelables para representar mapas, ya que AIDL no admite de forma nativa tiposMapque se traducen de forma segura en todos los backends nativos (por ejemplo,FeatureToScoreEntry[]). - Usa arrays de objetos
parcelablepara los campos repetidos en lugar de arrays de primitivos, para evitar la necesidad de arrays paralelos en el futuro. - Usa objetos
parcelablecon escritura segura en lugar de cadenas serializadas o JSON a través de la IPC. - Usa enumeraciones en lugar de booleanos para los estados y permitir la expansión en el futuro. Para las máscaras de bits, usa
const inten lugar de tiposenumpara evitar conversiones engorrosas en algunos backends.
Objetos Parcelable no estructurados
1. Cuándo debe utilizarse
Los objetos Parcelable no estructurados están disponibles en Java con @JavaOnlyStableParcelable y en el backend del NDK con @NdkOnlyStableParcelable. Por lo general, se trata de objetos Parcelable antiguos y existentes que no se pueden estructurar.
Constantes y enumeraciones
1. Los campos de bits deben usar campos constantes
Los campos de bits deben usar campos constantes (por ejemplo, const int FOO = 3; en una interfaz).
2. Los enums deben ser conjuntos cerrados.
Los enums deben ser conjuntos cerrados. Nota: Solo el propietario de la interfaz puede agregar elementos de enumeración. Si los proveedores o los OEM necesitan extender estos campos, se requiere un mecanismo alternativo. Siempre que sea posible, se debe preferir la funcionalidad del proveedor que se incorpora al upstream. Sin embargo, en algunos casos, se pueden permitir valores personalizados del proveedor (aunque los proveedores deben tener un mecanismo para versionar esto, tal vez AIDL en sí, no deberían poder entrar en conflicto entre sí y estos valores no deberían exponerse a apps de terceros).
3. Evita valores como "NUM_ELEMENTS".
Dado que los enums tienen versiones, se deben evitar los valores que indican cuántos valores hay. En C++, esto se puede solucionar con enum_range<>. En Rust, usa enum_values(). En Java, aún no hay una solución.
No se recomienda: Usar valores numerados
@Backing(type="int")
enum FruitType {
APPLE = 0,
BANANA = 1,
MANGO = 2,
NUM_TYPES, // BAD
}
4. Evita los prefijos y sufijos redundantes
[-Wredundant-name] Evita los prefijos y sufijos redundantes o repetitivos en las constantes y los enumeradores.
No se recomienda: Usar un prefijo redundante
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
Recomendado: Asignar un nombre directamente a la enumeración
enum MyStatus {
GOOD,
BAD
}
FileDescriptor
[-Wfile-descriptor] Se desaconseja el uso de FileDescriptor como argumento o valor de devolución de un método de interfaz de AIDL. En especial, cuando el AIDL se implementa en Java, esto puede provocar una pérdida de descriptores de archivos, a menos que se controle con cuidado. Básicamente, si aceptas un FileDescriptor, debes cerrarlo de forma manual cuando ya no se use.
En el caso de los backends nativos, no hay problemas porque FileDescriptor se asigna a unique_fd, que se puede cerrar automáticamente. Pero, independientemente del lenguaje de backend que uses, es recomendable NO usar FileDescriptor en absoluto, ya que esto limitará tu libertad para cambiar el lenguaje de backend en el futuro.
En su lugar, usa ParcelFileDescriptor, que se puede cerrar automáticamente.
Unidades de la variable
Asegúrate de que las unidades de las variables se incluyan en el nombre para que sus unidades estén bien definidas y se comprendan sin necesidad de consultar la documentación de referencia.
Ejemplos
long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good
double energy; // Bad
double energyMilliJoules; // Good
int frequency; // Bad
int frequencyHz; // Good
Las marcas de tiempo deben indicar su referencia
Las marcas de tiempo (de hecho, todas las unidades) deben indicar claramente sus unidades y puntos de referencia.
Ejemplos
/**
* 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;
Operaciones simultáneas y asíncronas
Controla las operaciones de larga duración con una interfaz asíncrona (oneway) para evitar bloqueos.
Si un servicio no confía en sus clientes, todas las devoluciones de llamada que reciba de los clientes deben ser interfaces oneway. Esto evita que los clientes puedan bloquear el servicio de forma indefinida.
Estructura APIs asíncronas que constan de una llamada de reenvío, argumentos de entrada y una interfaz de devolución de llamada para obtener resultados. Consulta Usa solicitudes y respuestas únicas para obtener recomendaciones sobre los argumentos.