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 a 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 criar 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), você precisará
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ões), ela
nunca pode ser modificada. Só é possível editar o diretório current. É possível
adicionar métodos ao final de uma interface, campos ao final de um
parcelable, enumeradores a um tipo enumerado 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 tratado de maneira adequada pelo cliente.
Para mais detalhes e informações sobre o tipo de mudanças permitidas, consulte Interfaces de controle de versão.
Dependências do build
Os módulos do Android não podem depender de várias versões diferentes das bibliotecas
geradas de um 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 não seja necessário atualizar todas de forma independente.
Recomendamos o uso de 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 não seja necessário atualizar todas de forma independente.
Diretrizes de design de APIs
Geral
1. Documente tudo
- Documente cada método para semântica, argumentos, uso de exceções integradas, exceções específicas do serviço e valor de retorno.
- Documente todas as interfaces para a semântica delas.
- Documente o significado semântico de tipos enumerados e constantes.
- Documente tudo o que possa não estar claro para um implementador.
- Dê exemplos quando relevante.
2. Casing
Use letras concatenadas maiúsculas para tipos e letras concatenadas minúsculas para métodos, campos e
argumentos. Por exemplo, MyParcelable para um tipo parcelable e anArgument
para um argumento. Para siglas, considere a sigla uma palavra (NFC -> Nfc).
[-Wconst-name] Os valores e as constantes de tipo enumerado precisam ser ENUM_VALUE e
CONSTANT_NAME
3. Evite exigir conhecimento global
As APIs não devem presumir que os desenvolvedores têm conhecimento global de toda a base de código ou conhecimento específico do 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 vêm esses identificadores 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 as colisões e evita que os usuários precisem entender 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 momento da execução, use um tipo enumerado, uma constante ou ServiceSpecificException.
Da mesma forma, não serialize objetos em byte[] ou memória compartilhada, a menos que
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 única interface 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 dificulta a compreensão do modelo de threading para clientes e servidores. Especificamente, ao ler o código do cliente de uma interface específica, você precisa procurar cada método para saber se ele vai bloquear ou não.
4. Evitar o retorno de 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, essa será a única vez em que um objeto de resposta personalizado deverá representar um
erro. Para mais informações, consulte
Tratamento de erros.
5. Matrizes como parâmetros de saída consideradas prejudiciais
[-Wout-array] Métodos com 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. Portanto, 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. Evitar parâmetros inout
[-Winout-parameter] Isso pode confundir os clientes, porque até mesmo os parâmetros in se parecem
com os parâmetros out.
7. Evite parâmetros out e inout @nullable que não sejam matrizes
[-Wout-nullable] Como o back-end do Java não processa a anotação @nullable,
enquanto outros back-ends fazem isso, out/inout @nullable T pode levar a um comportamento inconsistente
entre os back-ends. Por exemplo, back-ends que não são Java podem definir um parâmetro @nullable
de saída como nulo (em C++, definindo-o como std::nullopt), mas o cliente Java
não pode lê-lo como nulo.
8. Usar solicitações e respostas exclusivas
Agrupe todos os parâmetros necessários em uma única entrada parcelable.
Crie parcelables dedicados de solicitação e resposta 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 posteriormente sem mudar a
assinatura da função. Esse padrão é altamente recomendado 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 beneficiarão dessa sugestão. Pensar explicitamente em cada caso e manter a flexibilidade para mudanças futuras pode reduzir o número de métodos descontinuados e a complexidade do código compatível com versões anteriores.
Se um método não foi criado usando esse padrão, é possível mudar para ele criando um novo método com um parcelable de solicitação e resposta e descontinuando o método antigo. 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 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;
}
Assim, no futuro, você poderá estendê-lo da seguinte maneira:
parcelable User {
String username;
int id;
}
2. Forneça valores padrão explicitamente
[-Wexplicit-default, -Wenum-explicit-default] Forneça padrões explícitos para campos. Quando novos campos são adicionados a um parcelable, os clientes e servidores antigos os descartam, mas os valores padrão são preenchidos automaticamente para novos clientes e servidores.
3. Usar ParcelableHolder para extensões de fornecedor
Se você definir um parcelable do AOSP que os implementadores de dispositivos precisam estender,
incorpore uma instância de ParcelableHolder ao 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 reservado com o
parcelable atual sem criar uma interface e tipos próprios.
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 por IPC. - Use tipos enumerados em vez de booleanos para estados para permitir expansões futuras. Para
máscaras de bits, use tipos
const intem vez deenumpara evitar conversões complicadas 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 do NDK com
@NdkOnlyStableParcelable. Normalmente, são parcelables antigos e existentes
que não podem ser estruturados.
Constantes e tipos enumerados
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. Os tipos enumerados precisam ser conjuntos fechados.
Os tipos enumerados precisam ser conjuntos fechados. Observação: somente o proprietário da interface pode adicionar elementos de tipo enumerado. Se os fornecedores ou OEMs precisarem estender esses campos, será necessário um mecanismo alternativo. Sempre que possível, é preferível fazer o upstream da funcionalidade do fornecedor. No entanto, em alguns casos, valores personalizados de fornecedores podem ser permitidos (embora os fornecedores precisem ter um mecanismo para controlar as versões, 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 os tipos enumerados são versionados, evite valores que indiquem quantos valores estão presentes. Em C++, isso pode ser resolvido com enum_range<>. Para
Rust, use enum_values(). No 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 o tipo enumerado
enum MyStatus {
GOOD,
BAD
}
FileDescriptor
[-Wfile-descriptor] O uso de FileDescriptor como argumento ou valor de retorno
de um método de interface AIDL é altamente desaconselhável. Principalmente quando a
AIDL é implementada em Java, isso pode causar vazamento de descritor de arquivo, a menos que
seja tratado com cuidado. Basicamente, se você aceitar um FileDescriptor, precisará
fechá-lo manualmente quando ele não for mais usado.
Para back-ends nativos, você está em segurança porque FileDescriptor é mapeado para unique_fd,
que é fechado automaticamente. No entanto, independentemente da linguagem de back-end que você usaria,
é 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 é autocloseable.
Unidades variáveis
Verifique se as unidades de variáveis estão incluídas no nome para que 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;
Simultaneidade e operações assíncronas
Gerencie 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
precisam ser interfaces oneway. Isso impede que os clientes bloqueiem
o serviço indefinidamente.
Estruture APIs assíncronas que consistem em uma chamada de encaminhamento, argumentos de entrada e uma interface de callback para receber resultados. Consulte Usar solicitações e respostas exclusivas para recomendações de argumentos.