AIDL stabile

Android 10 aggiunge il supporto per Android Interface Definition Language (AIDL) stabile, un nuovo modo per tenere traccia dell'interfaccia del programma applicativo (API)/dell'interfaccia binaria dell'applicazione (ABI) fornita dalle interfacce AIDL. L'AIDL stabile presenta le seguenti differenze chiave rispetto all'AIDL:

  • Le interfacce sono definite nel sistema di compilazione con aidl_interfaces .
  • Le interfacce possono contenere solo dati strutturati. I particellabili che rappresentano i tipi desiderati vengono creati automaticamente in base alla loro definizione AIDL e vengono automaticamente smistati e annullati.
  • Le interfacce possono essere dichiarate stabili (compatibili con le versioni precedenti). Quando ciò accade, la loro API viene tracciata e sottoposta a versionamento in un file accanto all'interfaccia AIDL.

Definizione di un'interfaccia AIDL

Una definizione di aidl_interface è simile a questa:

aidl_interface {
    name: "my-aidl",
    srcs: ["srcs/aidl/**/*.aidl"],
    local_include_dir: "srcs/aidl",
    imports: ["other-aidl"],
    versions: ["1", "2"],
    stability: "vintf",
    backend: {
        java: {
            enabled: true,
            platform_apis: true,
        },
        cpp: {
            enabled: true,
        },
        ndk: {
            enabled: true,
        },
    },

}
  • name : il nome del modulo di interfaccia AIDL che identifica in modo univoco un'interfaccia AIDL.
  • srcs : l'elenco dei file sorgente AIDL che compongono l'interfaccia. Il percorso per un tipo AIDL Foo definito in un pacchetto com.acme dovrebbe trovarsi in <base_path>/com/acme/Foo.aidl , dove <base_path> potrebbe essere qualsiasi directory correlata alla directory in cui si trova Android.bp . Nell'esempio sopra, <base_path> è srcs/aidl .
  • local_include_dir : il percorso da cui inizia il nome del pacchetto. Corrisponde a <base_path> spiegato sopra.
  • imports : un elenco di moduli aidl_interface che utilizza. Se una delle tue interfacce AIDL utilizza un'interfaccia o un parcelable da un'altra aidl_interface , inserisci il suo nome qui. Può essere il nome stesso, per fare riferimento alla versione più recente, o il nome con il suffisso della versione (come -V1 ) per fare riferimento a una versione specifica. La specifica di una versione è supportata da Android 12
  • versions : le versioni precedenti dell'interfaccia bloccate in api_dir , a partire da Android 11, le versions sono bloccate in aidl_api/ name . Se non ci sono versioni bloccate di un'interfaccia, questo non dovrebbe essere specificato e non ci saranno controlli di compatibilità.
  • stability : il flag opzionale per la promessa di stabilità di questa interfaccia. Attualmente supporta solo "vintf" . Se questo non è impostato, corrisponde a un'interfaccia con stabilità all'interno di questo contesto di compilazione (quindi un'interfaccia caricata qui può essere utilizzata solo con elementi compilati insieme, ad esempio su system.img). Se è impostato su "vintf" , ciò corrisponde a una promessa di stabilità: l'interfaccia deve essere mantenuta stabile finché viene utilizzata.
  • gen_trace : il flag opzionale per attivare o disattivare la traccia. L'impostazione predefinita è false .
  • host_supported : il flag facoltativo che, se impostato su true , rende disponibili le librerie generate all'ambiente host.
  • unstable : il flag opzionale utilizzato per contrassegnare che questa interfaccia non ha bisogno di essere stabile. Quando è impostato su true , il sistema di compilazione non crea il dump dell'API per l'interfaccia né richiede che venga aggiornato.
  • backend.<type>.enabled : questi flag alternano ciascuno dei backend per i quali il compilatore AIDL genererà il codice. Attualmente sono supportati tre backend: java , cpp e ndk . I backend sono tutti abilitati per impostazione predefinita. Quando un backend specifico non è necessario, deve essere disabilitato in modo esplicito.
  • backend.<type>.apex_available : l'elenco dei nomi APEX per cui è disponibile la libreria stub generata.
  • backend.[cpp|java].gen_log : il flag facoltativo che controlla se generare codice aggiuntivo per raccogliere informazioni sulla transazione.
  • backend.[cpp|java].vndk.enabled : il flag opzionale per rendere questa interfaccia una parte di VNDK. L'impostazione predefinita è false .
  • backend.java.platform_apis : il flag facoltativo che controlla se la libreria stub Java è compilata rispetto alle API private dalla piattaforma. Questo dovrebbe essere impostato su "true" quando stability è impostata su "vintf" .
  • backend.java.sdk_version : il flag facoltativo per specificare la versione dell'SDK su cui è costruita la libreria stub Java. L'impostazione predefinita è "system_current" . Questo non dovrebbe essere impostato quando backend.java.platform_apis è true.
  • backend.java.platform_apis : il flag facoltativo che dovrebbe essere impostato su true quando le librerie generate devono essere compilate sull'API della piattaforma anziché sull'SDK.

Per ogni combinazione delle versions e dei backend abilitati viene creata una libreria stub. Vedi Regole di denominazione dei moduli per come fare riferimento alla versione specifica della libreria stub per un backend specifico.

Scrittura di file AIDL

Le interfacce in AIDL stabile sono simili alle interfacce tradizionali, con l'eccezione che non possono utilizzare pacchettizzabili non strutturati (perché questi non sono stabili!). La differenza principale nell'AIDL stabile è come vengono definiti i parcelables. In precedenza, i parcelables erano dichiarati a termine; in AIDL stabile, i campi parcelable e le variabili sono definiti in modo esplicito.

// in a file like 'some/package/Thing.aidl'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}

Un valore predefinito è attualmente supportato (ma non richiesto) per boolean , char , float , double , byte , int , long e String . In Android 12 sono supportate anche le impostazioni predefinite per le enumerazioni definite dall'utente. Quando non viene specificato un valore predefinito, viene utilizzato un valore simile a 0 o vuoto. Le enumerazioni senza un valore predefinito vengono inizializzate su 0 anche se non è presente un enumeratore zero.

Utilizzo delle librerie di stub

Dopo aver aggiunto le librerie stub come dipendenza al tuo modulo, puoi includerle nei tuoi file. Di seguito sono riportati esempi di librerie stub nel sistema di compilazione ( Android.mk può essere utilizzato anche per le definizioni di moduli legacy):

cc_... {
    name: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // can also be shared_libs if desire is to load a library and share
    // it among multiple users or if you only need access to constants
    static_libs: ["my-module-name-java"],
    ...
}

Esempio in C++:

#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
    // use just like traditional AIDL

Esempio in Java:

import some.package.IFoo;
import some.package.Thing;
...
    // use just like traditional AIDL

Interfacce di controllo delle versioni

La dichiarazione di un modulo con nome foo crea anche una destinazione nel sistema di compilazione che puoi utilizzare per gestire l'API del modulo. Una volta compilato, foo-freeze-api aggiunge una nuova definizione API in api_dir o aidl_api/ name , a seconda della versione di Android, e aggiunge un file .hash , che rappresentano entrambi la versione appena bloccata dell'interfaccia. La creazione di questo aggiorna anche la proprietà delle versions per riflettere la versione aggiuntiva. Una volta specificata la proprietà delle versions , il sistema di compilazione esegue i controlli di compatibilità tra le versioni bloccate e anche tra Top of Tree (ToT) e l'ultima versione bloccata.

Inoltre, devi gestire la definizione API della versione ToT. Ogni volta che un'API viene aggiornata, esegui foo-update-api per aggiornare aidl_api/ name /current che contiene la definizione API della versione ToT.

Per mantenere la stabilità di un'interfaccia, i proprietari possono aggiungere nuovi:

  • Metodi alla fine di un'interfaccia (o metodi con nuovi seriali definiti in modo esplicito)
  • Elementi alla fine di un parcelable (richiede l'aggiunta di un valore predefinito per ogni elemento)
  • Valori costanti
  • In Android 11, enumeratori
  • In Android 12, campi alla fine di un'unione

Non sono consentite altre azioni e nessun altro può modificare un'interfaccia (altrimenti rischiano la collisione con le modifiche apportate da un proprietario).

Utilizzo di interfacce con versione

Metodi di interfaccia

In fase di esecuzione, quando si tenta di chiamare nuovi metodi su un vecchio server, i nuovi client ricevono un errore o un'eccezione, a seconda del back-end.

  • il backend cpp ottiene ::android::UNKNOWN_TRANSACTION .
  • Il backend ndk ottiene STATUS_UNKNOWN_TRANSACTION .
  • il backend java ottiene android.os.RemoteException con un messaggio che dice che l'API non è implementata.

Per le strategie per gestire questo problema, vedere l'esecuzione di query sulle versioni e l' utilizzo dei valori predefiniti .

Parcellabili

Quando vengono aggiunti nuovi campi ai parcelable, i vecchi client e server li eliminano. Quando nuovi client e server ricevono vecchi parcelable, i valori predefiniti per i nuovi campi vengono compilati automaticamente. Ciò significa che è necessario specificare le impostazioni predefinite per tutti i nuovi campi in un parcelable.

I client non devono aspettarsi che i server utilizzino i nuovi campi a meno che non sappiano che il server sta implementando la versione in cui è definito il campo (consultare la query sulle versioni ).

Enum e costanti

Allo stesso modo, client e server dovrebbero rifiutare o ignorare i valori costanti e gli enumeratori non riconosciuti a seconda dei casi, poiché potrebbero essere aggiunti altri in futuro. Ad esempio, un server non dovrebbe interrompersi quando riceve un enumeratore di cui non è a conoscenza. Dovrebbe ignorarlo o restituire qualcosa in modo che il client sappia che non è supportato in questa implementazione.

sindacati

Il tentativo di inviare un'unione con un nuovo campo non riesce se il destinatario è vecchio e non conosce il campo. L'attuazione non vedrà mai l'unione con il nuovo campo. L'errore viene ignorato se si tratta di una transazione unidirezionale; in caso contrario, l'errore è BAD_VALUE (per il back-end C++ o NDK) o IllegalArgumentException (per il back-end Java). L'errore viene ricevuto se il client invia un'unione impostata al nuovo campo a un vecchio server o quando è un vecchio client che riceve l'unione da un nuovo server.

Regole di denominazione dei moduli

In Android 11, per ogni combinazione delle versioni e dei backend abilitati, viene creato automaticamente un modulo libreria stub. Per fare riferimento a uno specifico modulo della libreria stub per il collegamento, non utilizzare il nome del modulo aidl_interface , ma il nome del modulo della libreria stub, che è ifacename - version - backend , dove

  • ifacename : nome del modulo aidl_interface
  • la version è una di
    • V version-number per le versioni bloccate
    • V latest-frozen-version-number + 1 per la versione della punta dell'albero (ancora da congelare)
  • il backend è uno dei due
    • java per il backend Java,
    • cpp per il backend C++,
    • ndk o ndk_platform per il backend NDK. Il primo è per le app e il secondo è per l'utilizzo della piattaforma.

Supponiamo che ci sia un modulo con nome foo e la sua ultima versione sia 2 e supporti sia NDK che C++. In questo caso AIDL genera questi moduli:

  • Basato sulla versione 1
    • foo-V1-(java|cpp|ndk|ndk_platform)
  • Basato sulla versione 2 (l'ultima versione stabile)
    • foo-V2-(java|cpp|ndk|ndk_platform)
  • Basato sulla versione ToT
    • foo-V3-(java|cpp|ndk|ndk_platform)

Rispetto ad Android 11,

  • foo- backend , che si riferiva all'ultima versione stabile diventa foo- V2 - backend
  • foo-unstable- backend , che si riferiva alla versione foo- V3 - backend

I nomi dei file di output sono sempre gli stessi dei nomi dei moduli.

  • Basato sulla versione 1: foo-V1-(cpp|ndk|ndk_platform).so
  • Basato sulla versione 2: foo-V2-(cpp|ndk|ndk_platform).so
  • Basato sulla versione ToT: foo-V3-(cpp|ndk|ndk_platform).so

Si noti che il compilatore AIDL non crea né un modulo di versione unstable , né un modulo senza versione per un'interfaccia AIDL stabile. A partire da Android 12, il nome del modulo generato da un'interfaccia AIDL stabile include sempre la sua versione.

Nuovi metodi di meta interfaccia

Android 10 aggiunge diversi metodi di meta interfaccia per AIDL stabile.

Interrogazione della versione dell'interfaccia dell'oggetto remoto

I client possono interrogare la versione e l'hash dell'interfaccia che l'oggetto remoto sta implementando e confrontare i valori restituiti con i valori dell'interfaccia utilizzata dal client.

Esempio con il backend cpp :

sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::HASH;
std::string remote_hash = foo->getInterfaceHash();

Esempio con il ndk (e ndk_platform ):

IFoo* foo = ... // the remote object
int32_t my_ver = IFoo::version;
int32_t remote_ver = 0;
if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::hash;
std::string remote_hash;
foo->getInterfaceHash(&remote_hash);

Esempio con il backend java :

IFoo foo = ... // the remote object
int myVer = IFoo.VERSION;
int remoteVer = foo.getInterfaceVersion();
if (remoteVer < myVer) {
  // the remote side is using an older interface
}

String myHash = IFoo.HASH;
String remoteHash = foo.getInterfaceHash();

Per il linguaggio Java, il lato remoto DEVE implementare getInterfaceVersion() e getInterfaceHash() come segue:

class MyFoo extends IFoo.Stub {
    @Override
    public final int getInterfaceVersion() { return IFoo.VERSION; }

    @Override
    public final String getInterfaceHash() { return IFoo.HASH; }
}

Questo perché le classi generate ( IFoo , IFoo.Stub , ecc.) sono condivise tra il client e il server (ad esempio, le classi possono trovarsi nel percorso di classe di avvio). Quando le classi sono condivise, il server è anche collegato alla versione più recente delle classi anche se potrebbe essere stato creato con una versione precedente dell'interfaccia. Se questa meta interfaccia è implementata nella classe condivisa, restituisce sempre la versione più recente. Tuttavia, implementando il metodo come sopra, il numero di versione dell'interfaccia è incorporato nel codice del server (perché IFoo.VERSION è un static final int che è inline quando si fa riferimento) e quindi il metodo può restituire la versione esatta in cui è stato creato il server con.

Gestire le vecchie interfacce

È possibile che un client venga aggiornato con la versione più recente di un'interfaccia AIDL ma il server utilizza la vecchia interfaccia AIDL. In questi casi, la chiamata di un metodo su una vecchia interfaccia restituisce UNKNOWN_TRANSACTION .

Con AIDL stabile, i clienti hanno un maggiore controllo. Sul lato client, puoi impostare un'implementazione predefinita su un'interfaccia AIDL. Un metodo nell'implementazione predefinita viene invocato solo quando il metodo non è implementato sul lato remoto (perché è stato compilato con una versione precedente dell'interfaccia). Poiché le impostazioni predefinite sono impostate a livello globale, non dovrebbero essere utilizzate da contesti potenzialmente condivisi.

Esempio in C++ in Android T (AOSP sperimentale) e versioni successive:

class MyDefault : public IFooDefault {
  Status anAddedMethod(...) {
   // do something default
  }
};

// once per an interface in a process
IFoo::setDefaultImpl(::android::sp<MyDefault>::make());

foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                         // remote side is not implementing it

Esempio in Java:

IFoo.Stub.setDefaultImpl(new IFoo.Default() {
    @Override
    public xxx anAddedMethod(...)  throws RemoteException {
        // do something default
    }
}); // once per an interface in a process


foo.anAddedMethod(...);

Non è necessario fornire l'implementazione predefinita di tutti i metodi in un'interfaccia AIDL. I metodi che sono garantiti per essere implementati sul lato remoto (perché si è certi che il telecomando sia compilato quando i metodi erano nella descrizione dell'interfaccia AIDL) non è necessario sovrascrivere nella classe impl predefinita.

Conversione dell'AIDL esistente in AIDL strutturato/stabile

Se si dispone di un'interfaccia AIDL esistente e del codice che la utilizza, utilizzare i passaggi seguenti per convertire l'interfaccia in un'interfaccia AIDL stabile.

  1. Identifica tutte le dipendenze della tua interfaccia. Per ogni pacchetto da cui dipende l'interfaccia, determinare se il pacchetto è definito in AIDL stabile. Se non definito, il pacchetto deve essere convertito.

  2. Converti tutti i parcelable nella tua interfaccia in parcelable stabili (i file dell'interfaccia stessi possono rimanere invariati). Fallo esprimendo la loro struttura direttamente nei file AIDL. Le classi di gestione devono essere riscritte per utilizzare questi nuovi tipi. Questo può essere fatto prima di creare un pacchetto aidl_interface (sotto).

  3. Crea un pacchetto aidl_interface (come descritto sopra) che contenga il nome del tuo modulo, le sue dipendenze e qualsiasi altra informazione di cui hai bisogno. Per renderlo stabilizzato (non solo strutturato), deve anche essere versionato. Per ulteriori informazioni, vedere Interfacce di controllo delle versioni .