Wytyczne dotyczące interfejsu AIDL API

Przedstawione tu sprawdzone metody służą jako przewodnik po skutecznym tworzeniu interfejsów AIDL z uwzględnieniem ich elastyczności, zwłaszcza gdy AIDL jest używany do definiowania stabilnego, zgodnego wstecznie interfejsu API.

AIDL można używać do definiowania interfejsu API, gdy aplikacje muszą komunikować się ze sobą w procesie w tle lub z systemem.

Stabilny AIDL z @VintfStability jest używany w interfejsach HAL i umożliwia niezależne aktualizowanie klientów i serwerów. Wymaga to zgodności wstecznej i uporządkowanych danych.

Więcej informacji o tworzeniu interfejsów programowania w aplikacjach za pomocą AIDL znajdziesz w artykule o języku definiowania interfejsu Androida (AIDL). Przykłady AIDL w praktyce znajdziesz w artykułach AIDL dla HAL i Stabilny AIDL.

Obsługa wersji

Każda zgodna wstecznie migawka interfejsu AIDL API odpowiada wersji. Aby zrobić zrzut, uruchom m <module-name>-freeze-api. Za każdym razem, gdy klient lub serwer interfejsu API jest udostępniany (np. w pociągu Mainline), musisz zrobić zrzut i utworzyć nową wersję. W przypadku interfejsów API między systemem a dostawcą powinno to nastąpić podczas corocznej weryfikacji platformy.

Gdy interfejs jest zamrożony (zapisany w katalogu aidl_api z wersją), nie wolno go modyfikować. Możesz edytować tylko katalog current. Możesz bezpiecznie dodawać metody na końcu interfejsu, pola na końcu obiektu parcelable, enumeratory do typu wyliczeniowego i elementy do unii.

Klienty wywołujące nowe metody na starszych serwerach otrzymują błąd UNKNOWN_TRANSACTION, który powinien być odpowiednio obsługiwany przez klienta.

Więcej informacji o typach zmian, które są dozwolone, znajdziesz w artykule o interfejsach obsługujących wersje.

Zależności kompilacji

Moduły Androida nie mogą korzystać z różnych wersji wygenerowanych bibliotek z aidl_interface. Różne wersje bibliotek definiują te same typy w tych samych przestrzeniach nazw. System kompilacji aidl Androida wykrywa ten problem i zgłasza błąd w przypadku każdego wykresu zależności, który kończy się niezgodnymi wersjami bibliotek.

Może to utrudnić aktualizację jednej wersji wspólnego interfejsu, gdy moduł zawiera wiele zależności z własnymi zależnościami.

Deweloperzy mogą używać aidl_interface_defaults, aby deklarować zależności udostępnionego interfejsu od innych interfejsów, dzięki czemu nie trzeba ich wszystkich aktualizować niezależnie.

Zalecamy używanie modułów *_defaults (takich jak rust_defaults, cc_defaults, java_defaults) do porządkowania zależności w wygenerowanych bibliotekach. Często używa się domyślnej wersji interfejsów latest, a także domyślnych wersji poprzednich, jeśli są nadal używane.

Deweloperzy mogą używać aidl_interface_defaults, aby deklarować zależności udostępnionego interfejsu od innych interfejsów, dzięki czemu nie trzeba ich wszystkich aktualizować niezależnie.

Wytyczne dotyczące projektowania interfejsów API

Ogólne

1. Zbierz wszystkie informacje.

  • Dokumentuj każdą metodę pod kątem jej semantyki, argumentów, użycia wbudowanych wyjątków, wyjątków specyficznych dla usługi i wartości zwracanej.
  • Dokumentuj każdy interfejs pod kątem jego semantyki.
  • Dokumentuj znaczenie semantyczne typów wyliczeniowych i stałych.
  • Udokumentuj wszystko, co może być niejasne dla osoby wdrażającej.
  • W razie potrzeby podaj przykłady.

2. Obudowa

W przypadku typów używaj zapisu UpperCamelCase, a w przypadku metod, pól i argumentów – lowerCamelCase. Na przykład MyParcelable dla typu parcelable i anArgument dla argumentu. W przypadku akronimów traktuj akronim jako słowo (NFC -> Nfc).

[-Wconst-name] Wartości typu wyliczeniowego i stałe powinny być ENUM_VALUE i CONSTANT_NAME

3. Unikaj wymagania wiedzy globalnej

Interfejsy API nie powinny zakładać, że deweloperzy mają globalną wiedzę o całej bazie kodu lub specjalistyczną wiedzę o konkretnej domenie. W przypadku identyfikatorów związanych z domeną (takich jak nazwy urządzeń, identyfikatory lub nicki):

  • Jeśli jest to ważne dla obu stron interfejsu, należy wyraźnie określić i udokumentować, skąd pochodzą te identyfikatory i jaki jest ich format.
  • Możesz też użyć identyfikatorów specyficznych dla interfejsu (takich jak obiekty binder lub tokeny niestandardowe) i zarządzać mapowaniem na wartości bazowe po jednej stronie. Zmniejsza to liczbę kolizji i pozwala uniknąć sytuacji, w których użytkownicy muszą rozumieć szczegóły implementacji spoza ich obszaru.

4. Wszystkie dane są uporządkowane i zgodne wstecznie.

Dane nieuporządkowane, takie jak string, byte[] i pamięć współdzielona, muszą mieć stabilny format zawartości lub być nieprzezroczyste dla jednej strony interfejsu.

Na przykład argument ciągu znaków używany jako komunikat o błędzie wyniku może zostać odebrany i zarejestrowany na potrzeby debugowania, ale nie może być analizowany ani interpretowany, ponieważ format i zawartość mogą nie być zgodne wstecznie. Jeśli druga strona interfejsu musi wiedzieć, na czym polega błąd w czasie działania, użyj typu wyliczeniowego, stałej lub ServiceSpecificException.

Podobnie nie serializuj obiektów do byte[] ani pamięci współdzielonej, chyba że są stabilne i zgodne wstecznie. W niektórych przypadkach możesz używać adnotacji @FixedSize do udostępniania obiektów Parcelable i unii w pamięci współdzielonej i kolejkach Fast Message Queue.

Interfejsy

1. Nazwa

[-Winterface-name] Nazwa interfejsu powinna zaczynać się od I, np. IFoo.

2. Unikaj dużego interfejsu z „obiektami” opartymi na identyfikatorach

Jeśli jest wiele wywołań związanych z konkretnym interfejsem API, preferuj podinterfejsy. Zapewnia to te korzyści:

  • ułatwia zrozumienie kodu klienta lub serwera;
  • ułatwia zarządzanie cyklem życia obiektów,
  • Wykorzystuje fakt, że obiektów binder nie da się sfałszować.

Niezalecane: pojedynczy, duży interfejs z obiektami opartymi na identyfikatorach

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
}

Zalecane: poszczególne interfejsy

interface IManager {
    IFoo getFoo();
}

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

3. Nie mieszaj metod jednokierunkowych z dwukierunkowymi.

[-Wmixed-oneway] Nie mieszaj metod jednokierunkowych z niejednokierunkowymi, ponieważ utrudnia to klientom i serwerom zrozumienie modelu wątków. W szczególności podczas odczytywania kodu klienta konkretnego interfejsu musisz sprawdzić, czy każda metoda będzie blokować, czy nie.

4. Unikanie zwracania kodów stanu

Metody nie powinny używać kodów stanu jako wartości zwracanych, ponieważ wszystkie metody AIDL mają niejawny kod stanu. Zobacz ServiceSpecificException lub EX_SERVICE_SPECIFIC. Zgodnie z konwencją te wartości są definiowane jako stałe w interfejsie AIDL. Jeśli oprócz błędu potrzebne jest niestandardowe opóźnienie lub unikalne dane o błędzie, to jest to jedyny przypadek, gdy niestandardowy obiekt odpowiedzi powinien reprezentować błąd. Więcej informacji znajdziesz w artykule Obsługa błędów.

5. Tablice jako parametry wyjściowe są uznawane za szkodliwe

[-Wout-array] Metody z parametrami wyjściowymi tablicy, takimi jak void foo(out String[] ret), są zwykle nieprawidłowe, ponieważ rozmiar tablicy wyjściowej musi być zadeklarowany i przydzielony przez klienta w języku Java, więc rozmiar tablicy wyjściowej nie może być wybrany przez serwer. To niepożądane zachowanie wynika ze sposobu działania tablic w języku Java (nie można ich ponownie przydzielić). Zamiast tego używaj interfejsów API takich jak String[] foo().

6. Unikaj parametrów wejściowo-wyjściowych

[-Winout-parameter] Może to wprowadzać klientów w błąd, ponieważ nawet parametry in wyglądają jak parametry out.

7. Unikaj parametrów wyjściowych i wejściowo-wyjściowych @nullable, które nie są tablicami

[-Wout-nullable] Ponieważ backend Javy nie obsługuje adnotacji @nullable, podczas gdy inne backendy ją obsługują, out/inout @nullable T może powodować niespójne działanie w różnych backendach. Na przykład backendy inne niż Java mogą ustawić parametr wyjściowy @nullable na wartość null (w C++ ustawiając go jako std::nullopt), ale klient Java nie może odczytać go jako null.

8. Używanie unikalnych żądań i odpowiedzi

Zgrupuj wszystkie niezbędne parametry w jednym wejściu parcelable. Utwórz osobne obiekty Parcelable żądania i odpowiedzi dla każdej metody interfejsu zamiast przekazywać typy podstawowe (np. użyj ComputeResponse compute(in ComputeRequest request) zamiast przekazywać osobne zmienne). Dzięki temu można później dodawać nowe argumenty bez zmiany sygnatury funkcji. Ten wzorzec jest zdecydowanie zalecany, gdy w przyszłości może zostać dodanych więcej parametrów lub gdy metoda ma już więcej niż 4 parametry.

Metody, które nie wymagają dodatkowych danych wejściowych ani wyjściowych, nie skorzystają z tej sugestii. Dokładne przemyślenie każdego przypadku i zachowanie elastyczności na potrzeby przyszłych zmian może prowadzić do zmniejszenia liczby wycofanych metod i złożoności kodu zgodnego wstecznie.

Jeśli metoda nie została utworzona przy użyciu tego wzorca, możesz się na niego przełączyć, tworząc nową metodę z obiektem Parcelable żądania i odpowiedzi oraz wycofując starą metodę. Na przykład:

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.

Uporządkowane obiekty Parcelable

1. Kiedy używać

Używaj uporządkowanych obiektów Parcelable, gdy masz do wysłania wiele typów danych.

Możesz też użyć tego rozwiązania, gdy masz jeden typ danych, ale spodziewasz się, że w przyszłości trzeba będzie go rozszerzyć. Nie używaj na przykład String username. Użyj rozszerzalnego obiektu parcelable, takiego jak ten:

parcelable User {
    String username;
}

Dzięki temu w przyszłości możesz go rozszerzyć w ten sposób:

parcelable User {
    String username;
    int id;
}

2. Podaj domyślne wartości

[-Wexplicit-default, -Wenum-explicit-default] Podaj wyraźne wartości domyślne dla pól. Gdy do obiektu parcelable dodawane są nowe pola, stare klienty i serwery je odrzucają, ale wartości domyślne są automatycznie wypełniane w przypadku nowych klientów i serwerów.

3. Używanie ParcelableHolder do rozszerzeń dostawcy

Jeśli definiujesz parcelable AOSP, który producenci urządzeń muszą rozszerzyć, umieść instancję ParcelableHolder w swoim obiekcie. Działa to jako punkt rozszerzenia bez tworzenia konfliktów scalania. Jest to podobne do dołączonych rozszerzeń interfejsu, ale umożliwia implementującym dołączenie własnego parcelable obok istniejącego parcelable bez tworzenia własnego interfejsu i typów.

4. Struktury danych

  • Do reprezentowania map używaj tablic lub List obiektów parcelable, ponieważ AIDL nie obsługuje natywnie typów Map, które można bezpiecznie tłumaczyć we wszystkich natywnych backendach (np. FeatureToScoreEntry[]).
  • W przypadku pól powtarzanych używaj tablic obiektów parcelable, a nie tablic elementów podstawowych, aby uniknąć konieczności stosowania tablic równoległych w przyszłości.
  • Używaj obiektów parcelable o silnym typowaniu zamiast ciągów znaków lub obiektów JSON serializowanych przez IPC.
  • Zamiast wartości logicznych używaj typów wyliczeniowych, aby umożliwić rozszerzanie w przyszłości. W przypadku masek bitowych używaj typów const int zamiast enum, aby uniknąć uciążliwego rzutowania w niektórych backendach.

Nieuporządkowane obiekty Parcelable

1. Kiedy używać

Nieuporządkowane obiekty Parcelable są dostępne w języku Java z @JavaOnlyStableParcelable i w backendzie NDK z @NdkOnlyStableParcelable. Zwykle są to stare i istniejące obiekty parcelable, których nie można ustrukturyzować.

Stałe i typy wyliczeniowe

1. Pola bitowe powinny używać pól stałych

Pola bitowe powinny używać pól stałych (np. const int FOO = 3; w interfejsie).

2. Typy wyliczeniowe powinny być zbiorami zamkniętymi.

Typy wyliczeniowe powinny być zbiorami zamkniętymi. Uwaga: tylko właściciel interfejsu może dodawać elementy typu wyliczeniowego. Jeśli dostawcy lub producenci OEM muszą rozszerzyć te pola, potrzebny jest alternatywny mechanizm. Jeśli to możliwe, należy preferować funkcje dostawcy upstream. W niektórych przypadkach niestandardowe wartości dostawcy mogą być jednak dozwolone (choć dostawcy powinni mieć mechanizm do obsługi wersji, być może sam AIDL, nie powinny one ze sobą kolidować i nie powinny być widoczne dla aplikacji innych firm).

3. Unikaj wartości takich jak „NUM_ELEMENTS”.

Ponieważ typy wyliczeniowe są wersjonowane, należy unikać wartości, które wskazują, ile wartości jest obecnych. W C++ można to obejść za pomocą enum_range<>. W przypadku Rusta użyj enum_values(). W przypadku Javy nie ma jeszcze rozwiązania.

Niezalecane: używanie wartości liczbowych

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

4. Unikaj zbędnych prefiksów i sufiksów.

[-Wredundant-name] Unikaj zbędnych lub powtarzających się prefiksów i sufiksów w stałych i wyliczeniach.

Niezalecane: używanie zbędnego prefiksu

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Zalecane: bezpośrednie nazwanie typu wyliczeniowego

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] Zdecydowanie odradzamy używanie FileDescriptor jako argumentu lub wartości zwracanej metody interfejsu AIDL. Jeśli AIDL jest zaimplementowany w języku Java, może to spowodować wyciek deskryptora pliku, chyba że zostanie on odpowiednio obsłużony. Zasadniczo, jeśli zaakceptujesz FileDescriptor, musisz zamknąć go ręcznie, gdy nie będzie już używany.

W przypadku natywnych backendów nie musisz się martwić, ponieważ FileDescriptor jest mapowany na unique_fd, który jest automatycznie zamykany. Niezależnie od tego, jakiego języka backendu używasz, najlepiej w ogóle nie używać FileDescriptor, ponieważ ograniczy to możliwość zmiany języka backendu w przyszłości.

Zamiast tego użyj ParcelFileDescriptor, który jest automatycznie zamykany.

Zmienne jednostki

Upewnij się, że nazwa zawiera jednostki zmiennych, aby były one dobrze zdefiniowane i zrozumiałe bez konieczności odwoływania się do dokumentacji.

Przykłady

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

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

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

Sygnatury czasowe muszą wskazywać odniesienie.

Sygnatury czasowe (a także wszystkie jednostki) muszą wyraźnie wskazywać jednostki i punkty odniesienia.

Przykłady

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

Równoczesność i operacje asynchroniczne

Obsługuj długo trwające operacje za pomocą interfejsu asynchronicznego (oneway), aby uniknąć blokowania.

Jeśli usługa nie ufa swoim klientom, wszystkie wywołania zwrotne, które otrzymuje od klientów, powinny być interfejsami oneway. Zapobiega to blokowaniu usługi przez klientów na czas nieokreślony.

Struktura asynchronicznych interfejsów API składających się z wywołania przekierowania, argumentów wejściowych i interfejsu wywołania zwrotnego do uzyskiwania wyników. Zalecenia dotyczące argumentów znajdziesz w sekcji Używanie unikalnych żądań i odpowiedzi.