Przewodnik po stylach AIDL

Opisane tutaj najlepsze praktyki służą jako przewodnik po skutecznym opracowywaniu interfejsów AIDL, zwracając uwagę na elastyczność interfejsu, szczególnie gdy AIDL jest używany do definiowania interfejsu API lub interakcji z powierzchniami API.

AIDL można wykorzystać do zdefiniowania interfejsu API, gdy aplikacje muszą komunikować się ze sobą w procesie w tle lub muszą komunikować się z systemem. Aby uzyskać więcej informacji na temat tworzenia interfejsów programistycznych w aplikacjach za pomocą AIDL, zobacz Język definicji interfejsu systemu Android (AIDL) . Przykłady AIDL w praktyce można znaleźć w artykułach AIDL dla HAL i Stable AIDL .

Wersjonowanie

Każda zgodna wstecz migawka API AIDL odpowiada wersji. Aby zrobić migawkę, uruchom m <module-name>-freeze-api . Za każdym razem, gdy zostanie wydany klient lub serwer API (np. w pociągu głównym), należy wykonać migawkę i utworzyć nową wersję. W przypadku interfejsów API typu system-dostawca powinno to nastąpić podczas corocznej aktualizacji platformy.

Aby uzyskać więcej szczegółów i informacji o typie dozwolonych zmian, zobacz Interfejsy wersjonowania .

Wytyczne dotyczące projektowania API

Ogólny

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 zwracanej.
  • Dokumentuj każdy interfejs pod kątem jego semantyki.
  • Dokumentuj semantyczne znaczenie wyliczeń i stałych.
  • Dokumentuj wszystko, co może być niejasne dla wdrażającego.
  • W stosownych przypadkach podaj przykłady.

2. Obudowa

Użyj górnej litery wielbłądziej dla typów i dolnej wielkości liter wielbłądziej dla metod, pól i argumentów. Na przykład MyParcelable dla typu paczki i anArgument dla argumentu. W przypadku akronimów należy uznać akronim za słowo ( NFC -> Nfc ).

[-Wconst-name] Wartości wyliczeniowe i stałe powinny mieć ENUM_VALUE i CONSTANT_NAME

Interfejsy

1. Nazywanie

[-Winterface-name] Nazwa interfejsu powinna zaczynać się od I like IFoo .

2. Unikaj dużego interfejsu z „obiektami” opartymi na identyfikatorze

Preferuj podinterfejsy, gdy istnieje wiele wywołań związanych z konkretnym API. Daje to następujące korzyści: - Ułatwia zrozumienie kodu klienta/serwera - Ułatwia cykl życia obiektów - Wykorzystuje fakt, że segregatory nie mogą zostać podrobione.

Niezalecane: pojedynczy, duży interfejs z obiektami opartymi na identyfikatorze

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: Indywidualne podinterfejsy

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 zrozumienie modelu wątków dla klientów i serwerów. W szczególności, czytając kod klienta konkretnego interfejsu, musisz sprawdzić każdą metodę, czy ta metoda będzie blokować, czy nie.

4. Unikaj zwracania kodów statusu

Metody powinny unikać kodów stanu jako wartości zwracanych, ponieważ wszystkie metody AIDL mają ukryty kod powrotu stanu. Zobacz ServiceSpecificException lub EX_SERVICE_SPECIFIC . Zgodnie z konwencją wartości te są definiowane jako stałe w interfejsie AIDL. Bardziej szczegółowe informacje znajdują się w sekcji Obsługa błędów w Backendach AIDL .

5. Tablice jako parametry wyjściowe uważane za szkodliwe

[-Wout-array] Metody posiadające 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 Javie, więc rozmiar tablicy wyjściowej nie może zostać wybrany przez serwer. To niepożądane zachowanie wynika ze sposobu działania tablic w Javie (nie można ich ponownie przydzielić). Zamiast tego preferuj interfejsy API, takie jak String[] foo() .

6. Unikaj parametrów wejściowych

[-Winout-parametr] Może to dezorientować klientów, ponieważ nawet parametry in wyglądają jak parametry out .

7. Unikaj parametrów out/inout @nullable, które nie są tablicami

[-Wout-nullable] Ponieważ backend Java nie obsługuje adnotacji @nullable , podczas gdy inne backendy to robią, out/inout @nullable T może powodować niespójne zachowanie pomiędzy backendami. Na przykład backendy inne niż Java mogą ustawić parametr out @nullable na wartość null (w C++ ustawiając go jako std::nullopt ), ale klient Java nie może odczytać go jako wartości null.

Paczki strukturyzowane

1. Kiedy stosować

Używaj ustrukturyzowanych przesyłek do przesyłek, jeśli masz do wysłania wiele typów danych.

Lub gdy obecnie masz jeden typ danych, ale spodziewasz się, że w przyszłości będziesz musiał go rozszerzyć. Na przykład nie używaj String username . Użyj rozszerzalnego opakowania, takiego jak poniżej:

parcelable User {
    String username;
}

Aby w przyszłości móc go rozszerzyć w następujący sposób:

parcelable User {
    String username;
    int id;
}

2. Wyraźnie podaj wartości domyślne

[-Wexplicit-default, -Wenum-explicit-default] Podaj jawne wartości domyślne dla pól.

Paczki niestrukturalne

1. Kiedy stosować

Pakiety nieustrukturyzowane są obecnie dostępne w Javie z @JavaOnlyStableParcelable oraz w backendzie NDK z @NdkOnlyStableParcelable . Zwykle są to stare i istniejące działki, których nie można łatwo uporządkować.

Stałe i wyliczenia

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ć zbiorami domkniętymi.

Wyliczenia 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. Jeśli to możliwe, należy preferować funkcjonalność dostawcy wyższego szczebla. Jednakże w niektórych przypadkach mogą być dozwolone niestandardowe wartości dostawców (chociaż dostawcy powinni mieć mechanizm umożliwiający ich wersjonowanie, być może sam AIDL, nie powinni móc kolidować ze sobą i wartości te nie powinny być narażone na działanie aplikacji innych firm).

3. Unikaj wartości takich jak „NUM_ELEMENTS”

Ponieważ wyliczenia są wersjonowane, należy unikać wartości wskazujących, ile wartości jest obecnych. W C++ można to obejść za pomocą enum_range<> . W przypadku Rusta użyj enum_values() . W Javie nie ma jeszcze rozwiązania.

Niezalecane: używanie wartości numerowanych

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. Unikaj zbędnych przedrostków i przyrostków

[-Wredundant-name] Unikaj zbędnych lub powtarzających się przedrostków i sufiksów w stałych i modułach wyliczających.

Niezalecane: używanie nadmiarowego prefiksu

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Zalecane: Bezpośrednie nadanie nazwy wyliczeniu

enum MyStatus {
    GOOD,
    BAD
}

Deskryptor pliku

[-Wfile-descriptor] Zdecydowanie odradza się używanie FileDescriptor jako argumentu lub wartości zwracanej przez metodę interfejsu AIDL. Zwłaszcza, gdy AIDL jest zaimplementowany w Javie, może to spowodować wyciek deskryptora pliku, jeśli nie zostanie ostrożnie potraktowany. Zasadniczo, jeśli zaakceptujesz FileDescriptor , musisz go zamknąć ręcznie, gdy nie jest już używany.

W przypadku natywnych backendów jesteś bezpieczny, ponieważ FileDescriptor odwzorowuje na unique_fd , który można automatycznie zamknąć. Jednak niezależnie od języka backendu, którego byś używał, mądrze jest w ogóle NIE używać FileDescriptor , ponieważ ograniczy to twoją swobodę zmiany języka backendu w przyszłości.

Zamiast tego użyj ParcelFileDescriptor , który można automatycznie zamknąć.

Zmienne jednostki

Upewnij się, że w nazwie znajdują się jednostki zmienne, aby ich jednostki 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

Znaczki czasowe muszą wskazywać ich odniesienie

Znaczniki czasu (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;