As práticas recomendadas descritas aqui servem como um guia para desenvolver interfaces AIDL de maneira eficaz e com atenção à flexibilidade da interface, principalmente quando a AIDL é usada para definir uma API estável e compatível com versões anteriores.
A AIDL pode ser usada para definir uma API quando os apps precisam interagir entre si em um processo em segundo plano ou com o sistema.
A AIDL estável com @VintfStability é usada para interfaces HAL e permite que clientes e servidores sejam atualizados de forma independente. Isso exige compatibilidade com versões anteriores e dados estruturados.
Para mais informações sobre como desenvolver interfaces de programação em apps com AIDL, consulte Linguagem de definição de interface do Android (AIDL). Para exemplos de AIDL na prática, consulte AIDL para HALs e AIDL estável.
Controle de versões
Cada snapshot compatível com versões anteriores de uma API AIDL corresponde a uma versão.
Para fazer um snapshot, execute m <module-name>-freeze-api. Sempre que um cliente ou servidor da API for lançado (por exemplo, em um trem do Mainline), será necessário fazer um snapshot e criar uma nova versão. Para APIs de sistema para fornecedor, isso precisa acontecer com a revisão anual da plataforma.
Quando uma interface é congelada (salva no diretório aidl_api com controle de versão), ela nunca pode ser modificada. Só é possível editar o diretório current. Você pode adicionar métodos com segurança ao final de uma interface, campos ao final de um parcelable, enumeradores a uma enumeração e membros a uma união.
Os clientes que chamam novos métodos em servidores mais antigos recebem um erro UNKNOWN_TRANSACTION, que precisa ser processado corretamente pelo cliente.
Para mais detalhes e informações sobre o tipo de mudanças permitidas, consulte Interfaces de controle de versões.
Dependências do build
Os módulos do Android não podem depender de várias versões diferentes das bibliotecas geradas de uma aidl_interface. As diferentes versões das bibliotecas definem os mesmos tipos nos mesmos namespaces. O sistema de build aidl do Android identifica esse problema e gera um erro com cada um dos gráficos de dependência que terminam nas versões incompatíveis das bibliotecas.
Isso pode dificultar a atualização de uma versão de uma interface comum quando um módulo contém muitas dependências com as próprias dependências.
Os desenvolvedores podem usar aidl_interface_defaults para declarar as dependências de uma interface compartilhada em outras interfaces para que elas não precisem ser atualizadas de forma independente.
Recomendamos usar módulos *_defaults (como rust_defaults, cc_defaults, java_defaults) para organizar as dependências nas bibliotecas geradas. É comum ter um padrão para a versão latest das interfaces, bem como padrões para versões anteriores, se elas ainda forem usadas.
Os desenvolvedores podem usar aidl_interface_defaults para declarar as dependências de uma interface compartilhada em outras interfaces para que elas não precisem ser atualizadas de forma independente.
Diretrizes de projeto da API
Geral
1. Documente tudo
- Documente cada método para sua semântica, argumentos, uso de exceções integradas, exceções específicas do serviço e valor de retorno.
- Documente cada interface para sua semântica.
- Documente o significado semântico de enumerações e constantes.
- Documente tudo o que possa não estar claro para um implementador.
- Forneça exemplos quando relevante.
2. Carcaça
Use a capitalização de camel case superior para tipos e a capitalização de camel case inferior para métodos, campos e argumentos. Por exemplo, MyParcelable para um tipo parcelable e anArgument
para um argumento. Para acrônimos, considere o acrônimo uma palavra (NFC -> Nfc).
[-Wconst-name] Os valores de enumeração e as constantes precisam ser ENUM_VALUE e
CONSTANT_NAME
3. Evite exigir conhecimento global
As APIs não podem presumir que os desenvolvedores tenham conhecimento global de toda a base de código ou experiência específica no domínio. Ao lidar com identificadores específicos do domínio (como nomes, IDs ou identificadores de dispositivos):
- Seja explícito e documente de onde esses identificadores vêm e o formato deles, se for importante que os dois lados da interface os conheçam.
- Como alternativa, use identificadores específicos da interface (como objetos de binder ou tokens personalizados) e faça com que um lado gerencie o mapeamento para os valores subjacentes. Isso reduz colisões e evita que os usuários entendam detalhes de implementação fora da área deles.
4. Todos os dados são estruturados e compatíveis com versões anteriores
Dados não estruturados, como string, byte[] e memória compartilhada, precisam ter um formato estável para o conteúdo ou ser opacos para um lado da interface.
Por exemplo, um argumento de string usado como uma mensagem de erro para um resultado pode ser recebido e registrado para depuração, mas não pode ser analisado e interpretado porque o formato e o conteúdo podem não ser compatíveis com versões anteriores. Se o outro lado da interface precisar saber qual é o erro no tempo de execução, use uma enumeração, uma constante ou ServiceSpecificException.
Da mesma forma, não serialize objetos em byte[] ou memória compartilhada, a menos que eles sejam estáveis e compatíveis com versões anteriores. Em alguns casos, é possível usar a anotação @FixedSize para compartilhar parcelables e uniões na memória compartilhada e nas filas de mensagens rápidas.
Interfaces
1. Nomeação
[-Winterface-name] Um nome de interface precisa começar com I, como IFoo.
2. Evite interfaces grandes com "objetos" baseados em ID
Prefira subinterfaces quando houver muitas chamadas relacionadas a uma API específica. Isso oferece os seguintes benefícios:
- Facilita a compreensão do código do cliente ou do servidor
- Simplifica o ciclo de vida dos objetos
- Aproveita o fato de que os binders não podem ser falsificados.
Não recomendado: uma interface única e grande com objetos baseados em ID
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
}
Recomendado: interfaces individuais
interface IManager {
IFoo getFoo();
}
interface IFoo {
void begin(); // clients in other processes can't guess a binder
void op();
}
3. Não misture métodos unidirecionais com bidirecionais
[-Wmixed-oneway] Não misture métodos unidirecionais com não unidirecionais, porque isso complica a compreensão do modelo de linhas de execução para clientes e servidores. Especificamente, ao ler o código do cliente de uma interface específica, é necessário pesquisar cada método para saber se ele vai bloquear ou não.
4. Evite retornar códigos de status
Os métodos precisam evitar códigos de status como valores de retorno, já que todos os métodos AIDL têm um código de retorno de status implícito. Consulte ServiceSpecificException ou EX_SERVICE_SPECIFIC. Por convenção, esses valores são definidos como constantes em uma interface AIDL. Se um atraso personalizado ou dados de erro exclusivos forem necessários junto com um erro, esse será o único momento em que um objeto de resposta personalizado representará um erro. Para mais informações, consulte
Tratamento de erros.
5. Matrizes como parâmetros de saída consideradas prejudiciais
[-Wout-array] Os métodos que têm parâmetros de saída de matriz, como void foo(out String[] ret), geralmente são ruins porque o tamanho da matriz de saída precisa ser declarado e alocado pelo cliente em Java. Assim, o tamanho da saída da matriz não pode ser escolhido pelo servidor. Esse comportamento indesejável acontece devido à forma como as matrizes funcionam em Java (elas não podem ser realocadas). Em vez disso, prefira APIs como String[] foo().
6. Evite parâmetros inout
[-Winout-parameter] Isso pode confundir os clientes porque até mesmo os parâmetros in parecem parâmetros out.
7. Evite parâmetros não de matriz @nullable out e inout
[-Wout-nullable] Como o back-end Java não processa a anotação @nullable, enquanto outros back-ends processam, out/inout @nullable T pode levar a um comportamento inconsistente entre os back-ends. Por exemplo, back-ends não Java podem definir um parâmetro @nullable como nulo (em C++, definindo-o como std::nullopt), mas o cliente Java não pode lê-lo como nulo.
8. Use solicitações e respostas exclusivas
Agrupe todos os parâmetros necessários em um único parcelable de entrada.
Crie parcelables de solicitação e resposta dedicados para cada método de interface em vez de transmitir primitivos (por exemplo, use ComputeResponse compute(in ComputeRequest request) em vez de transmitir variáveis separadas). Isso permite que novos argumentos sejam adicionados mais tarde sem mudar a assinatura da função. Esse padrão é fortemente sugerido quando se espera que mais parâmetros sejam adicionados no futuro ou se um método já tiver mais de quatro parâmetros.
Os métodos que não exigem entradas ou saídas adicionais não se beneficiam dessa sugestão. Pensar explicitamente em cada caso e permanecer flexível para mudanças futuras pode levar a menos métodos obsoletos e menos complexidade para o código compatível com versões anteriores.
Se um método não foi criado usando esse padrão, você pode mudar para ele criando um novo método com um parcelable de solicitação e resposta e tornando o método antigo obsoleto. Exemplo:
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.
Parcelables estruturados
1. Quando usar
Use parcelables estruturados quando você tiver vários tipos de dados para enviar.
Ou, quando você tem um único tipo de dados, mas espera que precise estendê-lo no futuro. Por exemplo, não use String username. Use um parcelable extensível, como este:
parcelable User {
String username;
}
Para que, no futuro, você possa estendê-lo da seguinte maneira:
parcelable User {
String username;
int id;
}
2. Forneça padrões explicitamente
[-Wexplicit-default, -Wenum-explicit-default] Forneça padrões explícitos para campos. Quando novos campos são adicionados a um parcelable, clientes e servidores antigos os descartam, mas os valores padrão são preenchidos automaticamente para novos clientes e servidores.
3. Use ParcelableHolder para extensões de fornecedor
Se você definir um parcelable AOSP que os implementadores de dispositivos precisam estender, incorpore uma instância de ParcelableHolder no seu objeto. Isso funciona como um ponto de extensão sem criar conflitos de mesclagem. Isso é semelhante às
extensões de interface anexadas
mas permite que os implementadores incluam o parcelable proprietário deles junto com o
existente parcelable sem criar a própria interface e tipos.
4. Estruturas de dados
- Use matrizes ou
Listde parcelables para representar mapas, já que a AIDL não oferece suporte nativo a tiposMapque são traduzidos com segurança em todos os back-ends nativos (por exemplo,FeatureToScoreEntry[]). - Use matrizes de objetos
parcelablepara campos repetidos em vez de matrizes de primitivos, para evitar a necessidade de matrizes paralelas no futuro. - Use objetos
parcelablefortemente tipados em vez de strings serializadas ou JSON em IPC. - Use enumerações em vez de booleanos para estados, permitindo expansão futura. Para bitmasks, use
const intem vez de tiposenumpara evitar a conversão complicada em alguns back-ends.
Parcelables não estruturados
1. Quando usar
Parcelables não estruturados estão disponíveis em Java com @JavaOnlyStableParcelable e no back-end NDK com @NdkOnlyStableParcelable. Geralmente, são parcelables antigos e atuais que não podem ser estruturados.
Constantes e enumerações
1. Os campos de bits precisam usar campos constantes
Os campos de bits precisam usar campos constantes (por exemplo, const int FOO = 3; em uma interface).
2. As enumerações precisam ser conjuntos fechados.
As enumerações precisam ser conjuntos fechados. Observação: somente o proprietário da interface pode adicionar elementos de enumeração. Se fornecedores ou OEMs precisarem estender esses campos, um mecanismo alternativo será necessário. Sempre que possível, é preferível o upstream da funcionalidade do fornecedor. No entanto, em alguns casos, valores de fornecedor personalizados podem ser permitidos (embora os fornecedores precisem ter um mecanismo para controlar a versão, talvez a própria AIDL, eles não podem entrar em conflito entre si, e esses valores não podem ser expostos a apps de terceiros).
3. Evite valores como "NUM_ELEMENTS"
Como as enumerações são controladas por versão, os valores que indicam quantos valores estão presentes precisam ser evitados. Em C++, isso pode ser contornado com enum_range<>. Para Rust, use enum_values(). Em Java, ainda não há uma solução.
Não recomendado: usar valores numerados
@Backing(type="int")
enum FruitType {
APPLE = 0,
BANANA = 1,
MANGO = 2,
NUM_TYPES, // BAD
}
4. Evite prefixos e sufixos redundantes
[-Wredundant-name] Evite prefixos e sufixos redundantes ou repetitivos em constantes e enumeradores.
Não recomendado: usar um prefixo redundante
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
Recomendado: nomear diretamente a enumeração
enum MyStatus {
GOOD,
BAD
}
FileDescriptor
[-Wfile-descriptor] O uso de FileDescriptor como um argumento ou o valor de retorno de um método de interface AIDL é altamente desencorajado. Principalmente quando a AIDL é implementada em Java, isso pode causar vazamento de descritor de arquivo, a menos que seja processado com cuidado. Basicamente, se você aceitar um FileDescriptor, será necessário fechá-lo manualmente quando ele não for mais usado.
Para back-ends nativos, você está seguro porque FileDescriptor é mapeado para unique_fd, que é fechado automaticamente. No entanto, independentemente da linguagem de back-end que você usar, é recomendável NÃO usar FileDescriptor, porque isso vai limitar sua liberdade de mudar a linguagem de back-end no futuro.
Em vez disso, use ParcelFileDescriptor, que é fechado automaticamente.
Unidades variáveis
Verifique se as unidades variáveis estão incluídas no nome para que as unidades sejam bem definidas e compreendidas sem a necessidade de consultar a documentação de referência.
Exemplos
long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good
double energy; // Bad
double energyMilliJoules; // Good
int frequency; // Bad
int frequencyHz; // Good
Os carimbos de data/hora precisam indicar a referência
Os carimbos de data/hora (na verdade, todas as unidades) precisam indicar claramente as unidades e os pontos de referência.
Exemplos
/**
* 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;
Concorrência e operações assíncronas
Processe operações de longa duração com uma interface assíncrona (oneway) para evitar bloqueios.
Se um serviço não confiar nos clientes, todos os callbacks recebidos deles precisarão ser interfaces oneway. Isso impede que os clientes bloqueiem o serviço indefinidamente.
Estruture APIs assíncronas que consistem em uma chamada direta, argumentos de entrada e uma interface de callback para receber resultados. Consulte Usar solicitações e respostas exclusivas para recomendações de argumentos.