Backend AIDL jest celem generowania kodu stub. Zawsze używaj plików AIDL w określonym języku z konkretnym środowiskiem wykonawczym. W zależności od kontekstu należy używać różnych backendów AIDL.
W tabeli poniżej stabilność interfejsu API odnosi się do możliwości skompilowania kodu w odniesieniu do tego interfejsu w taki sposób, aby kod można było dostarczać niezależnie od pliku binarnego system.img
libbinder.so
.
AIDL ma te interfejsy backendu:
Backend | Język | Powierzchnia interfejsu API | Systemy kompilacji |
---|---|---|---|
Java | Java | SDK lub SystemApi (stabilna*) |
Wszystko |
NDK | C++ | libbinder_ndk (stabilna*) |
aidl_interface |
CPP | C++ | libbinder (niestabilny) |
Wszystko |
Rust | Rust | libbinder_rs (stabilna*) |
aidl_interface |
- Te interfejsy API są stabilne, ale wiele z nich, np. interfejsy API do zarządzania usługami, jest zarezerwowanych do użytku wewnętrznego na platformie i nie są dostępne dla aplikacji. Więcej informacji o używaniu AIDL w aplikacjach znajdziesz w artykule Android Interface Definition Language (AIDL).
- Backend w Rust został wprowadzony w Androidzie 12, a backend NDK jest dostępny od Androida 10.
- Pakiet Rust jest oparty na
libbinder_ndk
, dzięki czemu jest stabilny i przenośny. Moduły APEX korzystają z kontenera binder w standardowy sposób po stronie systemu. Część napisana w Rust jest dołączana do pakietu APEX i w nim dostarczana. Ta część zależy odlibbinder_ndk.so
na partycji systemowej.
Systemy kompilacji
W zależności od backendu kod AIDL można skompilować do kodu stub na 2 sposoby. Więcej informacji o systemach kompilacji znajdziesz w dokumentacji modułów Soong.
Podstawowy system kompilacji
W dowolnym pliku cc_
lub java_
Android.bp module
(lub w ich odpowiednikach Android.mk
) możesz określić pliki AIDL (.aidl
) jako pliki źródłowe. W tym przypadku używane są backendy AIDL w Javie lub CPP (nie backend NDK), a klasy do używania odpowiednich plików AIDL są automatycznie dodawane do modułu. W tych modułach możesz określić opcje, takie jak local_include_dirs
(która informuje system kompilacji o ścieżce głównej do plików AIDL w tym module), w grupie aidl:
.
Backend Rust jest przeznaczony tylko do używania z językiem Rust. Moduły rust_
są obsługiwane inaczej, ponieważ pliki AIDL nie są określane jako pliki źródłowe. Zamiast tego moduł aidl_interface
generuje rustlib
o nazwie aidl_interface_name-rust
, z którym można utworzyć połączenie. Więcej informacji znajdziesz w przykładzie Rust AIDL.
platforms/$PLATFORM_NAME/optional/libbinder_ndk_cpp/
.
aidl_interface
Typy używane w systemie kompilacji aidl_interface
muszą być strukturalne. Aby można było je strukturyzować, obiekty Parcelable muszą zawierać pola bezpośrednio, a nie deklaracje typów zdefiniowanych bezpośrednio w językach docelowych. Informacje o tym, jak uporządkowany AIDL pasuje do stabilnego AIDL, znajdziesz w sekcji Uporządkowany a stabilny AIDL.
Typy
Jako implementację referencyjną typów możesz traktować kompilator aidl
.
Gdy utworzysz interfejs, wywołaj aidl --lang=<backend> ...
, aby wyświetlić wynikowy plik interfejsu. Gdy używasz modułu aidl_interface
, możesz wyświetlić dane wyjściowe w out/soong/.intermediates/<path to module>/
.
Typ Java lub AIDL | Typ C++ | Typ NDK | Rodzaj rdzy |
---|---|---|---|
boolean |
bool |
bool |
bool |
byte 8 |
int8_t |
int8_t |
i8 |
char |
char16_t |
char16_t |
u16 |
int |
int32_t |
int32_t |
i32 |
long |
int64_t |
int64_t |
i64 |
float |
float |
float |
f32 |
double |
double |
double |
f64 |
String |
android::String16 |
std::string |
Wejście: &str Wyjście: String |
android.os.Parcelable |
android::Parcelable |
Nie dotyczy | Nie dotyczy |
IBinder |
android::IBinder |
ndk::SpAIBinder |
binder::SpIBinder |
T[] |
std::vector<T> |
std::vector<T> |
Wejście: &[T] Wyjście: Vec<T> |
byte[] |
std::vector |
std::vector 1 |
Wejście: &[u8] Wyjście: Vec<u8> |
List<T> |
std::vector<T> 2 |
std::vector<T> 3 |
W: In: &[T] 4Wyjście: Vec<T> |
FileDescriptor |
android::base::unique_fd |
Nie dotyczy | Nie dotyczy |
ParcelFileDescriptor |
android::os::ParcelFileDescriptor |
ndk::ScopedFileDescriptor |
binder::parcel::ParcelFileDescriptor |
Typ interfejsu (T ) |
android::sp<T> |
std::shared_ptr<T> 7 |
binder::Strong |
Typ obiektu Parcelable (T ) |
T |
T |
T |
Typ unii (T )5 |
T |
T |
T |
T[N] 6 |
std::array<T, N> |
std::array<T, N> |
[T; N] |
1. W Androidzie 12 lub nowszym tablice bajtów używają znaku uint8_t
zamiast int8_t
ze względu na zgodność.
2. Backend C++ obsługuje List<T>
, gdzie T
to jeden z tych typów: String
, IBinder
, ParcelFileDescriptor
lub parcelable. Na Androidzie 13 lub nowszym T
może być dowolnym typem niepierwotnym (w tym typem interfejsu) z wyjątkiem tablic. AOSP zaleca używanie typów tablicowych, takich jak T[]
, ponieważ działają one we wszystkich backendach.
3. Backend NDK obsługuje List<T>
, gdzie T
to jeden z tych typów: String
, ParcelFileDescriptor
lub parcelable. W Androidzie 13 lub nowszym T
może być dowolnym typem niepierwotnym z wyjątkiem tablic.
4. Typy są przekazywane w kodzie Rust w różny sposób w zależności od tego, czy są danymi wejściowymi (argumentem) czy wyjściowymi (wartością zwracaną).
5. Typy unii są obsługiwane na urządzeniach z Androidem 12 i nowszym.
6. W Androidzie 13 lub nowszym obsługiwane są tablice o stałym rozmiarze. Tablice o stałym rozmiarze mogą mieć wiele wymiarów (np. int[3][4]
). W backendzie Javy tablice o stałym rozmiarze są reprezentowane jako typy tablicowe.
7. Aby utworzyć instancję obiektu SharedRefBase
, użyj tego kodu:SharedRefBase::make\<My\>(... args ...)
Ta funkcja tworzy obiekt std::shared_ptr\<T\>
, który jest też zarządzany wewnętrznie, jeśli binder należy do innego procesu. Utworzenie obiektu w inny sposób spowoduje podwójne prawo własności.
8. Zobacz też typ Java lub AIDL byte[]
.
Kierunek (wejściowy, wyjściowy i wejściowo-wyjściowy)
Podczas określania typów argumentów funkcji możesz podać je jako in
, out
lub inout
. Określa kierunek przekazywania informacji w przypadku wywołania IPC.
Specyfikator argumentu
in
wskazuje, że dane są przekazywane z funkcji wywołującej do funkcji wywoływanej. Specyfikatorin
jest domyślnym kierunkiem, ale jeśli typy danych mogą być teżout
, musisz określić kierunek.Specyfikator argumentu
out
oznacza, że dane są przekazywane z funkcji wywoływanej do funkcji wywołującej.Specyfikator argumentu
inout
to połączenie obu tych elementów. Zalecamy jednak unikanie specyfikatora argumentuinout
. Jeśli używaszinout
z interfejsem z wersją i starszym wywoływanym interfejsem, dodatkowe pola, które występują tylko w wywołującym interfejsie, zostaną zresetowane do wartości domyślnych. W przypadku języka Rust normalny typinout
otrzymuje&mut T
, a lista typuinout
otrzymuje&mut Vec<T>
.
interface IRepeatExamples {
MyParcelable RepeatParcelable(MyParcelable token); // implicitly 'in'
MyParcelable RepeatParcelableWithIn(in MyParcelable token);
void RepeatParcelableWithInAndOut(in MyParcelable param, out MyParcelable result);
void RepeatParcelableWithInOut(inout MyParcelable param);
}
UTF-8 i UTF-16
W przypadku backendu CPP możesz wybrać, czy ciągi znaków mają być w formacie UTF-8 czy UTF-16.
Deklaruj ciągi znaków jako @utf8InCpp String
w AIDL, aby automatycznie przekonwertować je na UTF-8. Backendy NDK i Rust zawsze używają ciągów znaków UTF-8. Więcej informacji o adnotacji utf8InCpp
znajdziesz w sekcji utf8InCpp.
Dopuszczalność wartości null
Typy, które mogą mieć wartość null, możesz oznaczyć symbolem @nullable
.
Więcej informacji o adnotacji nullable
znajdziesz w sekcji nullable.
Niestandardowe obiekty Parcelable
Niestandardowy obiekt Parcelable to obiekt Parcelable, który jest implementowany ręcznie w docelowym backendzie. Używaj niestandardowych obiektów Parcelable tylko wtedy, gdy chcesz dodać obsługę innych języków do istniejącego niestandardowego obiektu Parcelable, którego nie można zmienić.
Oto przykład deklaracji typu parcelable w AIDL:
package my.pack.age;
parcelable Foo;
Domyślnie deklaruje to obiekt Java Parcelable, w którym my.pack.age.Foo
jest klasą Java implementującą interfejs Parcelable
.
Aby zadeklarować niestandardowy pakiet backendu CPP z możliwością przekształcenia w AIDL, użyj cpp_header
:
package my.pack.age;
parcelable Foo cpp_header "my/pack/age/Foo.h";
Implementacja w C++ w my/pack/age/Foo.h
wygląda tak:
#include <binder/Parcelable.h>
class MyCustomParcelable : public android::Parcelable {
public:
status_t writeToParcel(Parcel* parcel) const override;
status_t readFromParcel(const Parcel* parcel) override;
std::string toString() const;
friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
};
Aby zadeklarować niestandardowy obiekt NDK parcelable w AIDL, użyj ndk_header
:
package my.pack.age;
parcelable Foo ndk_header "android/pack/age/Foo.h";
Implementacja NDK w android/pack/age/Foo.h
wygląda tak:
#include <android/binder_parcel.h>
class MyCustomParcelable {
public:
binder_status_t writeToParcel(AParcel* _Nonnull parcel) const;
binder_status_t readFromParcel(const AParcel* _Nonnull parcel);
std::string toString() const;
friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
};
W Androidzie 15 w przypadku deklaracji niestandardowego obiektu parcelable w języku Rust w AIDL użyj rust_type
:
package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";
Implementacja w Rust w rust_crate/src/lib.rs
wygląda tak:
use binder::{
binder_impl::{BorrowedParcel, UnstructuredParcelable},
impl_deserialize_for_unstructured_parcelable, impl_serialize_for_unstructured_parcelable,
StatusCode,
};
#[derive(Clone, Debug, Eq, PartialEq)]
struct Foo {
pub bar: String,
}
impl UnstructuredParcelable for Foo {
fn write_to_parcel(&self, parcel: &mut BorrowedParcel) -> Result<(), StatusCode> {
parcel.write(&self.bar)?;
Ok(())
}
fn from_parcel(parcel: &BorrowedParcel) -> Result<Self, StatusCode> {
let bar = parcel.read()?;
Ok(Self { bar })
}
}
impl_deserialize_for_unstructured_parcelable!(Foo);
impl_serialize_for_unstructured_parcelable!(Foo);
Następnie możesz użyć tego obiektu Parcelable jako typu w plikach AIDL, ale nie będzie on generowany przez AIDL. Udostępnij operatory <
i ==
dla niestandardowych obiektów parcelable w CPP i NDK, aby można było ich używać w union
.
Wartości domyślne
Uporządkowane obiekty Parcelable mogą deklarować domyślne wartości poszczególnych pól dla typów prostych,String
pól i tablic tych typów.
parcelable Foo {
int numField = 42;
String stringField = "string value";
char charValue = 'a';
...
}
W przypadku braku wartości domyślnych w backendzie Java pola są inicjowane jako zera w przypadku typów prostych i null
w przypadku typów złożonych.
W innych backendach pola są inicjowane domyślnymi wartościami, gdy nie są zdefiniowane wartości domyślne. Na przykład w backendzie C++ pola String
są inicjowane jako pusty ciąg znaków, a pola List<T>
są inicjowane jako pusta vector<T>
. Pola @nullable
są inicjowane jako pola o wartości null.
Związki zawodowe
Unie AIDL są otagowane, a ich funkcje są podobne we wszystkich backendach. Są one tworzone na podstawie domyślnej wartości pierwszego pola i mają specyficzny dla danego języka sposób interakcji:
union Foo {
int intField;
long longField;
String stringField;
MyParcelable parcelableField;
...
}
Przykład w Javie
Foo u = Foo.intField(42); // construct
if (u.getTag() == Foo.intField) { // tag query
// use u.getIntField() // getter
}
u.setStringField("abc"); // setter
Przykład w C++ i NDK
Foo u; // default constructor
assert (u.getTag() == Foo::intField); // tag query
assert (u.get<Foo::intField>() == 0); // getter
u.set<Foo::stringField>("abc"); // setter
assert (u == Foo::make<Foo::stringField>("abc")); // make<tag>(value)
Przykład w języku Rust
W języku Rust unie są implementowane jako wyliczenia i nie mają jawnych funkcji pobierających ani ustawiających.
let mut u = Foo::Default(); // default constructor
match u { // tag match + get
Foo::IntField(x) => assert!(x == 0);
Foo::LongField(x) => panic!("Default constructed to first field");
Foo::StringField(x) => panic!("Default constructed to first field");
Foo::ParcelableField(x) => panic!("Default constructed to first field");
...
}
u = Foo::StringField("abc".to_string()); // set
Obsługa błędów
System operacyjny Android udostępnia wbudowane typy błędów, których usługi mogą używać podczas zgłaszania błędów. Są one używane przez bindery i mogą być używane przez wszystkie usługi implementujące interfejs bindera. Ich użycie jest dobrze udokumentowane w definicji AIDL i nie wymagają one żadnego zdefiniowanego przez użytkownika stanu ani typu zwracanego.
Parametry wyjściowe z błędami
Gdy funkcja AIDL zgłosi błąd, może nie zainicjować lub zmodyfikować parametrów wyjściowych. W szczególności parametry wyjściowe mogą zostać zmodyfikowane, jeśli błąd wystąpi podczas rozpakowywania, a nie podczas przetwarzania samej transakcji. Ogólnie rzecz biorąc, gdy funkcja AIDL zwraca błąd, wszystkie parametry inout
i out
, a także wartość zwracana (która w niektórych backendach działa jak parametr out
) powinny być traktowane jako znajdujące się w nieokreślonym stanie.
Jakich wartości błędów używać
Wiele wbudowanych wartości błędów można stosować w dowolnych interfejsach AIDL, ale niektóre z nich są traktowane w specjalny sposób. Na przykład symbole EX_UNSUPPORTED_OPERATION
i EX_ILLEGAL_ARGUMENT
można stosować, gdy opisują stan błędu, ale nie wolno używać symbolu EX_TRANSACTION_FAILED
, ponieważ jest on traktowany w specjalny sposób przez infrastrukturę bazową. Więcej informacji o tych wbudowanych wartościach znajdziesz w definicjach backendu.
Jeśli interfejs AIDL wymaga dodatkowych wartości błędów, które nie są objęte wbudowanymi typami błędów, może użyć specjalnego wbudowanego błędu specyficznego dla usługi, który umożliwia uwzględnienie wartości błędu specyficznego dla usługi zdefiniowanej przez użytkownika. Te błędy specyficzne dla usługi są zwykle definiowane w interfejsie AIDL jako const int
lub int
-backed enum
i nie są analizowane przez binder.
W Javie błędy są mapowane na wyjątki, np. android.os.RemoteException
. W przypadku wyjątków specyficznych dla usługi Java używa android.os.ServiceSpecificException
wraz z błędem zdefiniowanym przez użytkownika.
Kod natywny w Androidzie nie używa wyjątków. Backend CPP używa
android::binder::Status
. Backend NDK używa ndk::ScopedAStatus
. Każda metoda wygenerowana przez AIDL zwraca jedną z tych wartości, która reprezentuje stan metody. Backend w Rust używa tych samych wartości kodów wyjątków co NDK, ale przed przekazaniem ich użytkownikowi przekształca je w natywne błędy Rusta (StatusCode
, ExceptionCode
). W przypadku błędów specyficznych dla usługi zwracany kod Status
lub ScopedAStatus
używa kodu EX_SERVICE_SPECIFIC
wraz z błędem zdefiniowanym przez użytkownika.
Wbudowane typy błędów znajdziesz w tych plikach:
Backend | Definicja |
---|---|
Java | android/os/Parcel.java |
CPP | binder/Status.h |
NDK | android/binder_status.h |
Rust | android/binder_status.h |
Używanie różnych backendów
Te instrukcje dotyczą kodu platformy Android. W tych przykładach użyto zdefiniowanego typu my.package.IFoo
. Instrukcje korzystania z backendu Rust znajdziesz w przykładzie Rust AIDL w wzorcach Androida Rust.
Typy importu
Niezależnie od tego, czy zdefiniowany typ jest interfejsem, obiektem z możliwością przekazywania, czy unią, możesz go zaimportować w języku Java:
import my.package.IFoo;
Lub w backendzie platformy CPP:
#include <my/package/IFoo.h>
lub w backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl
):
#include <aidl/my/package/IFoo.h>
Lub w backendzie Rust:
use my_package::aidl::my::package::IFoo;
Chociaż w Java możesz importować typ zagnieżdżony, w przypadku backendów CPP i NDK musisz uwzględnić nagłówek dla jego typu głównego. Jeśli na przykład importujesz zagnieżdżony typ Bar
zdefiniowany w my/package/IFoo.aidl
(IFoo
to typ główny pliku), musisz uwzględnić <my/package/IFoo.h>
w przypadku backendu CPP (lub <aidl/my/package/IFoo.h>
w przypadku backendu NDK).
Implementowanie interfejsu
Aby zaimplementować interfejs, musisz dziedziczyć po natywnej klasie stub. Implementacja interfejsu jest często nazywana usługą, gdy jest zarejestrowana w menedżerze usług lub android.app.ActivityManager
, a wywołaniem zwrotnym, gdy jest zarejestrowana przez klienta usługi. Jednak w zależności od konkretnego zastosowania do opisywania implementacji interfejsu używa się różnych nazw. Klasa pośrednicząca odczytuje polecenia z sterownika bindera i wykonuje zaimplementowane przez Ciebie metody. Załóżmy, że masz taki plik AIDL:
package my.package;
interface IFoo {
int doFoo();
}
W języku Java musisz rozszerzyć wygenerowaną klasę Stub
:
import my.package.IFoo;
public class MyFoo extends IFoo.Stub {
@Override
int doFoo() { ... }
}
W backendzie CPP:
#include <my/package/BnFoo.h>
class MyFoo : public my::package::BnFoo {
android::binder::Status doFoo(int32_t* out) override;
}
W backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl
):
#include <aidl/my/package/BnFoo.h>
class MyFoo : public aidl::my::package::BnFoo {
ndk::ScopedAStatus doFoo(int32_t* out) override;
}
W backendzie Rust:
use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFoo};
use binder;
/// This struct is defined to implement IRemoteService AIDL interface.
pub struct MyFoo;
impl Interface for MyFoo {}
impl IFoo for MyFoo {
fn doFoo(&self) -> binder::Result<()> {
...
Ok(())
}
}
lub w przypadku asynchronicznego kodu w Rust:
use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFooAsyncServer};
use binder;
/// This struct is defined to implement IRemoteService AIDL interface.
pub struct MyFoo;
impl Interface for MyFoo {}
#[async_trait]
impl IFooAsyncServer for MyFoo {
async fn doFoo(&self) -> binder::Result<()> {
...
Ok(())
}
}
Rejestracja i korzystanie z usług
Usługi na platformie Android są zwykle rejestrowane w procesie servicemanager
. Oprócz tych interfejsów API niektóre interfejsy API sprawdzają usługę (co oznacza, że jeśli usługa jest niedostępna, natychmiast zwracają wynik).
Szczegółowe informacje znajdziesz w odpowiednim interfejsie servicemanager
. Te operacje możesz wykonywać tylko podczas kompilowania na platformę Android.
W Javie:
import android.os.ServiceManager;
// registering
ServiceManager.addService("service-name", myService);
// return if service is started now
myService = IFoo.Stub.asInterface(ServiceManager.checkService("service-name"));
// waiting until service comes up (new in Android 11)
myService = IFoo.Stub.asInterface(ServiceManager.waitForService("service-name"));
// waiting for declared (VINTF) service to come up (new in Android 11)
myService = IFoo.Stub.asInterface(ServiceManager.waitForDeclaredService("service-name"));
W backendzie CPP:
#include <binder/IServiceManager.h>
// registering
defaultServiceManager()->addService(String16("service-name"), myService);
// return if service is started now
status_t err = checkService<IFoo>(String16("service-name"), &myService);
// waiting until service comes up (new in Android 11)
myService = waitForService<IFoo>(String16("service-name"));
// waiting for declared (VINTF) service to come up (new in Android 11)
myService = waitForDeclaredService<IFoo>(String16("service-name"));
W backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl
):
#include <android/binder_manager.h>
// registering
binder_exception_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
// return if service is started now
myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_checkService("service-name")));
// is a service declared in the VINTF manifest
// VINTF services have the type in the interface instance name.
bool isDeclared = AServiceManager_isDeclared("android.hardware.light.ILights/default");
// wait until a service is available (if isDeclared or you know it's available)
myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_waitForService("service-name")));
W backendzie Rust:
use myfoo::MyFoo;
use binder;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;
fn main() {
binder::ProcessState::start_thread_pool();
// [...]
let my_service = MyFoo;
let my_service_binder = BnFoo::new_binder(
my_service,
BinderFeatures::default(),
);
binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
// Does not return - spawn or perform any work you mean to do before this call.
binder::ProcessState::join_thread_pool()
}
W asynchronicznym backendzie Rust z jednowątkowym środowiskiem wykonawczym:
use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;
#[tokio::main(flavor = "current_thread")]
async fn main() {
binder::ProcessState::start_thread_pool();
// [...]
let my_service = MyFoo;
let my_service_binder = BnFoo::new_async_binder(
my_service,
TokioRuntime(Handle::current()),
BinderFeatures::default(),
);
binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
// Sleeps forever, but does not join the binder threadpool.
// Spawned tasks run on this thread.
std::future::pending().await
}
Ważną różnicą w porównaniu z innymi opcjami jest to, że w przypadku asynchronicznego języka Rust i środowiska wykonawczego z jednym wątkiem nie wywołujesz funkcji
join_thread_pool
. Dzieje się tak, ponieważ musisz przekazać Tokio wątek, w którym może wykonywać utworzone zadania. W poniższym przykładzie główny wątek służy do tego celu. Wszystkie zadania utworzone za pomocą funkcji tokio::spawn
są wykonywane w wątku głównym.
W asynchronicznym backendzie Rusta z wielowątkowym środowiskiem wykonawczym:
use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
binder::ProcessState::start_thread_pool();
// [...]
let my_service = MyFoo;
let my_service_binder = BnFoo::new_async_binder(
my_service,
TokioRuntime(Handle::current()),
BinderFeatures::default(),
);
binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
// Sleep forever.
tokio::task::block_in_place(|| {
binder::ProcessState::join_thread_pool();
});
}
W wielowątkowym środowisku wykonawczym Tokio utworzone zadania nie są wykonywane w wątku głównym. Dlatego lepiej jest wywołać join_thread_pool
w wątku głównym, aby nie był on bezczynny. Aby opuścić kontekst asynchroniczny, musisz zamknąć wywołanie w tagach
block_in_place
.
Link do śmierci
Możesz poprosić o powiadomienie, gdy usługa hostująca binder przestanie działać. Może to pomóc w uniknięciu wycieku serwerów proxy wywołania zwrotnego lub w przywróceniu działania po błędzie. Wywołuj te wywołania na obiektach proxy bindera.
- W języku Java użyj
android.os.IBinder::linkToDeath
. - W backendzie CPP użyj
android::IBinder::linkToDeath
. - W backendzie NDK użyj
AIBinder_linkToDeath
. - W backendzie Rust utwórz obiekt
DeathRecipient
, a następnie wywołajmy_binder.link_to_death(&mut my_death_recipient)
. Pamiętaj, że ponieważ wywołanie zwrotne jest własnością obiektuDeathRecipient
, musisz utrzymywać ten obiekt przy życiu tak długo, jak chcesz otrzymywać powiadomienia.
Informacje o rozmówcy
Podczas odbierania wywołania bindera jądra informacje o wywołującym są dostępne w kilku interfejsach API. Identyfikator procesu (PID) to identyfikator procesu Linux, który wysyła transakcję. Identyfikator użytkownika (UI) to identyfikator użytkownika systemu Linux. W przypadku połączenia jednokierunkowego identyfikator PID połączenia to 0. Poza kontekstem transakcji Binder te funkcje zwracają identyfikator PID i UID bieżącego procesu.
W backendzie Java:
... = Binder.getCallingPid();
... = Binder.getCallingUid();
W backendzie CPP:
... = IPCThreadState::self()->getCallingPid();
... = IPCThreadState::self()->getCallingUid();
W backendzie NDK:
... = AIBinder_getCallingPid();
... = AIBinder_getCallingUid();
W przypadku implementowania interfejsu w backendzie Rust określ te wartości (zamiast zezwalać na ich domyślne ustawienie):
... = ThreadState::get_calling_pid();
... = ThreadState::get_calling_uid();
Raporty o błędach i interfejs API do debugowania usług
Gdy raporty o błędach są uruchamiane (np. za pomocą polecenia adb bugreport
), zbierają informacje z całego systemu, aby ułatwić debugowanie różnych problemów.
W przypadku usług AIDL raporty o błędach używają pliku binarnego dumpsys
we wszystkich usługach zarejestrowanych w menedżerze usług, aby zrzucać informacje do raportu o błędach. Możesz też użyć dumpsys
w wierszu poleceń, aby uzyskać informacje z usługi za pomocą dumpsys SERVICE [ARGS]
. W przypadku backendów C++ i Java możesz kontrolować kolejność zrzucania usług, używając dodatkowych argumentów w funkcji addService
. Możesz też użyć dumpsys --pid SERVICE
, aby podczas debugowania uzyskać identyfikator PID usługi.
Aby dodać niestandardowe dane wyjściowe do usługi, zastąp metodę dump
w obiekcie serwera, tak jak w przypadku implementowania dowolnej innej metody IPC zdefiniowanej w pliku AIDL. W tym celu ogranicz zrzut do uprawnień aplikacjiandroid.permission.DUMP
lub do określonych identyfikatorów UID.
W backendzie Java:
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
@Nullable String[] args) {...}
W backendzie CPP:
status_t dump(int, const android::android::Vector<android::String16>&) override;
W backendzie NDK:
binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;
W przypadku implementowania interfejsu w backendzie Rust określ te wartości (zamiast zezwalać na ich domyślne ustawienie):
fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>
Używanie słabych wskaźników
Możesz przechowywać słabe odwołanie do obiektu bindera.
Java obsługuje WeakReference
, ale nie obsługuje słabych odwołań do powiązań w warstwie natywnej.
W backendzie CPP słaby typ to wp<IFoo>
.
W backendzie NDK użyj ScopedAIBinder_Weak
:
#include <android/binder_auto_utils.h>
AIBinder* binder = ...;
ScopedAIBinder_Weak myWeakReference = ScopedAIBinder_Weak(AIBinder_Weak_new(binder));
W backendzie Rust użyj WpIBinder
lub Weak<IFoo>
:
let weak_interface = myIface.downgrade();
let weak_binder = myIface.as_binder().downgrade();
Dynamiczne pobieranie deskryptora interfejsu
Deskryptor interfejsu określa typ interfejsu. Jest to przydatne podczas debugowania lub gdy masz nieznany binder.
W języku Java możesz uzyskać deskryptor interfejsu za pomocą kodu takiego jak ten:
service = /* get ahold of service object */
... = service.asBinder().getInterfaceDescriptor();
W backendzie CPP:
service = /* get ahold of service object */
... = IInterface::asBinder(service)->getInterfaceDescriptor();
Środowiska NDK i Rust nie obsługują tej funkcji.
Statyczne pobieranie deskryptora interfejsu
Czasami (np. podczas rejestrowania usług @VintfStability
) musisz znać statyczny deskryptor interfejsu. W Javie możesz uzyskać deskryptor, dodając kod taki jak ten:
import my.package.IFoo;
... IFoo.DESCRIPTOR
W backendzie CPP:
#include <my/package/BnFoo.h>
... my::package::BnFoo::descriptor
W backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl
):
#include <aidl/my/package/BnFoo.h>
... aidl::my::package::BnFoo::descriptor
W backendzie Rust:
aidl::my::package::BnFoo::get_descriptor()
Zakres wyliczenia
W natywnych backendach możesz iterować po możliwych wartościach, jakie może przyjmować wyliczenie. Ze względu na rozmiar kodu nie jest to obsługiwane w przypadku Javy.
W przypadku wyliczenia MyEnum
zdefiniowanego w AIDL iteracja jest zapewniana w ten sposób:
W backendzie CPP:
::android::enum_range<MyEnum>()
W backendzie NDK:
::ndk::enum_range<MyEnum>()
W backendzie Rust:
MyEnum::enum_values()
Zarządzanie wątkami
Każda instancja libbinder
w procesie utrzymuje jedną pulę wątków. W większości przypadków użycia powinien to być dokładnie 1 pula wątków współdzielona przez wszystkie interfejsy backendu.
Jedynym wyjątkiem jest sytuacja, w której kod dostawcy wczytuje inną kopię libbinder
, aby komunikować się z /dev/vndbinder
. Dzieje się to w osobnym węźle bindera, więc pula wątków nie jest udostępniana.
W przypadku backendu Java rozmiar puli wątków może się tylko zwiększać (ponieważ jest już uruchomiona):
BinderInternal.setMaxThreads(<new larger value>);
W przypadku backendu CPP dostępne są te operacje:
// set max threadpool count (default is 15)
status_t err = ProcessState::self()->setThreadPoolMaxThreadCount(numThreads);
// create threadpool
ProcessState::self()->startThreadPool();
// add current thread to threadpool (adds thread to max thread count)
IPCThreadState::self()->joinThreadPool();
Podobnie w przypadku backendu NDK:
bool success = ABinderProcess_setThreadPoolMaxThreadCount(numThreads);
ABinderProcess_startThreadPool();
ABinderProcess_joinThreadPool();
W backendzie Rust:
binder::ProcessState::start_thread_pool();
binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
binder::ProcessState::join_thread_pool();
W przypadku asynchronicznego backendu Rust potrzebujesz 2 puli wątków: binder i Tokio.
Oznacza to, że aplikacje korzystające z asynchronicznego języka Rust wymagają szczególnej uwagi, zwłaszcza jeśli chodzi o użycie join_thread_pool
. Więcej informacji znajdziesz w sekcji dotyczącej rejestrowania usług.
Zarezerwowane nazwy
W językach C++, Java i Rust niektóre nazwy są zarezerwowane jako słowa kluczowe lub do użytku w ramach danego języka. AIDL nie wymusza ograniczeń na podstawie reguł językowych, ale używanie nazw pól lub typów, które pasują do nazwy zastrzeżonej, może spowodować błąd kompilacji w przypadku języka C++ lub Java. W przypadku języka Rust nazwa pola lub typu jest zmieniana przy użyciu składni surowego identyfikatora, do której można uzyskać dostęp za pomocą prefiksu r#
.
Zalecamy unikanie w definicjach AIDL nazw zastrzeżonych, aby zapobiec nieergonomicznym powiązaniom lub całkowitemu niepowodzeniu kompilacji.
Jeśli w definicjach AIDL masz już zarezerwowane nazwy, możesz bezpiecznie zmienić nazwy pól, zachowując zgodność protokołu. Aby kontynuować tworzenie, może być konieczne zaktualizowanie kodu, ale wszystkie utworzone już programy będą nadal ze sobą współpracować.
Nazwy, których należy unikać: