Linee guida per l'API AIDL

Le best practice descritte qui fungono da guida per sviluppare interfacce AIDL in modo efficace e con attenzione alla flessibilità dell'interfaccia, in particolare quando AIDL viene utilizzato per definire un'API stabile e compatibile con le versioni precedenti.

AIDL può essere utilizzato per definire un'API quando le app devono interfacciarsi tra loro in un processo in background o con il sistema.

AIDL stabile con @VintfStability viene utilizzato per le interfacce HAL e consente di aggiornare client e server in modo indipendente. Ciò richiede la compatibilità con le versioni precedenti e dati strutturati.

Per ulteriori informazioni sullo sviluppo di interfacce di programmazione nelle app con AIDL, consulta Android Interface Definition Language (AIDL). Per esempi di AIDL in pratica, vedi AIDL per HAL e AIDL stabile.

Controllo delle versioni

Ogni snapshot compatibile con le versioni precedenti di un'API AIDL corrisponde a una versione. Per scattare una foto, esegui m <module-name>-freeze-api. Ogni volta che viene rilasciato un client o un server dell'API (ad esempio, in una traccia Mainline), devi acquisire uno snapshot e creare una nuova versione. Per le API da sistema a fornitore, questa operazione deve essere eseguita con la revisione annuale della piattaforma.

Quando un'interfaccia viene bloccata (salvata nella directory aidl_api con controllo delle versioni), non deve mai essere modificata. Puoi modificare solo la directory current. Puoi aggiungere in modo sicuro metodi alla fine di un'interfaccia, campi alla fine di un oggetto Parcelable, enumeratori a un'enumerazione e membri a un'unione.

I client che chiamano nuovi metodi su server meno recenti ricevono un errore UNKNOWN_TRANSACTION, che deve essere gestito correttamente dal client.

Per ulteriori dettagli e informazioni sul tipo di modifiche consentite, consulta Versioni delle interfacce.

Dipendenze build

I moduli Android non possono dipendere da più versioni diverse delle librerie generate da un aidl_interface. Le diverse versioni delle librerie definiscono gli stessi tipi negli stessi spazi dei nomi. Il sistema di compilazione aidl di Android identifica questo problema e genera un errore con ciascuno dei grafici delle dipendenze che terminano con le versioni non corrispondenti delle librerie.

Ciò può rendere difficile l'aggiornamento di una versione di un'interfaccia comune quando un modulo contiene molte dipendenze con le proprie dipendenze.

Gli sviluppatori possono utilizzare aidl_interface_defaults per dichiarare le dipendenze di un'interfaccia condivisa da altre interfacce, in modo che non debbano essere aggiornate indipendentemente.

Ti consigliamo di utilizzare i moduli *_defaults (come rust_defaults, cc_defaults, java_defaults) per organizzare le dipendenze dalle librerie generate. È comune avere un valore predefinito per la versione latest delle interfacce, nonché valori predefiniti per le versioni precedenti, se ancora utilizzate.

Gli sviluppatori possono utilizzare aidl_interface_defaults per dichiarare le dipendenze di un'interfaccia condivisa da altre interfacce, in modo che non debbano essere aggiornate indipendentemente.

Linee guida per la progettazione delle API

Generale

1. Documenta tutto

  • Documenta ogni metodo per la sua semantica, i suoi argomenti, l'utilizzo di eccezioni integrate, eccezioni specifiche del servizio e valore restituito.
  • Documenta ogni interfaccia per la sua semantica.
  • Documenta il significato semantico di enumerazioni e costanti.
  • Documenta tutto ciò che potrebbe non essere chiaro a un implementatore.
  • Fornisci esempi pertinenti.

2. Carcassa

Utilizza la notazione CamelCase maiuscola per i tipi e la notazione CamelCase minuscola per metodi, campi e argomenti. Ad esempio, MyParcelable per un tipo parcelable e anArgument per un argomento. Per gli acronimi, considera l'acronimo una parola (NFC -> Nfc).

[-Wconst-name] I valori enum e le costanti devono essere ENUM_VALUE e CONSTANT_NAME

3. Evitare di richiedere conoscenze globali

Le API non devono presupporre che gli sviluppatori abbiano una conoscenza globale dell'intero codebase o competenze specifiche del dominio. Quando si tratta di identificatori specifici del dominio (come nomi, ID o handle dei dispositivi):

  • Specifica e documenta la provenienza e il formato di questi identificatori se è importante che siano noti a entrambe le parti dell'interfaccia.
  • In alternativa, utilizza identificatori specifici dell'interfaccia (come oggetti binder o token personalizzati) e fai in modo che una parte gestisca la mappatura ai valori sottostanti. In questo modo si riducono le collisioni e si evita di richiedere agli utenti di comprendere i dettagli di implementazione al di fuori della loro area.

4. Tutti i dati sono strutturati e compatibili con le versioni precedenti

I dati non strutturati come string, byte[] e la memoria condivisa devono avere un formato stabile per i loro contenuti o essere opachi per una parte dell'interfaccia.

Ad esempio, un argomento stringa utilizzato come messaggio di errore per un risultato può essere ricevuto e registrato per il debug, ma non deve essere analizzato e interpretato perché il formato e i contenuti potrebbero non essere compatibili con le versioni precedenti. Se l'altra parte dell'interfaccia deve sapere qual è l'errore in fase di runtime, utilizza un'enumerazione, una costante o ServiceSpecificException.

Allo stesso modo, non serializzare gli oggetti in byte[] o nella memoria condivisa a meno che non siano stabili e compatibili con le versioni precedenti. In alcuni casi, puoi utilizzare l'annotazione @FixedSize per condividere parcelable e unioni in memoria condivisa e code di messaggi veloci.

Interfacce

1. Denominazione

[-Winterface-name] Il nome di un'interfaccia deve iniziare con I, ad esempio IFoo.

2. Evita interfacce di grandi dimensioni con "oggetti" basati su ID

Preferisci le sottointerfacce quando ci sono molte chiamate correlate a un'API specifica. Questo offre i seguenti vantaggi:

  • Rende più facile la comprensione del codice client o server
  • Semplifica il ciclo di vita degli oggetti
  • Sfrutta l'impossibilità di falsificare i raccoglitori.

Sconsigliato:un'unica interfaccia di grandi dimensioni con oggetti basati su 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
}

Consigliato:interfacce individuali

interface IManager {
    IFoo getFoo();
}

interface IFoo {
    void begin(); // clients in other processes can't guess a binder
    void op();
}

3. Non combinare metodi unidirezionali e bidirezionali

[-Wmixed-oneway] Non combinare metodi unidirezionali con metodi non unidirezionali, perché rende la comprensione del modello di threading complicata per client e server. Nello specifico, quando leggi il codice client di una determinata interfaccia, devi cercare per ogni metodo se questo bloccherà o meno.

4. Evita di restituire codici di stato

I metodi devono evitare i codici di stato come valori restituiti, poiché tutti i metodi AIDL hanno un codice di stato restituito implicito. Consulta ServiceSpecificException o EX_SERVICE_SPECIFIC. Per convenzione, questi valori sono definiti come costanti in un'interfaccia AIDL. Se è necessario un ritardo personalizzato o dati di errore univoci insieme a un errore, questo è l'unico caso in cui un oggetto di risposta personalizzato deve rappresentare un errore. Per informazioni più dettagliate, consulta la sezione Gestione degli errori.

5. Gli array come parametri di output sono considerati dannosi

[-Wout-array] I metodi con parametri di output di array, come void foo(out String[] ret), di solito non sono consigliati perché le dimensioni dell'array di output devono essere dichiarate e allocate dal client in Java, quindi le dimensioni dell'array di output non possono essere scelte dal server. Questo comportamento indesiderato si verifica perché gli array in Java non possono essere riallocati. Preferisci invece API come String[] foo().

6. Evita i parametri inout

[-Winout-parameter] Ciò può confondere i clienti perché anche i parametri in sembrano parametri out.

7. Evita i parametri out e inout @nullable non array

[-Wout-nullable] Poiché il backend Java non gestisce l'annotazione @nullable mentre altri backend lo fanno, out/inout @nullable T potrebbe comportare un comportamento incoerente tra i backend. Ad esempio, i backend non Java possono impostare un parametro out @nullable su null (in C++, impostandolo come std::nullopt), ma il client Java non può leggerlo come null.

8. Utilizzare richieste e risposte uniche

Raggruppa tutti i parametri necessari in un unico input parcelable. Crea parcelable di richiesta e risposta dedicati per ogni metodo di interfaccia anziché passare primitive (ad esempio, utilizza ComputeResponse compute(in ComputeRequest request) anziché passare variabili separate). Ciò consente di aggiungere nuovi argomenti in un secondo momento senza modificare la firma della funzione. Questo pattern è fortemente consigliato quando si prevede che in futuro potrebbero essere aggiunti altri parametri o se un metodo ha già più di quattro parametri.

I metodi che non richiedono input o output aggiuntivi non trarranno vantaggio da questo suggerimento. Pensare esplicitamente a ogni caso e rimanere flessibili per le modifiche future può portare a un minor numero di metodi deprecati e a una minore complessità per il codice compatibile con le versioni precedenti.

Se un metodo non è stato creato utilizzando questo pattern, puoi passare a questo pattern creando un nuovo metodo con un pacco di richiesta e risposta e ritirando il vecchio metodo. Ad esempio:

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.

Parcelable strutturati

1. Quando usare la funzionalità

Utilizza parcelable strutturati quando devi inviare più tipi di dati.

Oppure, quando hai un solo tipo di dati, ma prevedi di doverlo estendere in futuro. Ad esempio, non usare String username. Utilizza un oggetto Parcelable estendibile, come il seguente:

parcelable User {
    String username;
}

In modo che, in futuro, tu possa estenderlo come segue:

parcelable User {
    String username;
    int id;
}

2. Fornisci valori predefiniti in modo esplicito

[-Wexplicit-default, -Wenum-explicit-default] Fornisci valori predefiniti espliciti per i campi. Quando vengono aggiunti nuovi campi a un oggetto serializzabile, i client e i server precedenti li eliminano, ma i valori predefiniti vengono compilati automaticamente per i nuovi client e server.

3. Utilizzare ParcelableHolder per le estensioni del fornitore

Se definisci un parcelable AOSP che gli implementatori di dispositivi devono estendere, incorpora un'istanza di ParcelableHolder nel tuo oggetto. Funge da punto di estensione senza creare conflitti di unione. È simile alle estensioni dell'interfaccia allegate, ma consente agli implementatori di includere il proprio parcelable proprietario insieme al parcelable esistente senza creare interfacce e tipi personalizzati.

4. Strutture di dati

  • Utilizza array o List di parcelable per rappresentare le mappe, poiché AIDL non supporta in modo nativo i tipi Map che vengono convertiti in modo sicuro in tutti i backend nativi (ad esempio, FeatureToScoreEntry[]).
  • Utilizza array di oggetti parcelable per i campi ripetuti anziché array di primitive, per evitare la necessità di array paralleli in futuro.
  • Utilizza oggetti parcelable fortemente tipizzati anziché stringhe serializzate o JSON su IPC.
  • Utilizza gli enum anziché i booleani per gli stati per consentire l'espansione futura. Per le bitmask, utilizza i tipi const int anziché enum per evitare conversioni complesse in alcuni backend.

Parcelable non strutturati

1. Quando usare la funzionalità

I parcelable non strutturati sono disponibili in Java con @JavaOnlyStableParcelable e nel backend NDK con @NdkOnlyStableParcelable. In genere, si tratta di parcelable vecchi ed esistenti che non possono essere strutturati.

Costanti ed enumerazioni

1. I campi di bit devono utilizzare campi costanti

I campi di bit devono utilizzare campi costanti (ad esempio, const int FOO = 3; in un'interfaccia).

2. Gli enum devono essere insiemi chiusi.

Gli enum devono essere insiemi chiusi. Nota: solo il proprietario dell'interfaccia può aggiungere elementi enum. Se i fornitori o gli OEM devono estendere questi campi, è necessario un meccanismo alternativo. Se possibile, è preferibile utilizzare la funzionalità del fornitore upstream. Tuttavia, in alcuni casi, i valori personalizzati del fornitore potrebbero essere consentiti (anche se i fornitori devono disporre di un meccanismo per il controllo delle versioni, magari AIDL stesso, non devono essere in conflitto tra loro e questi valori non devono essere esposti ad app di terze parti).

3. Evita valori come "NUM_ELEMENTS"

Poiché gli enum sono versionati, è consigliabile evitare valori che indicano quanti valori sono presenti. In C++, questo problema può essere risolto con enum_range<>. Per Rust, utilizza enum_values(). In Java, non esiste ancora una soluzione.

Non consigliato:utilizzo di valori numerati

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. Evita prefissi e suffissi ridondanti

[-Wredundant-name] Evita prefissi e suffissi ridondanti o ripetitivi in costanti ed enumeratori.

Sconsigliato:utilizzo di un prefisso ridondante

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Consigliato: denominazione diretta dell'enum

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] L'utilizzo di FileDescriptor come argomento o valore restituito di un metodo di interfaccia AIDL è fortemente sconsigliato. In particolare, quando l'AIDL viene implementato in Java, ciò potrebbe causare una perdita di descrittori del file se non gestita con attenzione. In sostanza, se accetti un FileDescriptor, devi chiuderlo manualmente quando non viene più utilizzato.

Per i backend nativi, non ci sono problemi perché FileDescriptor corrisponde a unique_fd che è chiudibile automaticamente. Indipendentemente dal linguaggio del backend che utilizzerai, è consigliabile NON utilizzare FileDescriptor perché ciò limiterà la tua libertà di cambiare il linguaggio del backend in futuro.

Utilizza invece ParcelFileDescriptor, che può essere chiuso automaticamente.

Unità di misura delle variabili

Assicurati che le unità variabili siano incluse nel nome in modo che siano ben definite e comprensibili senza dover fare riferimento alla documentazione di riferimento.

Esempi

long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good

double energy; // Bad
double energyMilliJoules; // Good

int frequency; // Bad
int frequencyHz; // Good

I timestamp devono indicare il riferimento

I timestamp (in realtà, tutte le unità) devono indicare chiaramente le unità e i punti di riferimento.

Esempi

/**
 * 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;

Concorrenza e operazioni asincrone

Gestisci le operazioni a lunga esecuzione con un'interfaccia asincrona (oneway) per evitare blocchi.

Se un servizio non si fida dei suoi client, tutti i callback che riceve dai client devono essere interfacce oneway. In questo modo, i client non possono bloccare il servizio a tempo indeterminato.

Struttura le API asincrone costituite da una chiamata di inoltro, argomenti di input e un'interfaccia di callback per ottenere i risultati. Consulta Utilizzare richieste e risposte uniche per suggerimenti sugli argomenti.