Diretrizes da API AIDL

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 List de parcelables para representar mapas, já que a AIDL não oferece suporte nativo a tipos Map que são traduzidos com segurança em todos os back-ends nativos (por exemplo, FeatureToScoreEntry[]).
  • Use matrizes de objetos parcelable para campos repetidos em vez de matrizes de primitivos, para evitar a necessidade de matrizes paralelas no futuro.
  • Use objetos parcelable fortemente 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 int em vez de enum para 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.