Przewodnik stylistyczny dla AIDL

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;