Przewodnik stylistyczny dla AIDL

Zawarte tu sprawdzone metody służą jako przewodnik po skutecznym i elastycznym tworzeniu interfejsów AIDL, zwłaszcza gdy AIDL jest używany do definiowania interfejsu API lub interakcji z interfejsami API.

AIDL można użyć do zdefiniowania interfejsu API, gdy aplikacje muszą komunikować się ze sobą w ramach procesu w tle lub 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-iStabilny AIDL.

Obsługa wersji

Każdy zgodny wstecz snapshot interfejsu AIDL API odpowiada jednej wersji. Aby zrobić zrzut, uruchom m <module-name>-freeze-api. Za każdym razem, gdy klient lub serwer interfejsu API zostanie wydany (np. w głównej gałęzi), musisz utworzyć jego migawkę i 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. Wielkość liter

W przypadku typów używaj wielkiej litery, a w przypadku metod, pól i argumentów – małej litery. Na przykład MyParcelable dla typu parcelable i anArgument dla argumentu. W przypadku skrótów traktuj je jak słowo (NFC -> Nfc).

[-Wconst-name] Wartości i stałe typu enum powinny być typu ENUM_VALUECONSTANT_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;
  • Sprawia, że cykl życia obiektów jest prostszy
  • 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

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ą 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 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 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 typu out i inout @nullable, które nie są tablicami

[-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 zarządzaniu backendami. 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.

Strukturowane obiekty parcelable

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ć. 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 w prosty sposób.

[-Wexplicit-default, -Wenum-explicit-default] Podanie jawnych wartości domyślnych dla pól.

Nieuporządkowane obiekty do przesyłania

1. Kiedy używać

Obiekty parcelable o nieustrukturyzowanej strukturze są dostępne w Java za pomocą funkcji @JavaOnlyStableParcelable, a w backendzie NDK za pomocą funkcji @NdkOnlyStableParcelable. Zwykle są to stare i dotychczasowe obiekty, 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.

Wartości typu enum powinny być zbiorami zamkniętymi. Uwaga: tylko właściciel interfejsu może dodawać elementy enum. 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. 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] Zdecydowanie odradzamy stosowanie FileDescriptor jako argumentu lub wartości zwrotnej metody 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. 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 tego użyj interfejsu ParcelFileDescriptor, który można zamknąć automatycznie.

Zmienne jednostki

Upewnij się, że nazwa zawiera jednostki zmienne, 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ć ich odniesienie

Czas stempla (właściwie wszystkie jednostki) musi 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;