Backend AIDL jest celem generowania kodu zastępczego. Plików AIDL używa się zawsze w określonym języku i 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 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 wersja*) | 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 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.
- Skrzynka Rust jest zbudowana na podstawie
libbinder_ndk
, co pozwala na stabilność i przenośność. APEXe korzystają z binder crate w taki sam sposób jak każda inna osoba po stronie systemu. Część Rust jest wstępnie skompilowana 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 na 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 głównego plików AIDL w tym 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ć. Więcej informacji znajdziesz w przykładzie Rust AIDL.
aidl_interface
Typy używane w tym systemie kompilacji muszą być ustrukturyzowane. Aby były strukturalne, 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ądkowana wersja AIDL różni się od stabilnej wersji AIDL, znajdziesz w artykule Uporządkowana wersja a stabilna wersja 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 | bool | bool | bool |
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 |
float | float | float | f32 |
podwójny | podwójny | podwójny | f64 |
Ciąg znaków | android::String16 | std::string | W: &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> |
List<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 |
parcelable type (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ędów zgodności.
2. Backend w 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 we 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 nieprymitywnym (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 przypisanie własności.
8. Zobacz też typ Java/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
. To ustawienie kontroluje informacje o kierunku przekazywane w wywołaniu IPC. Argument in
wskazuje, że dane są przekazywane z poziomu wywołującego do wywoływanego. Wskaźnik argumentu out
oznacza, że dane są przekazywane od wywoływanego do wywołującego. Wskaźnik in
jest domyślnym kierunkiem, ale jeśli typy danych mogą też być out
, musisz określić kierunek. Parametr inout
to kombinacja obu tych parametrów. Zalecamy jednak unikanie użycia argumentu inout
. Jeśli używasz inout
z interfejsem w wersji 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 T
, a typ listy inout
– &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);
}
UTF8/UTF16
W backendzie CPP możesz wybrać, czy ciągi mają być w formacie utf-8 czy utf-16. W pliku AIDL zadeklaruj ciągi znaków jako @utf8InCpp String
, aby automatycznie przekonwertować je na kodowanie utf-8.
Backendy NDK i Rust zawsze używają ciągów znaków utf-8. Więcej informacji o annotacjach utf8InCpp
znajdziesz w artykule Annotacje w pliku AIDL.
Dopuszczalność wartości null
Typy, które mogą być puste, możesz oznaczyć za pomocą @nullable
.
Więcej informacji o adnotacji nullable
znajdziesz w artykule Adnotacje w pliku AIDL.
Niestandardowe obiekty do pakowania
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ć.
Aby zadeklarować niestandardowy obiekt parcelable, aby AIDL o nim wiedział, deklaracja obiektu parcelable w AIDL 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 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 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 deklaracji niestandardowego obiektu Rust w AIDL, który można opakować, użyj: 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. W celu użycia ich w union
podaj operatory <
i ==
dla backendu CPP/NDK dla niestandardowych obiektów do zapakowania.
Wartości domyślne
Strukturalne obiekty do zapakowania mogą deklarować wartości domyślne dla poszczególnych pól dla typów prymitywnych,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 nieprzymitywów.
W innych backendach pola są inicjowane za pomocą wartości domyślnych, jeśli te 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 dotyczący rdzy
W Rust zjednoczenia są implementowane jako typy wyliczeniowe i nie mają jawnych metod gettera i settera.
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 dowolne 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 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żywanych 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 stanu 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 serwisem, który umożliwia uwzględnienie wartości błędu określonej przez użytkownika. Te błędy związane z poszczególnymi usługami są zwykle definiowane w interfejsie AIDL jako const int
lub int
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 na Androida 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 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 na stronie Rust AIDL example (przykład Rust AIDL) w sekcji 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ą 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ż 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 stub. Implementacja interfejsu jest często nazywana usługą, gdy jest zarejestrowana w menedżerze usługi lub android.app.ActivityManager
, i nazywana 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 stub 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 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ą 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 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 wykonywać uruchamiane zadania. W tym przykładzie służy do tego wątek główny. Wszystkie zadania utworzone za pomocą funkcji tokio::spawn
będą wykonywane w głównym wątku.
W niesynchronizowanym 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 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 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 (lub identyfikator użytkownika) odnosi się do identyfikatora użytkownika systemu Linux. W przypadku połączenia jednokierunkowego PID dzwoniącego to 0. Gdy są używane poza kontekstem transakcji bindera, 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();
Zgłaszanie błędów i interfejs API do debugowania 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 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 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, 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 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 odnośników
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ż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 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 wyliczenie
W natywnych backendach możesz przejść przez możliwe wartości typu wyliczeniowego. 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 pulę wątków, współdzielona przez wszystkie backendy.
Jedynym wyjątkiem jest sytuacja, gdy kod dostawcy może wczytać kolejną kopię libbinder
, aby rozmawiać 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 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 korzystania z funkcji 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 konkretnym języku. Chociaż AIDL nie narzuca 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 Rust nazwa pola lub typu jest zmieniana za pomocą składni „identyfikator surowy”, która jest dostępna 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ć: