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
Listparcelable, ponieważ AIDL nie obsługuje natywnie typówMap, które bezpiecznie tłumaczą się na wszystkie natywne backendy (np.FeatureToScoreEntry[]). - W przypadku pól powtarzanych używaj tablic obiektów
parcelablezamiast 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
parcelableo 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 intzamiast typówenum, 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.