Przedstawione tu sprawdzone metody stanowią przewodnik po skutecznym tworzeniu interfejsów AIDL z uwzględnieniem elastyczności interfejsu, zwłaszcza gdy AIDL jest używany do definiowania interfejsu API lub interakcji z jego powierzchniami.
Interfejsu AIDL można używać do definiowania interfejsu API, gdy aplikacje muszą komunikować się ze sobą w procesie w tle lub z systemem. Więcej informacji o tworzeniu interfejsów programowania w aplikacjach z AIDL znajdziesz w artykule Android Interface Definition Language (AIDL). Przykłady użycia AIDL w praktyce znajdziesz w sekcjach AIDL dla HAL-i i Stabilny AIDL.
Obsługa wersji
Każda wersja to migawka interfejsu AIDL API zgodna wstecznie.
Aby zrobić zdjęcie, uruchom m <module-name>-freeze-api. Za każdym razem, gdy klient lub serwer interfejsu API zostanie wydany (np. w wersji głównej), musisz zrobić zrzut i utworzyć nową wersję. W przypadku interfejsów API typu system-dostawca powinno to nastąpić wraz z roczną aktualizacją platformy.
Więcej informacji o szczegółach i rodzajach dozwolonych zmian znajdziesz w artykule Wersjonowanie interfejsów.
Wytyczne dotyczące projektowania interfejsów API
Ogólne
1. Dokumentuj wszystko
- Opisz każdą metodę, podając jej semantykę, argumenty, użycie wbudowanych wyjątków, wyjątków specyficznych dla usługi i wartość zwracaną.
- Opisz każdy interfejs pod kątem jego semantyki.
- Dokumentuj znaczenie semantyczne wyliczeń i stałych.
- Opisz 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 notacji UpperCamelCase, a w przypadku metod, pól i argumentów – notacji lowerCamelCase. Na przykład MyParcelable w przypadku typu podlegającego serializacji i anArgument w przypadku argumentu. W przypadku akronimów traktuj je jako słowa (NFC -> Nfc).
[-Wconst-name] Wartości typu enum i stałe powinny być ENUM_VALUE iCONSTANT_NAME
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
Wybieraj podinterfejsy, gdy jest wiele wywołań związanych z określonym interfejsem API. Daje to te korzyści:
- Ułatwia zrozumienie kodu klienta lub serwera
- Upraszcza cykl życia obiektów
- Wykorzystuje fakt, że bindery są niemożliwe do sfałszowania.
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: interfejsy indywidualne
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. Podczas odczytywania kodu klienta określonego interfejsu musisz sprawdzić, czy każda metoda blokuje, czy nie.
4. Unikaj zwracania kodów stanu
Metody powinny unikać kodów stanu jako wartości zwracanych, ponieważ wszystkie metody AIDL mają niejawny kod stanu zwracany. Zobacz ServiceSpecificException lub EX_SERVICE_SPECIFIC. Zgodnie z konwencją wartości te są definiowane jako stałe w interfejsie AIDL. Więcej informacji znajdziesz w sekcji dotyczącej obsługi błędów w AIDL.
5. Tablice jako parametry wyjściowe uznawane za szkodliwe
[-Wout-array] Metody z parametrami wyjściowymi w postaci tablicy, np. void foo(out String[] ret), są zwykle niezalecane, ponieważ rozmiar tablicy wyjściowej musi zostać zadeklarowany i przydzielony przez klienta w Javie, 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 używaj interfejsów API, takich jak String[] foo().
6. Unikaj parametrów inout
[-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] Backend Java nie obsługuje adnotacji @nullable, podczas gdy inne backendy to robią, więc 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 wyjściowy @nullablena wartość null (w C++ ustawiają go jako std::nullopt), ale klient Java nie może odczytać go jako null.
Obiekty Parcelable o strukturze
1. Kiedy używać
Jeśli chcesz wysłać wiele typów danych, użyj strukturalnych obiektów Parcelable.
Możesz też użyć tego typu, gdy masz tylko jeden typ danych, ale spodziewasz się, że w przyszłości będziesz musiał go rozszerzyć. Nie używaj na przykład elementu 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. Jawne podawanie wartości domyślnych
[-Wexplicit-default, -Wenum-explicit-default] Podaj domyślne wartości pól.
Obiekty Parcelable bez struktury
1. Kiedy używać
Niestrukturalne obiekty Parcelable są dostępne w języku Java z użyciem
@JavaOnlyStableParcelable oraz w backendzie NDK z użyciem
@NdkOnlyStableParcelable. Zwykle są to stare i istniejące obiekty Parcelable, których nie można uporządkować.
Stałe i wartości w polu enum
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ć zamkniętymi zbiorami.
Wyliczenia powinny być zamkniętymi zbiorami. Uwaga: tylko właściciel interfejsu może dodawać elementy enum. Jeśli dostawcy lub producenci OEM potrzebują rozszerzenia tych pól, konieczny jest alternatywny mechanizm. W miarę możliwości należy preferować przekazywanie funkcji dostawcy do upstreamu. W niektórych przypadkach można jednak zezwolić na przekazywanie niestandardowych wartości dostawców (chociaż dostawcy powinni mieć mechanizm wersjonowania, np. AIDL, nie powinni wchodzić ze sobą w konflikt, a wartości te 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, które wskazują, 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 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 nadmiarowych prefiksów i sufiksów
[-Wredundant-name] Unikaj nadmiarowych lub powtarzających się prefiksów i sufiksów w stałych i enumeratorach.
Niezalecane: używanie nadmiarowego 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 wysoce odradzane. Szczególnie w przypadku implementacji AIDL w języku Java może to powodować wyciek deskryptora pliku, jeśli nie zostanie odpowiednio obsłużone. 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 mapowane na unique_fd, które można automatycznie zamknąć. Niezależnie od tego, jakiego języka backendu używasz, nie zalecamy używania znaku FileDescriptor, ponieważ ogranicza to możliwość zmiany języka backendu w przyszłości.
Zamiast tego użyj elementu ParcelFileDescriptor, który można automatycznie zamknąć.
Jednostki zmienne
Upewnij się, że jednostki zmiennych są uwzględnione w nazwie, aby 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
Sygnatury czasowe muszą wskazywać swoje odniesienie
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;