Wytyczne dotyczące interfejsu AIDL API

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

AIDL może służyć do definiowania interfejsu API, gdy aplikacje muszą komunikować się ze sobą w procesie działającym w tle lub z systemem.

Stabilny AIDL z adnotacją @VintfStability jest używany w przypadku interfejsów 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 Język definiowania interfejsu Androida (AIDL). Przykłady praktycznego zastosowania AIDL znajdziesz w artykułach AIDL for HALs and Stable AIDL.

Obsługa wersji

Każda wstecznie zgodna migawka interfejsu API AIDL odpowiada wersji. Aby zrobić migawkę, uruchom polecenie m <module-name>-freeze-api. Za każdym razem, gdy klient lub serwer interfejsu API zostanie opublikowany (np. w ramach Mainline), musisz zrobić migawkę i utworzyć nową wersję. W przypadku interfejsów API system-dostawca powinno to nastąpić wraz z roczną aktualizacją platformy.

Gdy interfejs zostanie zamrożony (zapisany w katalogu aidl_api z informacjami o wersji), nie można go modyfikować. Możesz edytować tylko katalog current. Możesz bezpiecznie dodawać metody na końcu interfejsu, pola na końcu parcelable, wyliczenia do wyliczenia i elementy do unii.

Klienci wywołujący nowe metody na starszych serwerach otrzymują błąd UNKNOWN_TRANSACTION, który powinien być przez nich obsługiwany.

Więcej informacji i szczegółów o typach dozwolonych zmian znajdziesz w artykule Obsługa wersji interfejsów.

Zależności kompilacji

Moduły Androida nie mogą zależeć od wielu 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 grafu zależności, który kończy się niezgodnymi wersjami bibliotek.

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

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

Zalecamy używanie modułów *_defaults (takich jak rust_defaults, cc_defaults, java_defaults) do organizowania zależności od wygenerowanych bibliotek. Często stosuje się domyślne ustawienia dla latest wersji interfejsów oraz dla poprzednich wersji jeśli są one nadal używane.

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

Wytyczne dotyczące projektowania interfejsu API

Ogólne

1. Zbierz wszystkie informacje

  • Zbierz informacje o każdej metodzie – jej semantyce, argumentach, użyciu wbudowanych wyjątków, wyjątków specyficznych dla usługi i wartości zwracanej.
  • Zbierz informacje o każdym interfejsie – jego semantyce.
  • Zbierz informacje o semantycznym znaczeniu wyliczeń i stałych.
  • Zbierz informacje o wszystkim, co może być niejasne dla osoby implementującej.
  • W razie potrzeby podaj przykłady.

2. Powłoka zewnętrzna

W przypadku typów używaj wielbłąda z wielką literą, a w przypadku metod, pól i argumentów – wielbłąda z małą literą. Na przykład MyParcelable w przypadku typu parcelable i anArgument w przypadku argumentu. W przypadku akronimów traktuj je jako słowa (NFC -> Nfc).

[-Wconst-name] Wartości wyliczeń i stałe powinny mieć postać ENUM_VALUE i CONSTANT_NAME

3. Unikaj wymagania wiedzy globalnej

Interfejsy API nie powinny zakładać, że programiści mają globalną wiedzę o całej bazie kodu lub specjalistyczną wiedzę w określonej dziedzinie. W przypadku identyfikatorów specyficznych dla domeny (takich jak nazwy urządzeń, identyfikatory lub uchwyty):

  • Bądź precyzyjny i wyjaśnij, skąd pochodzą te identyfikatory i jaki jest ich format, jeśli jest to ważne dla obu stron interfejsu.
  • Możesz też używać identyfikatorów specyficznych dla interfejsu (takich jak obiekty binder lub tokeny niestandardowe) i pozwolić jednej stronie zarządzać mapowaniem na wartości bazowe. Zmniejsza to liczbę kolizji i eliminuje konieczność zrozumienia przez użytkowników szczegółów implementacji poza ich obszarem.

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

Nieustrukturyzowane dane, 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 w postaci ciągu znaków używany jako komunikat o błędzie w 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ć wstecznie zgodne. Jeśli druga strona interfejsu musi wiedzieć, jaki błąd wystąpił w czasie działania, użyj wyliczenia, stałej lub ServiceSpecificException.

Podobnie nie serializuj obiektów do byte[] ani pamięci współdzielonej, chyba że są one stabilne i wstecznie zgodne. W niektórych przypadkach możesz użyć adnotacji @FixedSize do udostępniania parcelable i unii w pamięci współdzielonej oraz szybkich kolejkach komunikatów.

Interfejsy

1. Nazwa

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

2. Unikaj dużych interfejsów z „obiektami” opartymi na identyfikatorach

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

  • kod klienta lub serwera jest łatwiejszy do zrozumienia,
  • cykl życia obiektów jest prostszy,
  • można korzystać z tego, że bindery są niepodrabialne.

Niezalecane: jeden 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 metodami niejednokierunkowymi, ponieważ utrudnia to klientom i serwerom zrozumienie modelu wątków. W szczególności podczas czytania kodu klienta konkretnego interfejsu musisz sprawdzić, czy dana metoda będzie blokować, czy nie.

4. Unikaj zwracania kodów stanu

Metody powinny unikać kodów stanu jako wartości zwracanych, ponieważ wszystkie metody AIDL mają domyślny 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, tylko wtedy niestandardowy obiekt odpowiedzi powinien reprezentować błąd. Więcej informacji znajdziesz w sekcji Obsługa błędów.

5. Tablice jako parametry wyjściowe są szkodliwe

[-Wout-array] Metody z parametrami wyjściowymi w postaci tablic, takie jak void foo(out String[] ret), są zwykle nieprawidłowe, ponieważ rozmiar tablicy wyjściowej musi zostać zadeklarowany i przydzielony przez klienta w Javie, a więc serwer nie może wybrać rozmiaru tablicy wyjściowej. To niepożądane zachowanie wynika ze sposobu działania tablic w Javie (nie można ich ponownie przydzielić). Zamiast tego preferuj interfejsy API takie jak String[] foo().

6. Unikaj parametrów inout

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

7. Unikaj parametrów out i inout @nullable niebędących tablicami

[-Wout-nullable] Ponieważ backend Java nie obsługuje @nullable adnotacji podczas gdy inne backendy tak, out/inout @nullable T może prowadzić do niespójnego działania w różnych backendach. Na przykład backendy inne niż Java mogą ustawić parametr out @nullable na null (w C++ ustawiając go jako std::nullopt), ale klient Java nie może go odczytać jako null.

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

Zgrupuj wszystkie niezbędne parametry w jednym wejściowym parcelable. Zamiast przekazywać typy proste, utwórz dedykowane parcelable żądania i odpowiedzi dla każdej metody interfejsu (np. użyj ComputeResponse compute(in ComputeRequest request) zamiast przekazywać osobne zmienne). Umożliwia to dodawanie nowych argumentów w przyszłości bez zmiany sygnatury funkcji. Ten wzorzec jest zdecydowanie zalecany, gdy oczekuje się, że w przyszłości może zostać dodanych więcej parametrów, lub jeśli 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 w przypadku przyszłych zmian może zmniejszyć liczbę wycofanych metod i złożoność kodu wstecznie zgodnego.

Jeśli metoda nie została utworzona przy użyciu tego wzorca, możesz go zastosować, tworząc nową metodę z 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 parcelable

1. Kiedy używać

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

Lub gdy masz jeden typ danych, ale spodziewasz się, że w przyszłości będziesz musiał go rozszerzyć. Na przykład nie używaj String username. Użyj rozszerzalnego 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 ustawienia

[-Wexplicit-default, -Wenum-explicit-default] Podaj domyślne ustawienia pól. Gdy do parcelable zostaną dodane nowe pola, starsi klienci i serwery je odrzucą, ale w przypadku nowych klientów i serwerów domyślne wartości zostaną automatycznie wypełnione.

3. Używaj ParcelableHolder w przypadku rozszerzeń dostawcy

Jeśli zdefiniujesz AOSP parcelable, które osoby implementujące urządzenia muszą rozszerzyć, umieść w obiekcie instancję ParcelableHolder. Działa to jako punkt rozszerzenia bez tworzenia konfliktów scalania. Jest to podobne do rozszerzeń dołączonych interfejsów ale umożliwia osobom implementującym dołączenie własnego parcelable do istniejącego parcelable bez tworzenia własnego interfejsu i typów.

4. Struktury danych

  • Do reprezentowania map używaj tablic lub List parcelable, ponieważ AIDL nie obsługuje natywnie typów Map, które bezpiecznie tłumaczą się na wszystkie natywne backendy (np. FeatureToScoreEntry[]).
  • W przypadku pól powtarzanych używaj tablic obiektów parcelable zamiast tablic typów prostych, aby w przyszłości uniknąć konieczności stosowania tablic równoległych.
  • Zamiast serializowanych ciągów znaków lub JSON przez IPC używaj obiektów parcelable o silnym typowaniu.
  • W przypadku stanów używaj wyliczeń zamiast wartości logicznych, aby umożliwić rozszerzenie w przyszłości. W przypadku masek bitowych używaj const int zamiast typów enum, aby uniknąć kłopotliwego rzutowania w niektórych backendach.

Nieustrukturyzowane parcelable

1. Kiedy używać

Nieustrukturyzowane parcelable są dostępne w Javie z adnotacją @JavaOnlyStableParcelable, a w backendzie NDK z adnotacją @NdkOnlyStableParcelable. Zwykle są to stare i istniejące parcelable, których nie można uporządkować.

Stałe i wyliczenia

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. Wyliczenia powinny być zbiorami zamkniętymi

Wyliczenia powinny być zbiorami zamkniętymi. Uwaga: tylko właściciel interfejsu może dodawać elementy wyliczenia. Jeśli dostawcy lub producenci OEM muszą rozszerzyć te pola, potrzebny jest mechanizm alternatywny. W miarę możliwości należy preferować przesyłanie funkcji dostawcy do upstream. W niektórych przypadkach mogą być jednak dozwolone niestandardowe wartości dostawcy (dostawcy powinni jednak mieć mechanizm do obsługi wersji, np. AIDL, nie powinni mieć możliwości powodowania konfliktów, a te wartości nie powinny być udostępniane aplikacjom innych firm).

3. Unikaj wartości takich jak „NUM_ELEMENTS”

Ponieważ wyliczenia są wersjonowane, należy unikać wartości wskazujących, ile wartości jest obecnych. W C++ można to obejść za pomocą enum_range<>. W przypadku języka Rust użyj enum_values(). W Javie nie ma jeszcze rozwiązania.

Niezalecane: używanie wartości numerowanych

@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 wyliczenia

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] Używanie FileDescriptor jako argumentu lub wartości zwracanej metody interfejsu AIDL jest zdecydowanie odradzane. Szczególnie gdy AIDL jest implementowany w Javie, może to spowodować wyciek deskryptora pliku, chyba że zostanie starannie obsłużony. Jeśli akceptujesz FileDescriptor, musisz go zamknąć ręcznie, gdy nie jest już używany.

W przypadku backendów natywnych nie musisz się martwić, ponieważ FileDescriptor jest mapowany na unique_fd, który można automatycznie zamknąć. Niezależnie od języka backendu, którego używasz, warto 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 można automatycznie zamknąć.

Jednostki zmiennych

Upewnij się, że jednostki zmiennych są uwzględnione w nazwie, aby ich jednostki były 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

Znaczniki czasu muszą wskazywać swoje odniesienie

Znaczniki czasu (a właściwie wszystkie jednostki!) muszą wyraźnie wskazywać swoje 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;

Współbieżność i operacje asynchroniczne

Obsługuj długotrwałe 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.

Strukturyzuj asynchroniczne interfejsy API składające się z wywołania do przodu, argumentów wejściowych i interfejsu wywołania zwrotnego, aby uzyskać wyniki. Wskazówki dotyczące argumentów znajdziesz w sekcji Używaj unikalnych żądań i odpowiedzi.