AIDL-API-Richtlinien

Die hier beschriebenen Best Practices dienen als Leitfaden für die effektive Entwicklung von AIDL-Schnittstellen mit besonderem Augenmerk auf die Flexibilität der Schnittstelle, insbesondere wenn AIDL verwendet wird, um eine stabile, abwärtskompatible API zu definieren.

AIDL kann verwendet werden, um eine API zu definieren, wenn Apps in einem Hintergrundprozess miteinander interagieren oder mit dem System interagieren müssen.

Stabiles AIDL mit @VintfStability wird für HAL-Schnittstellen verwendet und ermöglicht die unabhängige Aktualisierung von Clients und Servern. Dazu sind Abwärtskompatibilität und strukturierte Daten erforderlich.

Weitere Informationen zum Entwickeln von Programmierschnittstellen in Apps mit AIDL finden Sie unter Android Interface Definition Language (AIDL). Beispiele für AIDL in der Praxis finden Sie unter AIDL for HALs und Stabiles AIDL.

Versionsverwaltung

Jeder abwärtskompatible Snapshot einer AIDL-API entspricht einer Version. Führen Sie m <module-name>-freeze-api aus, um einen Snapshot zu erstellen. Wenn ein Client oder Server der API veröffentlicht wird (z. B. in einem Mainline-Train), müssen Sie einen Snapshot erstellen und eine neue Version erstellen. Bei System-zu-Anbieter-APIs sollte dies mit der jährlichen Plattformrevision erfolgen.

Wenn eine Schnittstelle eingefroren (im versionierten aidl_api-Verzeichnis gespeichert) wird, darf sie nie geändert werden. Sie können nur das Verzeichnis current bearbeiten. Sie können Methoden gefahrlos am Ende einer Schnittstelle, Felder am Ende eines Parcelable, Enumeratoren am Ende eines Enum und Elemente am Ende einer Union hinzufügen.

Clients, die neue Methoden auf älteren Servern aufrufen, erhalten einen UNKNOWN_TRANSACTION-Fehler, der vom Client ordnungsgemäß behandelt werden sollte.

Weitere Informationen und Details zu den zulässigen Änderungen finden Sie unter Schnittstellenversionierung.

Build-Abhängigkeiten

Android-Module können nicht von mehreren verschiedenen Versionen der generierten Bibliotheken aus einer aidl_interface abhängig sein. Die verschiedenen Versionen der Bibliotheken definieren dieselben Typen in denselben Namespaces. Das aidl-Build-System von Android erkennt dieses Problem und gibt für jeden Abhängigkeitsgraphen, der in den nicht übereinstimmenden Versionen der Bibliotheken endet, einen Fehler aus.

Dies kann es erschweren, eine Version einer gemeinsamen Schnittstelle zu aktualisieren, wenn ein Modul viele Abhängigkeiten mit eigenen Abhängigkeiten enthält.

Entwickler können aidl_interface_defaults verwenden, um die Abhängigkeiten einer freigegebenen Schnittstelle von anderen Schnittstellen zu deklarieren, damit sie nicht alle unabhängig voneinander aktualisiert werden müssen.

Wir empfehlen, *_defaults-Module (z. B. rust_defaults, cc_defaults, java_defaults) zu verwenden, um die Abhängigkeiten von den generierten Bibliotheken zu organisieren. Es ist üblich, einen Standardwert für die latest-Version der Schnittstellen sowie Standardwerte für frühere Versionen zu haben, wenn diese noch verwendet werden.

Entwickler können aidl_interface_defaults verwenden, um die Abhängigkeiten einer freigegebenen Schnittstelle von anderen Schnittstellen zu deklarieren, damit sie nicht alle unabhängig voneinander aktualisiert werden müssen.

Richtlinien für das API-Design

Allgemein

1. Alles dokumentieren

  • Dokumentieren Sie jede Methode hinsichtlich ihrer Semantik, Argumente, Verwendung von integrierten Ausnahmen, dienstspezifischen Ausnahmen und Rückgabewerten.
  • Dokumentieren Sie jede Schnittstelle hinsichtlich ihrer Semantik.
  • Dokumentieren Sie die semantische Bedeutung von Enums und Konstanten.
  • Dokumentieren Sie alles, was für einen Implementierer unklar sein könnte.
  • Geben Sie gegebenenfalls Beispiele an.

2. Gehäuse

Verwenden Sie Upper Camel Case für Typen und Lower Camel Case für Methoden, Felder und Argumente. Beispiel: MyParcelable für einen Parcelable-Typ und anArgument für ein Argument. Bei Akronymen sollte das Akronym als Wort betrachtet werden (NFC -> Nfc).

[-Wconst-name] Enum-Werte und Konstanten sollten ENUM_VALUE und CONSTANT_NAME sein.

3. Kein globales Wissen erforderlich

APIs sollten nicht davon ausgehen, dass Entwickler über globales Wissen über die gesamte Codebasis oder spezifisches Fachwissen verfügen. Bei domänenspezifischen Kennungen (z. B. Gerätenamen, IDs oder Handles):

  • Seien Sie explizit und dokumentieren Sie, woher diese Kennungen stammen und wie ihr Format aussieht, wenn es für beide Seiten der Schnittstelle wichtig ist, sie zu kennen.
  • Alternativ können Sie schnittstellenspezifische Kennungen (z. B. Binder-Objekte oder benutzerdefinierte Tokens) verwenden und eine Seite die Zuordnung zu den zugrunde liegenden Werten verwalten lassen. Dadurch werden Konflikte reduziert und Nutzer müssen keine Implementierungsdetails außerhalb ihres Bereichs verstehen.

4. Alle Daten sind strukturiert und abwärtskompatibel

Unstrukturierte Daten wie string, byte[] und freigegebener Arbeitsspeicher müssen ein stabiles Format für ihre Inhalte haben oder für eine Seite der Schnittstelle undurchsichtig sein.

Ein String-Argument, das als Fehlermeldung für ein Ergebnis verwendet wird, kann beispielsweise empfangen und zur Fehlerbehebung protokolliert werden. Es darf aber nicht geparst und interpretiert werden, da Format und Inhalt möglicherweise nicht abwärtskompatibel sind. Wenn die andere Seite der Schnittstelle zur Laufzeit wissen muss, um welchen Fehler es sich handelt, verwenden Sie ein Enum, eine Konstante oder ServiceSpecificException.

Serialisieren Sie Objekte auch nicht in byte[] oder freigegebenen Arbeitsspeicher, es sei denn, sie sind stabil und abwärtskompatibel. In einigen Fällen können Sie die Annotation @FixedSize verwenden, um Parcelables und Unions im freigegebenen Arbeitsspeicher und in Fast Message Queues freizugeben.

Schnittstellen

1. Benennung

[-Winterface-name] Ein Schnittstellenname sollte mit I beginnen, z. B. IFoo.

2. Vermeiden Sie große Schnittstellen mit ID-basierten „Objekten“

Verwenden Sie Unterinterfaces, wenn viele Aufrufe im Zusammenhang mit einer bestimmten API stehen. Das bietet folgende Vorteile:

  • Client- oder Servercode ist leichter zu verstehen.
  • Der Lebenszyklus von Objekten wird vereinfacht.
  • Binder sind nicht fälschbar.

Nicht empfohlen: Eine einzelne, große Schnittstelle mit ID-basierten Objekten

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
}

Empfohlen: Einzelne Schnittstellen

interface IManager {
    IFoo getFoo();
}

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

3. Mischen Sie keine unidirektionalen und bidirektionalen Methoden

[-Wmixed-oneway] Mischen Sie keine unidirektionalen und nicht unidirektionalen Methoden, da dies das Threading-Modell für Clients und Server kompliziert macht. Wenn Sie beispielsweise Clientcode einer bestimmten Schnittstelle lesen, müssen Sie für jede Methode nachschlagen, ob sie blockiert oder nicht.

4. Vermeiden Sie die Rückgabe von Statuscodes

Methoden sollten keine Statuscodes als Rückgabewerte verwenden, da alle AIDL-Methoden einen impliziten Statusrückgabecode haben. Siehe ServiceSpecificException oder EX_SERVICE_SPECIFIC. Konventionsgemäß werden diese Werte als Konstanten in einer AIDL-Schnittstelle definiert. Nur wenn neben einem Fehler eine benutzerdefinierte Verzögerung oder eindeutige Fehlerdaten erforderlich sind, sollte ein benutzerdefiniertes Antwortobjekt einen Fehler darstellen. Weitere Informationen finden Sie unter Fehlerbehandlung.

5. Arrays als Ausgabeparameter sind schädlich

[-Wout-array] Methoden mit Array-Ausgabeparametern wie void foo(out String[] ret) sind in der Regel schlecht, da die Größe des Ausgabearrays vom Client in Java deklariert und zugewiesen werden muss. Daher kann die Größe der Array-Ausgabe nicht vom Server ausgewählt werden. Dieses unerwünschte Verhalten ist auf die Funktionsweise von Arrays in Java zurückzuführen (sie können nicht neu zugewiesen werden). Verwenden Sie stattdessen APIs wie String[] foo().

6. Vermeiden Sie Inout-Parameter

[-Winout-parameter] Dies kann Clients verwirren, da auch in-Parameter wie out-Parameter aussehen.

7. Vermeiden Sie nicht-Array-Parameter vom Typ „out“ und „inout“ mit der Annotation „@nullable“

[-Wout-nullable] Da das Java-Backend die @nullable Annotation nicht verarbeitet, während andere Backends dies tun, kann out/inout @nullable T zu inkonsistentem Verhalten zwischen den Backends führen. Nicht-Java-Backends können beispielsweise einen @nullable-Parameter vom Typ „out“ auf null setzen (in C++ als std::nullopt), aber der Java-Client kann ihn nicht als null lesen.

8. Verwenden Sie eindeutige Anfragen und Antworten

Gruppieren Sie alle erforderlichen Parameter in einem einzelnen parcelable vom Typ „input“. Erstellen Sie für jede Schnittstellenmethode separate Parcelables für Anfragen und Antworten, anstatt Primitive zu übergeben (verwenden Sie z. B. ComputeResponse compute(in ComputeRequest request) anstelle der Übergabe separater Variablen). So können später neue Argumente hinzugefügt werden, ohne die Funktionssignatur zu ändern. Dieses Muster wird dringend empfohlen, wenn erwartet wird, dass in Zukunft weitere Parameter hinzugefügt werden, oder wenn eine Methode bereits mehr als vier Parameter hat.

Methoden, die keine zusätzlichen Eingaben oder Ausgaben erfordern, profitieren nicht von diesem Vorschlag. Wenn Sie jeden Fall explizit berücksichtigen und flexibel für zukünftige Änderungen bleiben, kann dies zu weniger veralteten Methoden und weniger Komplexität für abwärtskompatiblen Code führen.

Wenn eine Methode nicht mit diesem Muster erstellt wurde, können Sie zu diesem Muster wechseln, indem Sie eine neue Methode mit einem Parcelable für Anfragen und Antworten erstellen und die alte Methode als veraltet kennzeichnen. Beispiel:

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.

Strukturierte Parcelables

1. Anwendung

Verwenden Sie strukturierte Parcelables, wenn Sie mehrere Datentypen senden müssen.

Oder wenn Sie einen einzelnen Datentyp haben, aber erwarten, dass Sie ihn in Zukunft erweitern müssen. Verwenden Sie beispielsweise nicht String username. Verwenden Sie ein erweiterbares Parcelable wie das folgende:

parcelable User {
    String username;
}

So können Sie es in Zukunft so erweitern:

parcelable User {
    String username;
    int id;
}

2. Standardwerte explizit angeben

[-Wexplicit-default, -Wenum-explicit-default] Geben Sie explizite Standardwerte für Felder an. Wenn einem Parcelable neue Felder hinzugefügt werden, werden sie von alten Clients und Servern ignoriert. Für neue Clients und Server werden jedoch automatisch Standardwerte eingefügt.

3. ParcelableHolder für Anbietererweiterungen verwenden

Wenn Sie ein AOSP-parcelable definieren, das von Geräteimplementierern erweitert werden muss, betten Sie eine Instanz von ParcelableHolder in Ihr Objekt ein. Dies dient als Erweiterungspunkt, ohne Merge-Konflikte zu verursachen. Dies ähnelt den angehängten Schnittstellenerweiterungen , ermöglicht es Implementierern jedoch, ihr proprietäres parcelable neben dem vorhandenen parcelable einzufügen, ohne eine eigene Schnittstelle und eigene Typen zu erstellen.

4. Datenstrukturen

  • Verwenden Sie Arrays oder List von Parcelables, um Maps darzustellen, da AIDL keine Map-Typen unterstützt, die sicher in alle nativen Backends übersetzt werden können (z. B. FeatureToScoreEntry[]).
  • Verwenden Sie Arrays von parcelable-Objekten für wiederholte Felder anstelle von Arrays von Primitiven, um parallele Arrays in Zukunft zu vermeiden.
  • Verwenden Sie stark typisierte parcelable-Objekte anstelle von serialisierten Strings oder JSON über IPC.
  • Verwenden Sie Enums anstelle von booleschen Werten für Zustände, um zukünftige Erweiterungen zu ermöglichen. Verwenden Sie für Bitmasks const int anstelle von enum-Typen, um umständliche Umwandlungen in einigen Backends zu vermeiden.

Nicht strukturierte Parcelables

1. Anwendung

Nicht strukturierte Parcelables sind in Java mit @JavaOnlyStableParcelable und im NDK-Backend mit @NdkOnlyStableParcelable verfügbar. In der Regel handelt es sich um alte und vorhandene Parcelables, die nicht strukturiert werden können.

Konstanten und Enums

1. Bitfelder sollten konstante Felder verwenden

Bitfelder sollten konstante Felder verwenden (z. B. const int FOO = 3; in einer Schnittstelle).

2. Enums sollten geschlossene Mengen sein.

Enums sollten geschlossene Mengen sein. Hinweis: Nur der Inhaber der Schnittstelle kann Enum-Elemente hinzufügen. Wenn Anbieter oder OEMs diese Felder erweitern müssen, ist ein alternativer Mechanismus erforderlich. Nach Möglichkeit sollte die Upstreaming-Anbieterfunktion bevorzugt werden. In einigen Fällen können jedoch benutzerdefinierte Anbieterwerte zugelassen werden (Anbieter sollten jedoch einen Mechanismus zur Versionierung haben, z. B. AIDL selbst, sie sollten nicht miteinander in Konflikt geraten können und diese Werte sollten nicht für Drittanbieter-Apps verfügbar sein).

3. Vermeiden Sie Werte wie „NUM_ELEMENTS“

Da Enums versioniert sind, sollten Werte vermieden werden, die angeben, wie viele Werte vorhanden sind. In C++ kann dies mit enum_range<> umgangen werden. Verwenden Sie für Rust enum_values(). Für Java gibt es noch keine Lösung.

Nicht empfohlen: Verwendung nummerierter Werte

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

4. Vermeiden Sie redundante Präfixe und Suffixe

[-Wredundant-name] Vermeiden Sie redundante oder sich wiederholende Präfixe und Suffixe in Konstanten und Enumeratoren.

Nicht empfohlen: Verwendung eines redundanten Präfixes

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Empfohlen: Direkte Benennung des Enums

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] Die Verwendung von FileDescriptor als Argument oder Rückgabewert einer AIDL-Schnittstellenmethode wird dringend abgeraten. Insbesondere wenn AIDL in Java implementiert ist, kann dies zu einem Leck des Dateideskriptors führen, wenn es nicht sorgfältig behandelt wird. Wenn Sie einen FileDescriptor akzeptieren, müssen Sie ihn manuell schließen, wenn er nicht mehr verwendet wird.

Bei nativen Backends sind Sie sicher, da FileDescriptor unique_fd zugeordnet wird, das automatisch geschlossen werden kann. Unabhängig von der verwendeten Backend-Sprache ist es jedoch ratsam, FileDescriptor überhaupt nicht zu verwenden, da dies Ihre Freiheit einschränkt, die Backend-Sprache in Zukunft zu ändern.

Verwenden Sie stattdessen ParcelFileDescriptor, das automatisch geschlossen werden kann.

Variable Einheiten

Achten Sie darauf, dass variable Einheiten im Namen enthalten sind, damit ihre Einheiten klar definiert und verständlich sind, ohne dass Sie die Referenzdokumentation lesen müssen.

Beispiele

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

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

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

Zeitstempel müssen ihre Referenz angeben

Zeitstempel (eigentlich alle Einheiten) müssen ihre Einheiten und Referenzpunkte klar angeben.

Beispiele

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

Nebenläufigkeit und asynchrone Vorgänge

Verarbeiten Sie Vorgänge mit langer Ausführungszeit mit einer asynchronen (oneway) Schnittstelle, um Blockierungen zu vermeiden.

Wenn ein Dienst seinen Clients nicht vertraut, sollten alle Callbacks, die er von den Clients erhält, oneway-Schnittstellen sein. So wird verhindert, dass die Clients den Dienst unbegrenzt blockieren können.

Strukturieren Sie asynchrone APIs, die aus einem Vorwärtsaufruf, Eingabeargumenten und einer Callback-Schnittstelle bestehen, um Ergebnisse zu erhalten. Unter Eindeutige Anfragen und Antworten verwenden finden Sie Empfehlungen für Argumente.