Backend AIDL jest celem generowania kodu zastępczego. Zawsze używaj plików AIDL w określonym języku z określonym środowiskiem uruchomieniowym. W zależności od kontekstu należy używać różnych mechanizmów AIDL.
W tabeli poniżej stabilność interfejsu API odnosi się do możliwości kompilowania kodu w sposób, który umożliwia jego dostarczenie niezależnie od binarnego pliku system.img
libbinder.so
.
AIDL ma te backendy:
Backend | Język | Interfejs API | Systemy kompilacji |
---|---|---|---|
Java | Java | Pakiet SDK lub SystemApi (stabilna wersja*) |
Wszystko |
NDK | C++ | libbinder_ndk (stabilna*) |
aidl_interface |
CPP | C++ | libbinder (niestabilna) |
Wszystko |
Rust | Rust | libbinder_rs (stabilna*) |
aidl_interface |
- Te interfejsy API są stabilne, ale wiele z nich, np. interfejsy do zarządzania usługami, jest zarezerwowanych do użytku wewnętrznego platformy i nie są dostępne dla aplikacji. Więcej informacji o używaniu języka AIDL w aplikacjach znajdziesz w artykule Język definiowania interfejsu Androida (AIDL).
- Backend Rust został wprowadzony w Androidzie 12, a backend NDK jest dostępny od Androida 10.
- Pakiet Rust jest zbudowany na podstawie pakietu
libbinder_ndk
, dzięki czemu jest stabilny i przenośny. APEXe używają standardowej skrzynki z binderem po stronie systemu. Część Rust jest wstępnie skompilowana w APEX i wysyłana w ramach tego pakietu. Ta część zależy odlibbinder_ndk.so
w partycji systemu.
Systemy kompilacji
W zależności od backendu możesz skompilować AIDL w postaci kodu stuba na 2 sposoby. Więcej informacji o systemach kompilacji znajdziesz w dokumentacji modułów Soong.
Główny system kompilacji
W dowolnym pliku cc_
lub java_
Android.bp module
(lub ich odpowiednikach Android.mk
) możesz jako pliki źródłowe określić pliki AIDL (.aidl
). W tym przypadku używane są backendy AIDL w języku Java lub C++ (a nie backend NDK), a klasy korzystające z odpowiednich plików AIDL są automatycznie dodawane do modułu. W tych modułach w grupie aidl:
możesz określić opcje takie jak local_include_dirs
(które informują system kompilacji o ścieżce do katalogu głównego plików AIDL w tym module).
Backend Rust jest przeznaczony tylko do użytku z Rust. Moduły rust_
są obsługiwane inaczej, ponieważ pliki AIDL nie są określone jako pliki źródłowe. Zamiast tego moduł aidl_interface
generuje rustlib
o nazwie aidl_interface_name-rust
, z którym można utworzyć link. Więcej informacji znajdziesz w przykładzie Rust AIDL.
aidl_interface
Typy używane w systemie kompilacji aidl_interface
muszą być uporządkowane. Aby obiekty parcelable były ustrukturyzowane, muszą zawierać pola bezpośrednio, a nie deklaracje typów zdefiniowanych bezpośrednio w językach docelowych. Informacje o tym, jak uporządkowana wersja AIDL różni się od stabilnej wersji AIDL, znajdziesz w artykule Uporządkowana wersja a stabilna wersja AIDL.
Typy
Jako implementację referencyjną typów możesz użyć kompilatora aidl
.
Podczas tworzenia interfejsu wywołaj funkcję aidl --lang=<backend> ...
, aby wyświetlić wynikowy plik interfejsu. Gdy używasz modułu aidl_interface
, możesz wyświetlić dane wyjściowe w komponencie out/soong/.intermediates/<path to module>/
.
Typ Java lub AIDL | Typ C++ | Typ NDK | Typ 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: 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 paczki (T ) |
T |
T |
T |
Typ zjednoczenia (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ą funkcji uint8_t
zamiast int8_t
ze względu na zgodność.
2. Backend C++ obsługuje List<T>
, gdzie T
jest jedną z tych wartości:
String
, IBinder
, ParcelFileDescriptor
lub parcelable. W Androidzie 13 lub nowszym T
może być dowolnym typem niepierwotnym (w tym typami interfejsu), z wyjątkiem tablic. AOSP zaleca używanie typów tablic takich jak T[]
, ponieważ działają one na wszystkich backendach.
3. Backend NDK obsługuje List<T>
, gdzie T
to jeden z wartości 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 różny sposób w zależności od tego, czy są danymi wejściowymi (argumentem), czy wyjściowymi (zwracaną wartością).
5. Typy zjednoczenia są obsługiwane w Androidzie 12 i nowszych.
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 Java tablice o stałym rozmiarze są reprezentowane jako typy tablic.
7. Aby utworzyć instancję obiektu binder SharedRefBase
, użyj instrukcji SharedRefBase::make\<My\>(... args ...)
. Ta funkcja tworzy obiekt std::shared_ptr\<T\>
, którym również zarządza się wewnętrznie, jeśli binder należy do innego procesu. Tworzenie obiektu w inny sposób powoduje podwójne posiadanie.
8. Zobacz też typ Java lub AIDL byte[]
.
Kierunkowość (w głąb, na zewnątrz i w głąb i na zewnątrz)
Podczas określania typów argumentów funkcji możesz użyć wartości in
, out
lub inout
. Określa kierunek przekazywania informacji w wywołaniu IPC.
Wskaźnik argumentu
in
wskazuje, że dane są przekazywane od wywołującego do wywoływanego. Wskaźnikin
to domyślny kierunek, ale jeśli typy danych mogą być teżout
, musisz określić kierunek.Argument
out
oznacza, że dane są przekazywane od wywoływanej funkcji do wywołującej.Parametr
inout
to kombinacja obu tych elementów. Zalecamy jednak unikanie użycia parametruinout
. Jeśli używasz interfejsuinout
z wersją i starszym wywoływanym elementem, dodatkowe pola, które są obecne tylko w wywoływanym elemencie, zostaną zresetowane do wartości domyślnych. W przypadku Rust normalny typinout
otrzymuje&mut T
, a typ listyinout
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 backendzie CPP możesz wybrać, czy ciągi mają być w formacie UTF-8 czy UTF-16.
Aby automatycznie konwertować je na UTF-8, w pliku AIDL deklaruj ciągi znaków jako @utf8InCpp String
. Backendy NDK i Rust zawsze używają ciągów znaków UTF-8. Więcej informacji o annotacjach utf8InCpp
znajdziesz w artykule utf8InCpp.
Dopuszczalność wartości null
Typy, które mogą być puste, możesz oznaczyć za pomocą adnotacji @nullable
.
Więcej informacji o adnotacji nullable
znajdziesz w sekcji wartości dozwolone.
Obiekty do pakowania niestandardowe
Niestandardowy obiekt do zapakowania to obiekt do zapakowania, 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 dla istniejącego niestandardowego obiektu Parcelable, którego nie można zmienić.
Oto przykład deklaracji parcelable w AIDL:
package my.pack.age;
parcelable Foo;
Domyślnie deklaruje to obiekt Java parcelable, gdzie my.pack.age.Foo
jest klasą Java implementującą interfejs Parcelable
.
Aby zadeklarować niestandardowy backend w C++ w formacie parcelable w pliku 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 Parcelable w NDK w pliku AIDL, użyj elementu 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 do deklarowania niestandardowego obiektu Rust w AIDL należy użyć:rust_type
package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";
Implementacja 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 typu w plikach AIDL, ale nie będzie on generowany przez AIDL. Podaj operatory <
i ==
dla niestandardowych obiektów CPP i NDK, aby używać ich w union
.
Wartości domyślne
Struktury parcelables mogą deklarować wartości domyślne dla poszczególnych pól dla typów prymitywnych, pól String
i tablic tych typów.
parcelable Foo {
int numField = 42;
String stringField = "string value";
char charValue = 'a';
...
}
W backendzie w języku Java, gdy brakuje wartości domyślnych, pola są inicjowane jako wartości zerowe w przypadku typów prymitywnych i null
w przypadku typów nieprzywodowych.
W innych backendach pola są inicjowane za pomocą wartości domyślnych, gdy wartości domyślne nie są zdefiniowane. Na przykład w backendzie C++ pola String
są inicjowane jako puste ciągi znaków, a pola List<T>
jako puste vector<T>
. Pola @nullable
są inicjowane jako pola o wartości null.
Związki
Związki AIDL są oznaczane tagami, a ich funkcje są podobne we wszystkich backendach. Są one tworzone na podstawie wartości domyślnej pierwszego pola i mają sposób interakcji zależny od języka:
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.setSringField("abc"); // setter
Przykład 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 dotyczący Rust
W Rust zjednoczenia są implementowane jako typy wyliczeniowe i nie mają jawnych metod getter ani setter.
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ć do 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 wymaga żadnego zdefiniowanego przez użytkownika stanu ani typu zwracanych danych.
Parametry wyjściowe z błędami
Gdy funkcja AIDL zgłasza błąd, może nie inicjować ani modyfikować parametrów wyjściowych. W szczególności parametry wyjściowe mogą ulec zmianie, jeśli błąd wystąpi podczas rozpakowywania, a nie podczas przetwarzania samej transakcji. Ogólnie, 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ć uważane za nieokreślone.
Których wartości błędów używać
Wiele wbudowanych wartości błędów może być używane w dowolnych interfejsach AIDL, ale niektóre z nich są traktowane w specjalny sposób. Na przykład EX_UNSUPPORTED_OPERATION
i EX_ILLEGAL_ARGUMENT
można używać do opisu warunków błędu, ale nie można używać EX_TRANSACTION_FAILED
, ponieważ jest ono traktowane w szczególny sposób przez infrastrukturę podstawową. Więcej informacji o tych wbudowanych wartościach znajdziesz w definicjach na zapleczu.
Jeśli interfejs AIDL wymaga dodatkowych wartości błędów, które nie są objęte wbudowanymi typami błędów, można użyć specjalnego wbudowanego błędu związanego z usługą, który umożliwia uwzględnienie wartości błędu określonej przez użytkownika. Te błędy związane z konkretną usługą są zwykle definiowane w interfejsie AIDL jako const int
lub int
z obsługą enum
i nie są analizowane przez binder.
W Javie błędy są mapowane na wyjątki, takie jak android.os.RemoteException
. W przypadku wyjątków związanych z usługą Java używa wartości android.os.ServiceSpecificException
oraz błędu zdefiniowanego przez użytkownika.
Kod natywny w Androidzie nie używa wyjątków. Backend CPP używa android::binder::Status
. Backend NDK korzysta z ndk::ScopedAStatus
. Każda metoda wygenerowana przez AIDL zwraca jedną z nich, która reprezentuje stan metody. Backend Rust używa tych samych wartości kodu wyjątku co NDK, ale przed przekazaniem ich użytkownikowi konwertuje je na natywne błędy Rust (StatusCode
, ExceptionCode
). W przypadku błędów związanych z poszczególnymi usługami zwracany błąd Status
lub ScopedAStatus
używa EX_SERVICE_SPECIFIC
wraz z błędem zdefiniowanym przez użytkownika.
Wbudowane typy błędów można znaleźć 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 dotyczące korzystania z back-endu Rust znajdziesz w sekcji Rust AIDL
w przykładzie w artykule Rust na Androidzie – wzorce.
Typy importu
Niezależnie od tego, czy zdefiniowany typ jest interfejsem, klasą z możliwością dzielenia na części czy klasą zjednoczenia, możesz go zaimportować w języku Java:
import my.package.IFoo;
Lub na zapleczu CPP:
#include <my/package/IFoo.h>
Możesz też użyć backendu 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ż możesz importować typy zagnieżdżone w języku Java, w backendach CPP i NDK musisz uwzględnić nagłówek dla typu wyższego poziomu. Podczas importowania zagnieżdżonego typu Bar
zdefiniowanego w my/package/IFoo.aidl
(IFoo
to typ katalogu źródeł pliku) musisz uwzględnić typ <my/package/IFoo.h>
w przypadku backendu CPP (lub <aidl/my/package/IFoo.h>
w przypadku backendu NDK).
Implementacja interfejsu
Aby zaimplementować interfejs, musisz odziedziczyć go od natywnej klasy stub. Implementacja interfejsu jest często nazywana usługą, gdy jest zarejestrowana przez menedżera usługi lub android.app.ActivityManager
, oraz zwrotnym wywołaniem, gdy jest zarejestrowana przez klienta usługi. Jednak w zależności od sposobu użycia do opisywania implementacji interfejsu używa się różnych nazw. Klasa podrzędna odczytuje polecenia z sterownika bindera i wykonuje zaimplementowane przez Ciebie metody. Załóżmy, że masz plik AIDL o takiej treści:
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ą nazwę przestrzeni 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 Rust z wykorzystaniem wątków asynchronicznych:
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 uzyskiwanie dostępu do usług
Usługi na platformie Android są zwykle rejestrowane za pomocą procesu servicemanager
. Oprócz tych interfejsów API niektóre interfejsy API sprawdzają usługę (co oznacza, że zwracają dane natychmiast, jeśli usługa jest niedostępna).
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ą nazwę przestrzeni 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ątkową obsługą:
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
}
Jedną z ważniejszych różnic w porównaniu z innymi opcjami jest to, że nie wywołujesz funkcji join_thread_pool
, gdy używasz asynchronicznego Rusta i jednowątkowego środowiska wykonawczego. Musisz to zrobić, ponieważ musisz przekazać Tokio wątek, w którym może wykonywać uruchamiane zadania. W tym przykładzie pełni tę funkcję wątek główny. Wszystkie zadania utworzone za pomocą funkcji tokio::spawn
są wykonywane w wątku głównym.
W niesynchronizowanym backendzie Rust z wielwątkowym środowiskiem uruchomieniowym:
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 przypadku wielowątkowego środowiska wykonawczego Tokio wygenerowane zadania nie są wykonywane w wątku głównym. Dlatego lepiej jest wywołać funkcję join_thread_pool
w głównym wątku, aby nie był on bezczynny. Aby opuścić kontekst asynchroniczny, musisz zakończyć wywołanie za pomocą funkcji block_in_place
.
Link do śmierci
Możesz poprosić o powiadomienie, gdy usługa hostująca binder przestanie działać. Pomoże to uniknąć wycieku danych w przypadku wywołania zwrotnego lub pomoże w naprawieniu błędu. Wykonuj te wywołania w przypadku obiektów pośredniczących binder.
- W języku Java użyj
android.os.IBinder::linkToDeath
. - Na 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łaj funkcjęmy_binder.link_to_death(&mut my_death_recipient)
. Pamiętaj, że obiektDeathRecipient
jest właścicielem funkcji wywołania zwrotnego, dlatego musisz utrzymywać go w pamięci tak długo, jak długo chcesz otrzymywać powiadomienia.
Informacje o rozmówcy
Gdy otrzymasz wywołanie bindera jądra, informacje o rozmówcy są dostępne w kilku interfejsach API. Identyfikator procesu (PID) odnosi się do identyfikatora procesu Linuxa, który wysyła transakcję. Identyfikator użytkownika (UI) odnosi się do identyfikatora użytkownika systemu Linux. W przypadku połączenia jednokierunkowego PID dzwoniącego to 0. Poza kontekstem transakcji bindera te funkcje zwracają PID i UID bieżącego procesu.
W backendzie w Javie:
... = Binder.getCallingPid();
... = Binder.getCallingUid();
W backendzie CPP:
... = IPCThreadState::self()->getCallingPid();
... = IPCThreadState::self()->getCallingUid();
W backendzie NDK:
... = AIBinder_getCallingPid();
... = AIBinder_getCallingUid();
Podczas implementowania interfejsu w backendzie Rust określ te opcje (zamiast korzystania z wartości domyślnych):
... = ThreadState::get_calling_pid();
... = ThreadState::get_calling_uid();
Raporty o błędach i interfejs debugowania API usług
Gdy raporty o błędach są uruchamiane (np. za pomocą 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ą binarnego pliku dumpsys
we wszystkich usługach zarejestrowanych w menedżerze usług, aby zapisywać informacje w raporcie o błędach. Aby uzyskać informacje z usługi za pomocą dumpsys SERVICE [ARGS]
, możesz też użyć dumpsys
w wierszu poleceń. W przypadku zaplecza C++ i Java możesz kontrolować kolejność, w jakiej usługi są zapisywane, za pomocą dodatkowych argumentów do addService
. Podczas debugowania możesz też użyć polecenia dumpsys --pid SERVICE
, aby uzyskać PID usługi.
Aby dodać niestandardowe dane wyjściowe do usługi, zastąp metodę dump
w obiekcie serwera tak, jak implementujesz dowolną inną metodę IPC zdefiniowaną w pliku AIDL. W tym celu ogranicz dumping do uprawnień aplikacji android.permission.DUMP
lub do określonych identyfikatorów UID.
W backendzie w Javie:
@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;
Podczas implementowania interfejsu w backendzie Rust określ te opcje (zamiast korzystania z wartości domyślnych):
fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>
Używanie słabych odniesień
Możesz przechowywać słabe odwołanie do obiektu binder.
Chociaż Java obsługuje WeakReference
, nie obsługuje odwołań do słabych binderów na poziomie natywnym.
W backendzie CPP typ słaby 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 związek.
W języku Java możesz uzyskać opis interfejsu za pomocą kodu takiego jak:
service = /* get ahold of service object */
... = service.asBinder().getInterfaceDescriptor();
W backendzie CPP:
service = /* get ahold of service object */
... = IInterface::asBinder(service)->getInterfaceDescriptor();
Backendy NDK i Rust nie obsługują tej funkcji.
Statyczne pobieranie deskryptora interfejsu
Czasami (np. podczas rejestrowania usług @VintfStability
) musisz wiedzieć, jak wygląda opis interfejsu w postaci statycznej. W Javie możesz uzyskać deskryptor, dodając kod, np.
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ą nazwę przestrzeni aidl
):
#include <aidl/my/package/BnFoo.h>
... aidl::my::package::BnFoo::descriptor
W backendzie Rust:
aidl::my::package::BnFoo::get_descriptor()
Zakres typu wyliczeniowego
W natywnych backendach możesz przejść przez możliwe wartości, które może przyjąć typ wyliczeniowy. Z powodu ograniczeń rozmiaru kodu nie jest to obsługiwane w języku Java.
W przypadku typu wyliczeniowego MyEnum
zdefiniowanego w pliku AIDL iteracja jest podawana 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 jeden pulę wątków. W większości przypadków powinien to być dokładnie jeden zbiornik wątków współdzielony przez wszystkie backendy.
Jedynym wyjątkiem jest sytuacja, gdy kod dostawcy wczytuje kolejną kopię libbinder
, aby nawiązać połączenie z /dev/vndbinder
. Jest to osobny węzeł bindera, więc pula wątków nie jest współdzielona.
W przypadku backendu w Javie rozmiar puli wątków może się tylko zwiększać (ponieważ jest już uruchomiony):
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 backendzie 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 zbiorów wątków: binder i Tokio.
Oznacza to, że aplikacje korzystające z asynchronicznego Rusta wymagają szczególnej uwagi, zwłaszcza w przypadku używania join_thread_pool
. Więcej informacji znajdziesz w sekcji poświęconej rejestrowaniu usług.
Zarezerwowane nazwy
C++, Java i Rust zarezerwowały niektóre nazwy jako słowa kluczowe lub do użycia w określonym języku. Chociaż AIDL nie narzuca ograniczeń na podstawie reguł językowych, używanie nazw pól lub typów, które pasują do nazw zastrzeżonych, może spowodować niepowodzenie kompilacji w przypadku języków C++ i Java. W przypadku Rust nazwa pola lub typu jest zmieniana za pomocą składni identyfikatora surowego, dostępnej za pomocą prefiksu r#
.
Zalecamy, aby w miarę możliwości unikać używania zarezerwowanych nazw w definicjach AIDL, aby uniknąć nieergonomicznych powiązań lub całkowitego niepowodzenia kompilacji.
Jeśli masz już zarezerwowane nazwy w definicjach AIDL, możesz bezpiecznie zmienić nazwy pól, zachowując zgodność z protokołem. Aby kontynuować kompilowanie, konieczne może być zaktualizowanie kodu, ale już skompilowane programy będą nadal ze sobą współpracować.
Nazwy, których należy unikać: