Przedstawione tutaj sprawdzone metody stanowią przewodnik po skutecznym tworzeniu interfejsów AIDL z uwzględnieniem elastyczności interfejsu, zwłaszcza gdy jest używany do definiowania interfejsu API lub interakcji z platformami API.
AIDL może służyć do definiowania interfejsu API, gdy aplikacje muszą łączyć się ze sobą w tle lub z systemem. Więcej informacji o tworzeniu interfejsów programowania w aplikacjach z użyciem AIDL znajdziesz w artykule Android Interface Definition Language (AIDL). Przykłady zastosowania AIDL w praktyce znajdziesz w artykułach AIDL dla interfejsów HAL i stabilnej AIDL.
Obsługa wersji
Każdy zgodny wstecznie zrzut interfejsu API AIDL odpowiada wersji.
Aby zrobić zrzut, uruchom m <module-name>-freeze-api
. Po udostępnieniu klienta lub serwera API (np. w pociągu Mainline) musisz zrobić zrzut i utworzyć nową wersję. W przypadku interfejsów API od dostawców powinno to być związane z roczną wersją platformy.
Więcej informacji oraz informacje o typach dozwolonych zmian znajdziesz w artykule o interfejsach wersji.
Wytyczne dotyczące projektowania interfejsów API
Ogólne
1. Dokumentuj wszystko
- Dokumentuj każdą metodę pod kątem jej semantyki, argumentów, użycia wbudowanych wyjątków, wyjątków dotyczących poszczególnych usług i zwracanych wartości.
- Opisz semantykę każdego interfejsu.
- Udokumentuj semantyczne znaczenie wyliczenia i stałych.
- Udokumentuj to, co może być niejasne dla osoby zajmującej się implementacją.
- Podaj odpowiednie przykłady.
2. Wielkość liter
W przypadku metod, pól i argumentów używaj wielkich liter w kształcie wielbłąda, a dolne litery wielbłąda. Na przykład MyParcelable
w przypadku typu parcelable i anArgument
jako argument. W przypadku akronimów rozważ użycie akronimu wyrazu (NFC
-> Nfc
).
[-Wconst-name] Wartości i stałe enum powinny mieć postać ENUM_VALUE
i CONSTANT_NAME
Interfejsy
1. Nazewnictwo
[-Winterface-name] Nazwa interfejsu powinna zaczynać się od I
, np. IFoo
.
2. Unikaj dużego interfejsu z „obiektami” opartymi na identyfikatorach
Preferuj podinterfejsy, gdy istnieje wiele wywołań związanych z określonym interfejsem API. Dzięki temu:
- Ułatwia zrozumienie kodu klienta lub serwera
- Sprawia, że cykl życia obiektów jest prostszy
- Wykorzystuje fakt, że spoiwania są niemożliwe do skopiowania.
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 należy łączyć metody jednokierunkowej z dwukierunkową
[-Wmixed-oneway] Nie łącz metod jednokierunkowych z niejednokierunkowymi, ponieważ może to utrudnić zrozumienie modelu wątków w przypadku klientów i serwerów. Odczytując kod klienta w konkretnym interfejsie, musisz szukać każdej z metod, pod warunkiem że nie będzie ona blokowana.
4. Unikaj zwracania kodów stanu
W przypadku metod należy unikać kodów stanu jako wartości zwracanych, ponieważ wszystkie metody AIDL mają niejawny kod zwrotny stanu. Zobacz ServiceSpecificException
lub EX_SERVICE_SPECIFIC
. Zgodnie z konwencją w interfejsie AIDL wartości te są zdefiniowane jako stałe. Bardziej szczegółowe informacje znajdziesz w sekcji obsługi błędów we backendach AIDL.
5. Tablice jako parametry wyjściowe uznane za szkodliwe
[-Wout-array] Metody z parametrami wyjściowymi tablicowymi, takimi jak void foo(out String[] ret)
, są zwykle niewłaściwe, ponieważ rozmiar tablicy wyjściowej musi być zadeklarowany i przydzielony przez klienta w Javie, więc serwer nie może wybrać rozmiaru tablicy wyjściowej. To niepożądane zachowanie dzieje się ze względu
na sposób działania tablic w Javie (nie można ich zmienić przydziału). Zamiast tego używaj interfejsów API takich jak String[] foo()
.
6. Unikaj parametrów inout
[-Winout-parameter] Może to zdezorientować klientów, ponieważ nawet parametry in
wyglądają jak parametry out
.
7. Unikaj zewnętrznych i zewnętrznych parametrów @nullable, które nie są tablicami
[-Wout-nullable] Backend Javy nie obsługuje adnotacji @nullable
, a inne backendy to robią, więc out/inout @nullable T
może powodować niespójne działanie w różnych backendach. Na przykład backendy w języku innym niż Java mogą ustawić parametr @nullable
na wartość null (w języku C++ jako std::nullopt
), ale klient Java nie może odczytać go jako pustej.
Działki o określonym strukturze
1. Kiedy używać
Używaj arkuszy zbiorczych, jeśli masz wiele typów danych do wysłania.
Z kolei jeśli masz 1 typ danych, ale spodziewasz się, że w przyszłości trzeba będzie go rozszerzyć. Na przykład nie używaj nazwy String username
. Użyj działki rozszerzanej, takiego jak:
parcelable User {
String username;
}
Aby w przyszłości można było go przedłużyć w następujący sposób:
parcelable User {
String username;
int id;
}
2. Należy jawnie podać wartości domyślne
[-Wexplicit-default, -Wenum-explicit-default] Podaj jawne wartości domyślne pól.
Działki nieuporządkowane
1. Kiedy używać
Paczki nieustrukturyzowane są dostępne w Javie w @JavaOnlyStableParcelable
oraz w backendzie NDK w @NdkOnlyStableParcelable
. Zazwyczaj są to wciąż stare i istniejące działki, których nie można uporządkować.
Stałe i wyliczenia
1. W polach bitowych powinny być używane pola stałe
W polach bitowych powinny być stosowane pola stałe (np. const int FOO = 3;
w interfejsie).
2. Wartości typu enum powinny być zbiorami zamkniętymi.
Wartości typu enum powinny być zbiorami zamkniętymi. Uwaga: tylko właściciel interfejsu może dodawać elementy wyliczeniowe. Jeśli dostawcy lub producenci OEM muszą rozszerzyć te pola, potrzebny jest alternatywny mechanizm. W miarę możliwości należy preferować funkcję dostarczania danych od dostawcy. Jednak w niektórych przypadkach niestandardowe wartości dostawców mogą być dozwolone (chociaż dostawcy powinni dysponować mechanizmem ich wersji (na przykład AIDL) i nie mogą ze sobą kolidować, a te wartości nie powinny być udostępniane aplikacjom innych firm).
3. Unikaj wartości takich jak „NUM_ELEMENTS”
Wyliczenia mają postać wersji, więc należy unikać wartości, które wskazują liczbę dostępnych wartości. W C++ tę czynność da się obejść, używając języka enum_range<>
. W przypadku Rusta użyj enum_values()
. W Javie nie ma jeszcze rozwiązania.
Niezalecane: używaj 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 elementach wyliczających.
Niezalecane: użycie zbędnego prefiksu
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
Zalecane: bezpośrednie nadanie nazwy wyliczeniu
enum MyStatus {
GOOD,
BAD
}
Deskryptor pliku
[-Wfile-descriptor] Zdecydowanie odradzamy stosowanie FileDescriptor
jako argumentu lub wartości zwrotnej metody interfejsu AIDL. Zwłaszcza jeśli AIDL jest zaimplementowany w Javie, może to spowodować wyciek deskryptora pliku, chyba że zostanie to zrobione ostrożnie. Krótko mówiąc, jeśli akceptujesz FileDescriptor
, musisz zamknąć go ręcznie, gdy nie jest już używany.
W przypadku natywnych backendów jest to bezpieczne, ponieważ FileDescriptor
mapuje się na unique_fd
, która jest automatycznie zamykana. Jednak niezależnie od używanego języka backendu warto w ogóle NIE używać FileDescriptor
, ponieważ ograniczy to Twoją swobodę zmiany języka backendu w przyszłości.
Zamiast nich użyj wartości ParcelFileDescriptor
, która umożliwia automatyczne zamykanie.
Zmienne jednostki
Zadbaj o to, by jednostki zmiennych zostały uwzględnione w nazwie, tak by ich jednostki były dobrze zdefiniowane i zrozumiałe, bez potrzeby zaglądania 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ć plik referencyjny
Sygnatury czasowe (a właściwie 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;