Backend AIDL jest celem generowania kodu zastępczego. Podczas korzystania z plików AIDL zawsze używasz ich w określonym języku z określonym środowiskiem uruchomieniowym. W zależności od kontekstu należy używać różnych usług backendowych AIDL.
W tabeli poniżej stabilność interfejsu API odnosi się do możliwości kompilowania kodu w sposób umożliwiający dostarczanie go niezależnie od binarnego pliku system.img
libbinder.so
.
AIDL ma te backendy:
Backend | Język | Interfejs API | Systemy kompilacji |
---|---|---|---|
Java | Java | SDK/SystemApi (stabilna wersja*) | wszystkie |
NDK | C++ | libbinder_ndk (stabilna*) | aidl_interface |
CPP | C++ | libbinder (niestabilna) | wszystkie |
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 jest dostępnych dla aplikacji. Więcej informacji o używaniu AIDL w aplikacjach znajdziesz w dokumentacji dla deweloperów.
- 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. APEX korzysta z binder crate w taki sam sposób jak każda inna osoba po stronie systemu. Część Rust jest zapakowana w APEX i wysyłana w ramach tego pakietu. To zależy odlibbinder_ndk.so
na partycji systemowej.
Systemy kompilacji
W zależności od backendu możesz skompilować AIDL w stub kodu na 2 sposoby. Więcej informacji o systemach kompilacji znajdziesz w dokumentacji modułu Soong.
Główny system kompilacji
W dowolnym module cc_
lub java_
w formacie Android.bp (lub ich odpowiednikach Android.mk
) jako pliki źródłowe można określić pliki .aidl
. W tym przypadku używane są backendy Java/C++ AIDL (a nie backend NDK), a klasy do korzystania z odpowiednich plików AIDL są automatycznie dodawane do modułu. Opcje takie jak local_include_dirs
, które informują system kompilacji o ścieżce do katalogu źródeł plików AIDL w danym module, można określić w tych modułach w grupie aidl:
. Pamiętaj, że 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
, który można połączyć z innym. Więcej informacji znajdziesz w przykładowym pliku AIDL w języku Rust.
aidl_interface
Typy używane w tym systemie kompilacji muszą być ustrukturyzowane. Aby były strukturyzowane, obiekty Parcelable muszą zawierać pola bezpośrednio, a nie być deklaracją typów zdefiniowanych bezpośrednio w językach docelowych. Informacje o tym, jak uporządkowane AIDL pasują do stabilnych AIDL, znajdziesz w artykule Uporządkowane a stabilne AIDL.
Typy
Kompilator aidl
może służyć jako implementacja referencyjna typów.
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 out/soong/.intermediates/<path to module>/
.
Typ Java/AIDL | Typ C++ | Typ NDK | Typ rdzy |
---|---|---|---|
wartość logiczna | wartość logiczna | wartość logiczna | wartość logiczna |
bajt8 | int8_t | int8_t | i8 |
char | char16_t | char16_t | u16 |
int | int32_t | int32_t | i32 |
długi | int64_t | int64_t | i64 |
zmiennoprzecinkowa | zmiennoprzecinkowa | zmiennoprzecinkowa | f32 |
podwójny | podwójny | podwójny | f64 |
Ciąg znaków | android::String16 | std::string | Wejście: &str Wyjście: ciąg znaków |
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<uint8_t> | std::vector<int8_t>1 | Wejście: &[u8] Wyjście: Vec<u8> |
Lista<T> | std::vector<T>2 | std::vector<T>3 | W: &[T]4 Wyjś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 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ą typu uint8_t zamiast int8_t ze względu na zgodność.
2. Backend w C++ obsługuje List<T>
, gdzie T
to jeden z tych typów: String
, IBinder
, ParcelFileDescriptor
lub parcelable. W Androidzie 13 lub nowszym T
może być dowolnym typem niepierwotnym (w tym interfejsów), 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 tych typów: String
, ParcelFileDescriptor
lub parcelable. W Androidzie 13 lub nowszym T
może być dowolnym typem nieprymitywnym z wyjątkiem tablic.
4. Typy są przekazywane w różny sposób w przypadku kodu Rust w zależności od tego, czy są danymi wejściowymi (argumentem), czy wyjściowymi (zwracaną wartością).
5. Typy związków są obsługiwane w Androidzie 12 i nowszych wersjach.
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 właścicielem bindera jest inny proces. Tworzenie obiektu w inny sposób powoduje podwójne posiadanie.
8. Zobacz też typ Java/AIDL byte[]
.
Kierunkowość (w/wyj/w/wyj)
Podczas określania typów argumentów funkcji możesz użyć wartości in
, out
lub inout
. Ta opcja określa kierunek przekazywania informacji w ramach wywołania IPC. in
to domyślny kierunek, który wskazuje, że dane są przekazywane od dzwoniącego do dzwonionego. out
oznacza, że dane są przekazywane od wywoływanego procesu do wywołującego. inout
to kombinacja tych dwóch wartości. Zespół Androida zaleca jednak unikanie użycia argumentu inout
.
Jeśli używasz inout
z interfejsem 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 typ inout
otrzymuje &mut Vec<T>
, a typ listy inout
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/UTF-16
W backendzie CPP możesz wybrać, czy ciągi mają być kodowane w standardzie utf-8 czy utf-16. W pliku AIDL zadeklaruj ciągi znaków jako @utf8InCpp String
, aby automatycznie konwertować je na kodowanie utf-8.
Backendy NDK i Rust zawsze używają ciągów znaków utf-8. Więcej informacji o adnotacji utf8InCpp
znajdziesz w artykule Adnotacje w AIDL.
Dopuszczalność wartości null
Typy, które mogą być puste, możesz oznaczyć za pomocą @nullable
.
Więcej informacji o annotacjach nullable
znajdziesz w artykule Annotacje w pliku AIDL.
Obiekty do pakowania niestandardowe
Niestandardowy obiekt do zapakowania to obiekt do zapakowania, który jest implementowany ręcznie w docelowym backendzie. Niestandardowych obiektów pakietowych używaj tylko wtedy, gdy chcesz dodać obsługę innych języków dla istniejącego niestandardowego obiektu pakietowego, którego nie można zmienić.
Aby zadeklarować niestandardowy obiekt parcelable, tak aby AIDL o nim wiedział, deklaracja AIDL parcelable będzie wyglądać tak:
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 CPP, który można podzielić na części 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 NDK w pliku 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 do deklaracji 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 backendu CPP/NDK dla niestandardowych obiektów do pakowania, aby używać ich w union
.
Wartości domyślne
Struktury parcelable mogą deklarować wartości domyślne dla poszczególnych pól dla prymitywów,
String
i tablic tych typów.
parcelable Foo {
int numField = 42;
String stringField = "string value";
char charValue = 'a';
...
}
W backendzie 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 nieprzymitywych.
W przypadku innych backendów pola są inicjowane za pomocą domyślnych wartości inicjowanych, 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 domyślnie 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 kodu 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 Rust
W Rust zjednoczenia są implementowane jako enumy 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 binder i mogą być używane przez wszystkie usługi implementujące interfejs binder. 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 działa jak parametr out
w niektórych backendach) powinny być uważane za nieokreślone.
Których wartości błędu używać
Wiele wbudowanych wartości błędów może być używanych w dowolnych interfejsach AIDL, ale niektóre są traktowane w specjalny sposób. Na przykład EX_UNSUPPORTED_OPERATION
i EX_ILLEGAL_ARGUMENT
można używać, gdy opisują one stan błędu, ale nie można używać EX_TRANSACTION_FAILED
, ponieważ jest on traktowany 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 danym typem usługi, 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 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 natywy na Androidzie nie używa wyjątków. Backend CPP korzysta z android::binder::Status
. Backend NDK używa 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 zwrócony błąd Status
lub ScopedAStatus
używa EX_SERVICE_SPECIFIC
wraz z definiowanym przez użytkownika błędem.
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 |
Korzystanie z 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 na stronie Rust AIDL example (przykład Rust AIDL) na stronie Android Rust Patterns (wzorce Rust na Androida).
Typy importu
Niezależnie od tego, czy zdefiniowany typ jest interfejsem, klasą z możliwością dzielenia na części czy klasą z zbiorem, możesz go zaimportować w języku Java:
import my.package.IFoo;
Lub na backendzie 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ż typ zagnieżdżony można importować w Javie, w backendach CPP/NDK należy uwzględnić nagłówek jego typu wyższego poziomu. Podczas importowania zagnieżdżonego typu Bar
zdefiniowanego w my/package/IFoo.aidl
(IFoo
to typ względny pliku) musisz uwzględnić <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 zastępczej. Implementacja interfejsu jest często nazywana usługą, gdy jest zarejestrowana w menedżerze 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 stub odczytuje polecenia z sterownika bindera i wykona zaimplementowane metody. Załóżmy, że masz plik AIDL o takim kształcie:
package my.package;
interface IFoo {
int doFoo();
}
W Javie 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ą domenę 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 asynchronicznym 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 uzyskiwanie dostępu do usług
Usługi w Androidzie są zwykle rejestrowane w procesie servicemanager
. Oprócz wymienionych 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żna wykonać 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ą domenę 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 will run on this thread.
std::future::pending().await
}
Jedną ważną różnicą w stosunku do innych opcji jest to, że nie wywołujemy funkcjijoin_thread_pool
, gdy używamy asynchronicznego Rusta i jednowątkowego środowiska wykonawczego. Musisz to zrobić, ponieważ musisz przekazać Tokio wątek, w którym może on wykonywać wygenerowane zadania. W tym przykładzie do tego celu służy wątek główny. Wszystkie zadania utworzone za pomocą funkcji tokio::spawn
będą wykonywane w wątku głównym.
W asynchronicznym backendzie Rust 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 przypadku wielowątkowego środowiska wykonawczego Tokio utworzone 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 owinąć wywołanie w 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łędów. Wykonuj te wywołania w przypadku obiektów proxy bindera.
- W Javie 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łaj funkcjęmy_binder.link_to_death(&mut my_death_recipient)
. Pamiętaj, że obiektDeathRecipient
jest właścicielem wywołania zwrotnego, dlatego musisz utrzymywać go w stanie aktywnym tak długo, jak 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. PID (identyfikator procesu) to identyfikator procesu Linuxa, który wysyła transakcję. UID (identyfikator użytkownika) odnosi się do identyfikatora użytkownika systemu Linux. Gdy odbierasz połączenie w jednym kierunku, PID dzwoniącego to 0. Gdy są używane poza kontekstem transakcji bindera, zwracają 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();
Podczas implementowania interfejsu w backendzie Rust określ te opcje (zamiast pozostawiania ich domyślnych ustawień):
... = ThreadState::get_calling_pid();
... = ThreadState::get_calling_uid();
Raporty o błędach i interfejsy API do debugowania usług
Gdy uruchamiane są zgłoszenia błędów (np. za pomocą adb bugreport
), zbierają one 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 z użyciem dumpsys SERVICE [ARGS]
, możesz też użyć dumpsys
w wierszu poleceń. W backendach 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, możesz zastąpić metodę dump
w obiekcie serwera tak, jak implementujesz dowolną inną metodę IPC zdefiniowaną w pliku AIDL. W tym celu należy ograniczyć zrzuty do uprawnień aplikacji android.permission.DUMP
lub do konkretnych 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;
Podczas implementowania interfejsu w backendzie Rust określ te opcje (zamiast pozostawiania ich domyślnych wartości):
fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>
Używanie wskaźników słabych
Możesz przechowywać słabe odwołanie do obiektu binder.
Chociaż Java obsługuje WeakReference
, nie obsługuje odwołań do słabego bindera 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żywasz 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 złączacz.
W Javie 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 interfejs w postaci statycznej. W języku Java 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ą domenę aidl
):
#include <aidl/my/package/BnFoo.h>
... aidl::my::package::BnFoo::descriptor
W backendzie Rust:
aidl::my::package::BnFoo::get_descriptor()
Zakres typu wyliczenie
W natywnej implementacji backendu możesz przejrzeć wszystkie możliwe wartości typu wyliczeniowego. Z powodu ograniczeń rozmiaru kodu nie jest to obsługiwane w języku Java.
W przypadku typu zbiorczego MyEnum
zdefiniowanego w 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żde wystąpienie libbinder
w procesie utrzymuje jeden pulę wątków. W większości przypadków powinien to być dokładnie jeden pulę wątków, współdzielona przez wszystkie backendy.
Jedynym wyjątkiem jest sytuacja, gdy kod dostawcy może wczytać kolejną kopię libbinder
, aby komunikować się z /dev/vndbinder
. Ponieważ jest to oddzielny węzeł bindera, 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 pooló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życia join_thread_pool
. Więcej informacji znajdziesz w sekcji poświęconej rejestrowaniu usług.
Zarezerwowane nazwy
W C++, Javie i Rust niektóre nazwy są zarezerwowane jako słowa kluczowe lub do użycia w określonym języku. Chociaż AIDL nie wymusza ograniczeń na podstawie reguł językowych, używanie nazw pól lub typów pasujących 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 „identyfikator w postaci surowych danych”, 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, być może trzeba będzie zaktualizować kod, ale już skompilowane programy będą nadal ze sobą współpracować.
Nazwy, których należy unikać: