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
Listdi parcelable per rappresentare le mappe, poiché AIDL non supporta in modo nativo i tipiMapche vengono convertiti in modo sicuro in tutti i backend nativi (ad esempio,FeatureToScoreEntry[]). - Utilizza array di oggetti
parcelableper i campi ripetuti anziché array di primitive, per evitare la necessità di array paralleli in futuro. - Utilizza oggetti
parcelablefortemente 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 intanzichéenumper 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.