Poradnik stylu AIDL

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-iStabilna 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_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;
  • 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;