Opisane tutaj 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 interfejsu API lub interakcji z interfejsem API.
AIDL można używać do definiowania interfejsu API, gdy aplikacje muszą komunikować się ze sobą w ramach procesu w tle lub gdy muszą komunikować się z systemem. Więcej informacji o tworzeniu interfejsów programowania w aplikacjach z użyciem AIDL znajdziesz w artykule Język definiowania interfejsu Androida (AIDL). Przykłady praktycznego zastosowania AIDL znajdziesz w artykułach AIDL dla HAL-i i Stabilna wersja AIDL.
Obsługa wersji
Każdy zgodny wstecz snapshot interfejsu AIDL API odpowiada jednej wersji.
Aby zrobić zrzut ekranu, uruchom m <module-name>-freeze-api
. Za każdym razem, gdy zostanie wydany klient lub serwer interfejsu API (np. w ramach głównego wątku), musisz utworzyć jego migawkę i utworzyć nową wersję. W przypadku interfejsów API między systemem a dostawcą powinno to nastąpić w ramach corocznej aktualizacji platformy.
Więcej informacji o dozwolonych zmianach znajdziesz w artykule Wersje interfejsów.
Wytyczne dotyczące projektowania interfejsów API
Ogólne
1. Udokumentuj wszystko
- Udokumentuj 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 zwrotnej.
- Udokumentuj semantykę każdego interfejsu.
- Dokumentowanie semantycznego znaczenia typów wyliczeniowych i stałych.
- Udokumentuj wszystko, co może być niejasne dla implementatora.
- W razie potrzeby podaj przykłady.
2. Obudowa
W przypadku typów używaj wielkiej litery w alfabecie łacińskim, a w przypadku metod, pól i argumentów – małej litery w alfabecie łacińskim. Na przykład MyParcelable
dla typu parcelable i anArgument
dla argumentu. W przypadku skrótów traktuj je jak słowa (NFC
-> Nfc
).
[-Wconst-name] Wartości i stałe typu enum powinny być typu ENUM_VALUE
i CONSTANT_NAME
Interfejsy
1. Nazwa
[-Winterface-name] Nazwa interfejsu powinna zaczynać się od I
, np. IFoo
.
2. Unikaj dużych interfejsów z „obiektowymi” identyfikatorami
Preferuj podinterfejsy, gdy występuje wiele wywołań związanych z danym interfejsem API. Dzięki temu:
- ułatwia zrozumienie kodu klienta lub serwera;
- upraszcza cykl życia obiektów;
- Wykorzystuje fakt, że skorore nie da się podrobić.
Niezalecane: jeden duży interfejs z obiektmi 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ż komplikuje to zrozumienie modelu wątków przez klientów i serwery. W szczególności podczas odczytywania kodu klienta danego interfejsu musisz sprawdzić, czy dana metoda będzie blokować czy nie.
4. Unikaj zwracania kodów stanu
Metody nie powinny używać kodów stanu jako wartości zwracanych, ponieważ wszystkie metody AIDL mają domyślny kod stanu. Zapoznaj się z artykułem ServiceSpecificException
lub EX_SERVICE_SPECIFIC
. Zgodnie z konwencją te wartości są definiowane jako stałe w interfejsie AIDL. Więcej szczegółowych informacji znajdziesz w sekcji dotyczącej obsługi błędów w backendach AIDL.
5. Tablice jako parametry wyjściowe uznane za szkodliwe
[-Wout-array] Metody, które mają parametry wyjściowe tablicy, takie jak void foo(out String[] ret)
, są zwykle złe, ponieważ rozmiar tablicy wyjściowej musi zostać zadeklarowany i przydzielony przez klienta w języku Java, a serwer nie może wybrać rozmiaru wyjścia tablicy. To niepożądane działanie wynika ze sposobu działania tablic w języku Java (nie można ich przydzielić ponownie). Zamiast tego użyj interfejsu API, takiego jak String[] foo()
.
6. Unikaj parametrów inout
[-Winout-parameter] Może to wprowadzać klientów w błąd, ponieważ parametry in
wyglądają jak parametry out
.
7. Unikaj parametrów out i inout @nullable innych niż tablice
[-Wout-nullable] Ponieważ backend Java nie obsługuje adnotacji @nullable
, a inne backendy tak, out/inout @nullable T
może powodować niespójności w działaniu różnych backendów. Na przykład backendy inne niż w Javie mogą ustawić parametr out @nullable
na null (w C++ ustawiając go jako std::nullopt
), ale klient w Javie nie może odczytać go jako null.
Strukturalne obiekty parcelable
1. Kiedy używać
Używaj obiektów strukturalnych, gdy chcesz wysłać wiele typów danych.
Możesz też użyć tego typu danych, jeśli masz tylko jeden typ danych, ale spodziewasz się, że w przyszłości będziesz musiał go rozszerzyć. Nie używaj na przykład String username
. Użyj rozszerzalnego obiektu Parcelable, na przykład:
parcelable User {
String username;
}
W przyszłości możesz go przedłużyć w ten sposób:
parcelable User {
String username;
int id;
}
2. Podawaj domyślne wartości wprost.
[-Wexplicit-default, -Wenum-explicit-default] Podanie domyślnych wartości pól.
Nieuporządkowane obiekty do przesyłania
1. Kiedy używać
Obiekty parcelable niestrukturyzowane są dostępne w Java z użyciem funkcji @JavaOnlyStableParcelable
i w backendzie NDK z użyciem funkcji @NdkOnlyStableParcelable
. Zwykle są to stare i dotychczasowe 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 (na przykład const int FOO = 3;
w interfejsie).
2. Enumy powinny być zamkniętymi zbiorami.
Enumy powinny być zamkniętymi zbiorami. Uwaga: tylko właściciel interfejsu może dodawać elementy enum. Jeśli dostawcy lub OEM-y muszą rozszerzyć te pola, potrzebny jest alternatywny mechanizm. Jeśli to możliwe, należy preferować funkcje dostawcy upstream. W niektórych przypadkach dozwolone mogą być jednak wartości niestandardowe dostawców (chociaż dostawcy powinni mieć mechanizm umożliwiający wersjonowanie, np. AIDL, który nie będzie ze sobą kolidować i nie będzie widoczny dla aplikacji innych firm).
3. Unikaj wartości takich jak „NUM_ELEMENTS”.
Wykazy są wersjonowane, dlatego należy unikać wartości, które wskazują, ile wartości jest obecnych. W C++ można to obejść, używając enum_range<>
. W przypadku Rust użyj enum_values()
. W przypadku Javy nie ma jeszcze rozwiązania.
Niezalecane: używanie wartości numerycznych
@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 nadmiarowych lub powtarzających się prefiksów i suffiksów w konstantach i wyliczeniach.
Niezalecane: używanie zbędącego prefiksu
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
Zalecany sposób: bezpośrednio nazwij typ enumeracji.
enum MyStatus {
GOOD,
BAD
}
FileDescriptor
[-Wfile-descriptor] Nie zalecamy używania argumentu FileDescriptor
ani wartości zwracanej przez metodę interfejsu AIDL. Szczególnie, gdy AIDL jest implementowany w języku Java, może to spowodować wyciek deskryptorów plików, chyba że zostaną one odpowiednio potraktowane. Jeśli zaakceptujesz FileDescriptor
, musisz zamknąć go ręcznie, gdy nie będzie już używany.
W przypadku natywnych backendów nie ma problemu, ponieważ FileDescriptor
mapuje się na unique_fd
, który można zamknąć automatycznie. Niezależnie od tego, którego języka backendu używasz, dobrze jest w ogóle nie używać znaku FileDescriptor
, ponieważ ogranicza to Twoją swobodę zmiany języka backendu w przyszłości.
Zamiast tego użyj interfejsu ParcelFileDescriptor
, który można zamknąć automatycznie.
Jednostki zmienne
Upewnij się, że nazwa zawiera jednostki zmiennej, 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ć na ich odniesienie
Czasy stempla (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;