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 o interagire con le superfici API.
AIDL può essere utilizzato per definire un'API quando le app devono interfacciarsi tra loro in un processo in background o con il sistema. 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.
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 un treno Mainline), devi scattare 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.
Per ulteriori dettagli e informazioni sul tipo di modifiche consentite, consulta Interfacce di controllo delle versioni.
Linee guida per la progettazione delle API
Generali
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 per 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 serializzabile 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
Interfacce
1. Nome
[-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. Per informazioni più dettagliate, consulta la
sezione Gestione degli errori dei backend AIDL.
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.
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.
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 i 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.
Sconsigliato: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'enumerazione
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 di file, a meno che
non venga 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 dalla lingua del backend che utilizzerai,
è consigliabile NON utilizzare FileDescriptor perché ciò limiterà la tua
libertà di cambiare la lingua del backend in futuro.
Utilizza invece ParcelFileDescriptor, che può essere chiuso automaticamente.
Unità di variabile
Assicurati che le unità delle variabili siano incluse nel nome in modo che siano ben definite e comprensibili senza dover fare riferimento alla documentazione.
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;