Stabilny AIDL

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Android 10 dodaje obsługę stabilnego języka Android Interface Definition Language (AIDL), nowego sposobu śledzenia interfejsu programu aplikacji (API)/interfejsu binarnego aplikacji (ABI) zapewnianego przez interfejsy AIDL. Stabilny AIDL ma następujące kluczowe różnice w stosunku do AIDL:

  • Interfejsy są zdefiniowane w systemie kompilacji za pomocą aidl_interfaces .
  • Interfejsy mogą zawierać tylko dane strukturalne. Parcelables reprezentujące żądane typy są tworzone automatycznie na podstawie ich definicji AIDL i są automatycznie kierowane i unmarshallowane.
  • Interfejsy można zadeklarować jako stabilne (kompatybilne wstecz). Kiedy tak się dzieje, ich interfejs API jest śledzony i wersjonowany w pliku obok interfejsu AIDL.

Definiowanie interfejsu AIDL

Definicja aidl_interface wygląda następująco:

aidl_interface {
    name: "my-aidl",
    srcs: ["srcs/aidl/**/*.aidl"],
    local_include_dir: "srcs/aidl",
    imports: ["other-aidl"],
    versions_with_info: [
        {
            version: "1",
            imports: ["ohter-aidl-V1"],
        },
        {
            version: "2",
            imports: ["other-aidl-V3"],
        }
    ],
    stability: "vintf",
    backend: {
        java: {
            enabled: true,
            platform_apis: true,
        },
        cpp: {
            enabled: true,
        },
        ndk: {
            enabled: true,
        },
        rust: {
            enabled: true,
        },
    },

}
  • name : Nazwa modułu interfejsu AIDL, który jednoznacznie identyfikuje interfejs AIDL.
  • srcs : Lista plików źródłowych AIDL, które składają się na interfejs. Ścieżka dla AIDL typu Foo zdefiniowana w pakiecie com.acme powinna znajdować się w <base_path>/com/acme/Foo.aidl , gdzie <base_path> może być dowolnym katalogiem powiązanym z katalogiem, w którym znajduje się Android.bp . W powyższym przykładzie <base_path> to srcs/aidl .
  • local_include_dir : Ścieżka, od której zaczyna się nazwa pakietu. Odpowiada <base_path> wyjaśnionej powyżej.
  • imports : Lista używanych przez to modułów aidl_interface . Jeśli jeden z twoich interfejsów AIDL używa interfejsu lub pliku parcelable z innego aidl_interface , umieść tutaj jego nazwę. Może to być sama nazwa, aby odnosić się do najnowszej wersji, lub nazwa z sufiksem wersji (takim jak -V1 ), aby odnosić się do określonej wersji. Określanie wersji jest obsługiwane od Androida 12
  • versions : poprzednie wersje interfejsu, które są zamrożone pod api_dir , Począwszy od Androida 11, versions są zamrożone pod aidl_api/ name . Jeśli nie ma zamrożonych wersji interfejsu, nie należy tego określać i nie będzie sprawdzania zgodności. To pole zostało zastąpione przezversions_with_info dla versions_with_info 13 i nowszych.
  • versions_with_info : Lista krotek, z których każda zawiera nazwę zamrożonej wersji oraz listę z importowanymi wersjami innych modułów aidl_interface zaimportowanych przez tę wersję interfejsu aidl_interface. Definicja wersji V interfejsu AIDL IFACE znajduje się pod adresem aidl_api/ IFACE / V . To pole zostało wprowadzone w Androidzie 13 i nie powinno być modyfikowane bezpośrednio w Android.bp. Pole jest dodawane lub aktualizowane przez wywołanie *-update-api lub *-freeze-api . Ponadto pola versions są automatycznie migrowane do versions_with_info z_informacjami, gdy użytkownik *-update-api lub *-freeze-api .
  • stability : Opcjonalna flaga obietnicy stabilności tego interfejsu. Obecnie obsługuje tylko "vintf" . Jeśli ta opcja jest nieustawiona, odpowiada to interfejsowi ze stabilnością w tym kontekście kompilacji (tak więc załadowany tutaj interfejs może być używany tylko z rzeczami skompilowanymi razem, na przykład w pliku system.img). Jeśli jest to ustawione na "vintf" , odpowiada to obietnicy stabilności: interfejs musi być stabilny tak długo, jak jest używany.
  • gen_trace : Opcjonalna flaga do włączania i wyłączania śledzenia. Wartość domyślna to false .
  • host_supported : Opcjonalna flaga, która po ustawieniu na wartość true udostępnia wygenerowane biblioteki środowisku hosta.
  • unstable : Opcjonalna flaga używana do oznaczenia, że ​​ten interfejs nie musi być stabilny. Gdy ta opcja ma wartość true , system kompilacji nie tworzy zrzutu interfejsu API dla interfejsu ani nie wymaga jego aktualizacji.
  • backend.<type>.enabled : te flagi przełączają każdy z backendów, dla których kompilator AIDL generuje kod. Obecnie obsługiwane są cztery backendy: Java, C++, NDK i Rust. Backendy Java, C++ i NDK są domyślnie włączone. Jeśli którykolwiek z tych trzech backendów nie jest potrzebny, należy go jawnie wyłączyć. Rdza jest domyślnie wyłączona.
  • backend.<type>.apex_available : Lista nazw APEX, dla których dostępna jest wygenerowana biblioteka pośrednicząca.
  • backend.[cpp|java].gen_log : Opcjonalna flaga określająca, czy generować dodatkowy kod do zbierania informacji o transakcji.
  • backend.[cpp|java].vndk.enabled : Opcjonalna flaga czyniąca ten interfejs częścią VNDK. Wartość domyślna to false .
  • backend.java.platform_apis : opcjonalna flaga kontrolująca, czy biblioteka pośrednicząca Java jest budowana na podstawie prywatnych interfejsów API z platformy. Powinno to być ustawione na "true" , gdy stability jest ustawiona na "vintf" .
  • backend.java.sdk_version : Opcjonalna flaga służąca do określania wersji zestawu SDK, na podstawie której zbudowana jest biblioteka pośrednicząca języka Java. Wartość domyślna to "system_current" . Nie należy tego ustawiać, gdy backend.java.platform_apis ma wartość true.
  • backend.java.platform_apis : opcjonalna flaga, która powinna być ustawiona na wartość true , gdy generowane biblioteki muszą być kompilowane na podstawie interfejsu API platformy, a nie zestawu SDK.

Dla każdej kombinacji wersji i włączonych backendów tworzona jest biblioteka pośrednicząca. Aby dowiedzieć się, jak odwoływać się do określonej wersji biblioteki pośredniczącej dla określonego zaplecza, zobacz Zasady nazewnictwa modułów .

Pisanie plików AIDL

Interfejsy w stabilnym AIDL są podobne do tradycyjnych interfejsów, z wyjątkiem tego, że nie wolno im używać nieustrukturyzowanych parcelables (ponieważ nie są one stabilne!). Podstawową różnicą w stabilnym AIDL jest sposób definiowania parcelables. Wcześniej parcelables były zadeklarowane w przód ; w stabilnym AIDL pola i zmienne parcelables są definiowane jawnie.

// in a file like 'some/package/Thing.aidl'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}

Wartość domyślna jest obecnie obsługiwana (ale nie wymagana) dla boolean , char , float , double , byte , int , long i String . W systemie Android 12 obsługiwane są również wartości domyślne dla wyliczeń zdefiniowanych przez użytkownika. Jeśli wartość domyślna nie jest określona, ​​używana jest wartość podobna do 0 lub pusta. Wyliczenia bez wartości domyślnej są inicjowane na 0, nawet jeśli nie ma modułu wyliczającego zero.

Korzystanie z bibliotek pośredniczących

Po dodaniu bibliotek pośredniczących jako zależności do modułu możesz dołączyć je do swoich plików. Oto przykłady bibliotek pośredniczących w systemie kompilacji ( Android.mk może być również używany do starszych definicji modułów):

cc_... {
    name: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // can also be shared_libs if desire is to load a library and share
    // it among multiple users or if you only need access to constants
    static_libs: ["my-module-name-java"],
    ...
}
# or
rust_... {
    name: ...,
    rust_libs: ["my-module-name-rust"],
    ...
}

Przykład w C++:

#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
    // use just like traditional AIDL

Przykład w Javie:

import some.package.IFoo;
import some.package.Thing;
...
    // use just like traditional AIDL

Przykład w Ruście:

use aidl_interface_name::aidl::some::package::{IFoo, Thing};
...
    // use just like traditional AIDL

Interfejsy wersjonowania

Zadeklarowanie modułu o nazwie foo tworzy również cel w systemie kompilacji, którego można użyć do zarządzania interfejsem API modułu. Po zbudowaniu foo-freeze-api dodaje nową definicję API w api_dir lub aidl_api/ name , w zależności od wersji Androida, i dodaje plik .hash , oba reprezentujące nowo zamrożoną wersję interfejsu. foo-freeze-api aktualizuje również właściwośćversions_with_info, aby odzwierciedlić dodatkową wersję i imports dla versions_with_info . Zasadniczo imports versions_with_info są kopiowane z pola imports . Ale najnowsza stabilna wersja jest określona w imports w versions_with_info dla importu, który nie ma jawnej wersji. Po określeniu właściwościversions_with_info system kompilacji uruchamia testy zgodności między zamrożonymi versions_with_info , a także między Top of Tree (ToT) a najnowszą zamrożoną wersją.

Ponadto musisz zarządzać definicją interfejsu API wersji ToT. Za każdym razem, gdy API jest aktualizowane, uruchom foo-update-api , aby zaktualizować aidl_api/ name /current , który zawiera definicję API wersji ToT.

Aby zachować stabilność interfejsu, właściciele mogą dodawać nowe:

  • Metody do końca interfejsu (lub metody z jawnie zdefiniowanymi nowymi numerami seryjnymi)
  • Elementy do końca parcelable (wymaga dodania wartości domyślnej dla każdego elementu)
  • Stałe wartości
  • W systemie Android 11 moduły wyliczające
  • W systemie Android 12 pola do końca unii

Żadne inne działania nie są dozwolone i nikt inny nie może modyfikować interfejsu (w przeciwnym razie istnieje ryzyko kolizji ze zmianami wprowadzonymi przez właściciela).

Aby sprawdzić, czy wszystkie interfejsy są zamrożone do wydania, możesz zbudować z następującymi zmiennymi środowiskowymi:

  • AIDL_FROZEN_REL=true m ... - kompilacja wymaga zamrożenia wszystkich stabilnych interfejsów AIDL, które nie mają określonego owner: pole.
  • AIDL_FROZEN_OWNERS="aosp test" - build wymaga zamrożenia wszystkich stabilnych interfejsów AIDL z owner: pole określone jako "aosp" lub "test".

Stabilność importu

Aktualizacja wersji importu dla zamrożonych wersji interfejsu jest wstecznie kompatybilna w stabilnej warstwie AIDL. Jednak ich aktualizacja wymaga zaktualizowania wszystkich serwerów i klientów korzystających ze starej wersji interfejsu, a niektóre aplikacje mogą być zdezorientowane podczas mieszania różnych wersji typów. Ogólnie rzecz biorąc, w przypadku pakietów zawierających tylko typy lub wspólnych pakietów jest to bezpieczne, ponieważ kod musi być już napisany, aby obsłużyć nieznane typy z transakcji IPC.

W platformie Android kod android.hardware.graphics.common jest największym przykładem tego typu aktualizacji wersji.

Używanie wersjonowanych interfejsów

Metody interfejsu

W czasie wykonywania, podczas próby wywołania nowych metod na starym serwerze, nowi klienci otrzymują błąd lub wyjątek, w zależności od zaplecza.

  • backend cpp pobiera ::android::UNKNOWN_TRANSACTION .
  • backend ndk pobiera STATUS_UNKNOWN_TRANSACTION .
  • backend java otrzymuje android.os.RemoteException z komunikatem, że interfejs API nie jest zaimplementowany.

Aby zapoznać się ze strategiami radzenia sobie z tym, zobacz odpytywanie wersji i używanie wartości domyślnych .

Paczki

Kiedy nowe pola są dodawane do parcelables, starzy klienci i serwery je odrzucają. Kiedy nowi klienci i serwery otrzymują stare paczki, wartości domyślne dla nowych pól są automatycznie wypełniane. Oznacza to, że wartości domyślne muszą być określone dla wszystkich nowych pól w przesyłce.

Klienci nie powinni oczekiwać, że serwery będą używać nowych pól, chyba że wiedzą, że serwer implementuje wersję, w której zdefiniowano pole (zobacz Wyszukiwanie wersji ).

Wyliczenia i stałe

Podobnie klienci i serwery powinni odpowiednio odrzucić lub zignorować nierozpoznane stałe wartości i moduły wyliczające, ponieważ w przyszłości może ich być więcej. Na przykład serwer nie powinien przerywać, gdy otrzyma moduł wyliczający, o którym nie wie. Powinien albo to zignorować, albo zwrócić coś, aby klient wiedział, że nie jest obsługiwany w tej implementacji.

Związki

Próba wysłania unii z nowym polem kończy się niepowodzeniem, jeśli odbiorca jest stary i nie wie o polu. Implementacja nigdy nie zobaczy unii z nowym polem. Błąd jest ignorowany, jeśli jest to transakcja jednokierunkowa; w przeciwnym razie błąd to BAD_VALUE (dla zaplecza C++ lub NDK) lub IllegalArgumentException (dla zaplecza Java). Błąd jest odbierany, jeśli klient wysyła zestaw unii do nowego pola na stary serwer lub gdy jest to stary klient odbierający unię z nowego serwera.

Zasady nazewnictwa modułów

W systemie Android 11 dla każdej kombinacji włączonych wersji i backendów automatycznie tworzony jest moduł biblioteki pośredniczącej. Aby odwołać się do określonego modułu biblioteki pośredniczącej w celu połączenia, nie używaj nazwy modułu aidl_interface , ale nazwy modułu biblioteki pośredniczącej, czyli ifacename - version - backend , gdzie

  • ifacename : nazwa modułu aidl_interface
  • version jest jedną z
    • V version-number dla wersji zamrożonych
    • V latest-frozen-version-number + 1 dla wersji wierzchołka drzewa (jeszcze do zamrożenia)
  • backend jest jednym z
    • java dla backendu Javy,
    • cpp dla backendu C++,
    • ndk lub ndk_platform dla zaplecza NDK. Ta pierwsza jest przeznaczona do aplikacji, a druga do korzystania z platformy,
    • rust dla zaplecza Rust.

Załóżmy, że istnieje moduł o nazwie foo , którego najnowsza wersja to 2 i obsługuje zarówno NDK, jak i C++. W tym przypadku AIDL generuje następujące moduły:

  • Na podstawie wersji 1
    • foo-V1-(java|cpp|ndk|ndk_platform|rust)
  • Na podstawie wersji 2 (najnowsza stabilna wersja)
    • foo-V2-(java|cpp|ndk|ndk_platform|rust)
  • Na podstawie wersji ToT
    • foo-V3-(java|cpp|ndk|ndk_platform|rust)

W porównaniu z Androidem 11,

  • foo- backend , który odnosił się do najnowszej stabilnej wersji staje się foo- V2 - backend
  • foo-unstable- backend , który odnosił się do wersji ToT staje się foo- foo- V3 - backend

Nazwy plików wyjściowych są zawsze takie same jak nazwy modułów.

  • Na podstawie wersji 1: foo-V1-(cpp|ndk|ndk_platform|rust).so
  • Na podstawie wersji 2: foo-V2-(cpp|ndk|ndk_platform|rust).so
  • Na podstawie wersji ToT: foo-V3-(cpp|ndk|ndk_platform|rust).so

Należy zauważyć, że kompilator AIDL nie tworzy ani modułu w wersji unstable , ani modułu bez wersji dla stabilnego interfejsu AIDL. Od Androida 12 nazwa modułu generowana ze stabilnego interfejsu AIDL zawsze zawiera jego wersję.

Nowe metody interfejsu meta

Android 10 dodaje kilka metod interfejsu meta dla stabilnego AIDL.

Zapytanie o wersję interfejsu obiektu zdalnego

Klienci mogą zapytać o wersję i skrót interfejsu, który implementuje obiekt zdalny, i porównać zwrócone wartości z wartościami interfejsu używanego przez klienta.

Przykład z zapleczem cpp :

sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::HASH;
std::string remote_hash = foo->getInterfaceHash();

Przykład z zapleczem ndk (i ndk_platform ):

IFoo* foo = ... // the remote object
int32_t my_ver = IFoo::version;
int32_t remote_ver = 0;
if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::hash;
std::string remote_hash;
foo->getInterfaceHash(&remote_hash);

Przykład z backendem java :

IFoo foo = ... // the remote object
int myVer = IFoo.VERSION;
int remoteVer = foo.getInterfaceVersion();
if (remoteVer < myVer) {
  // the remote side is using an older interface
}

String myHash = IFoo.HASH;
String remoteHash = foo.getInterfaceHash();

W przypadku języka Java strona zdalna MUSI zaimplementować getInterfaceVersion() i getInterfaceHash() w następujący sposób ( super jest używane zamiast IFoo , aby uniknąć błędów kopiowania/wklejania. Adnotacja @SuppressWarnings("static") może być potrzebna do wyłączenia ostrzeżeń, w zależności od konfiguracja javac ):

class MyFoo extends IFoo.Stub {
    @Override
    public final int getInterfaceVersion() { return super.VERSION; }

    @Override
    public final String getInterfaceHash() { return super.HASH; }
}

Wynika to z faktu, że wygenerowane klasy ( IFoo , IFoo.Stub itd.) są współużytkowane przez klienta i serwer (na przykład klasy mogą znajdować się w ścieżce klas rozruchu). Gdy klasy są udostępniane, serwer jest również powiązany z najnowszą wersją klas, nawet jeśli został zbudowany ze starszą wersją interfejsu. Jeśli ten metainterfejs jest zaimplementowany w klasie udostępnionej, zawsze zwraca najnowszą wersję. Jednak implementując powyższą metodę, numer wersji interfejsu jest osadzony w kodzie serwera (ponieważ IFoo.VERSION jest static final int , która jest wstawiana, gdy następuje odwołanie), a zatem metoda może zwrócić dokładną wersję serwera, który został zbudowany z.

Radzenie sobie ze starszymi interfejsami

Możliwe, że klient został zaktualizowany do nowszej wersji interfejsu AIDL, ale serwer używa starego interfejsu AIDL. W takich przypadkach wywołanie metody na starym interfejsie zwraca UNKNOWN_TRANSACTION .

Dzięki stabilnemu AIDL klienci mają większą kontrolę. Po stronie klienta można ustawić domyślną implementację interfejsu AIDL. Metoda w domyślnej implementacji jest wywoływana tylko wtedy, gdy metoda nie jest zaimplementowana po stronie zdalnej (ponieważ została zbudowana ze starszą wersją interfejsu). Ponieważ wartości domyślne są ustawiane globalnie, nie należy ich używać z potencjalnie współdzielonych kontekstów.

Przykład w C++ w systemie Android 13 i nowszych:

class MyDefault : public IFooDefault {
  Status anAddedMethod(...) {
   // do something default
  }
};

// once per an interface in a process
IFoo::setDefaultImpl(::android::sp<MyDefault>::make());

foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                         // remote side is not implementing it

Przykład w Javie:

IFoo.Stub.setDefaultImpl(new IFoo.Default() {
    @Override
    public xxx anAddedMethod(...)  throws RemoteException {
        // do something default
    }
}); // once per an interface in a process


foo.anAddedMethod(...);

Nie musisz podawać domyślnej implementacji wszystkich metod w interfejsie AIDL. Metody, które są gwarantowane do zaimplementowania po stronie zdalnej (ponieważ masz pewność, że zdalny jest zbudowany, gdy metody były w opisie interfejsu AIDL) nie muszą być nadpisywane w domyślnej klasie impl .

Konwersja istniejącego AIDL na ustrukturyzowany/stabilny AIDL

Jeśli masz istniejący interfejs AIDL i kod, który go używa, wykonaj następujące kroki, aby przekonwertować interfejs na stabilny interfejs AIDL.

  1. Zidentyfikuj wszystkie zależności interfejsu. Dla każdego pakietu, od którego zależy interfejs, określ, czy pakiet jest zdefiniowany w stabilnym AIDL. Jeśli nie zdefiniowano, pakiet musi zostać przekonwertowany.

  2. Konwertuj wszystkie paczki w swoim interfejsie na stabilne paczki (same pliki interfejsu mogą pozostać niezmienione). Zrób to, wyrażając ich strukturę bezpośrednio w plikach AIDL. Klasy zarządzania muszą zostać przepisane, aby używać tych nowych typów. Można to zrobić przed utworzeniem pakietu aidl_interface (poniżej).

  3. Utwórz pakiet aidl_interface (jak opisano powyżej), który zawiera nazwę twojego modułu, jego zależności i wszelkie inne potrzebne informacje. Aby go ustabilizować (nie tylko ustrukturyzować), należy go również wersjonować. Aby uzyskać więcej informacji, zobacz Wersjonowanie interfejsów .