Backend AIDL jest miejscem docelowym generowania kodu pośredniego. Podczas korzystania z plików AIDL zawsze używać ich w określonym języku w konkretnym środowisku wykonawczym. W zależności od w kontekście, należy używać różnych backendów AIDL.
W tabeli poniżej stabilność interfejsu API odnosi się do możliwości kompilowania kodu w sposób umożliwiający jego dostarczanie niezależnie od binarnego pliku system.img
libbinder.so
.
AIDL ma następujące backendy:
Backend | Język | Interfejs API | Systemy kompilacji |
---|---|---|---|
Java | Java | SDK/SystemApi (stabilny*) | wszystkie |
NDK | C++ | libbinder_ndk (stabilna*) | interfejs AIDl |
CPP | C++ | libbinder (niestabilny) | wszystkie |
Rust | Rust | libbinder_rs (stabilna wersja*) | interfejs AIDl |
- Te interfejsy API są stabilne, ale wiele z nich, np. interfejsy API do zarządzania usługami, jest zarezerwowanych do użytku wewnętrznego platformy i niedostępnych dla aplikacji. Więcej informacji o korzystaniu z AIDL w aplikacjach znajdziesz tutaj: dokumentację dla programistów.
- Backend Rust został wprowadzony w Androidzie 12, a backend NDK jest dostępny od Androida 10.
- Skrzynka Rust jest zbudowana na konstrukcji
libbinder_ndk
, dzięki czemu może być 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 jej wnętrzu. To zależy odlibbinder_ndk.so
na partycji systemowej.
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: Odniesienie do modułu utworu.
Podstawowy system kompilacji
W dowolnym module cc_
lub java_
w pliku Android.bp (lub w ich odpowiednikach Android.mk
) jako pliki źródłowe można określić pliki .aidl
. W tym przypadku kod Java/CPP
używane są backendy AIDL (nie backend NDK), a klasy do użycia funkcji
odpowiednie pliki 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 do użytku tylko z Rust. rust_
moduł
obsługiwane w inny sposób, ponieważ pliki AIDL nie są określone jako pliki źródłowe.
Zamiast tego moduł aidl_interface
tworzy rustlib
o nazwie
<aidl_interface name>-rust
, które mogą być połączone. Więcej informacji:
przykład Rust AIDL.
interfejs AIDl
Typy używane w tym systemie kompilacji muszą być uporządkowane. 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ądkowana wersja AIDL różni się od stabilnej wersji AIDL, znajdziesz w artykule Uporządkowana i stabilna wersja AIDL.
Typy
Kompilator aidl
możesz traktować jako referencyjną implementację typów.
Podczas tworzenia interfejsu wywołaj funkcję aidl --lang=<backend> ...
, aby wyświetlić wynikowy plik interfejsu. Korzystając z modułu aidl_interface
, możesz zobaczyć
danych wyjściowych w narzędziu out/soong/.intermediates/<path to module>/
.
Typ Java/AIDL | Typ C++ | Typ NDK | Rodzaj rdzy |
---|---|---|---|
wartość logiczna | bool | wartość logiczna | wartość logiczna |
bajt | int8_t | int8_t | i8 |
char | znak16_t | char16_t | U16 |
int | int32_t | int32_t | i32 |
długi | int64_t | int64_t | i64 |
liczba zmiennoprzecinkowa | liczba zmiennoprzecinkowa | float | f32 |
podwójny | podwójny | podwójny | F64 |
Ciąg znaków | android::Ciąg16 | std::string | 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: wektor<T> |
byte[] | std::vector<uint8_t> | std::vector<int8_t>1 | Przy: &[u8] Wyjście: Vec<u8> |
Lista<T> | std::vector<T>2 | std::vector<T>3 | Wejście: &[T]4 Wyjście: Vec<T> |
Deskryptor pliku | android::base::Unique_fd | Nie dotyczy | binder::parcel::ParcelFileDescriptor |
ParcelFileDescriptor | android::os::ParcelFileDescriptor | ndk::ScopedFileDescriptor | binder::parcel::ParcelFileDescriptor |
typ interfejsu (T), | android::sp<T> | std::shared_ptr<T>7 | binder::Silne |
typ 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ędów zgodności.
2. Backend C++ obsługuje język List<T>
, gdzie T
to wartość z wartości String
,
IBinder
, ParcelFileDescriptor
lub przesyłka. Na Androidzie
13 lub wyższy, T
może być dowolnym typem niepodstawowym
(w tym typów interfejsów), z wyjątkiem tablic. AOSP zaleca
używają 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 tych typów: String
, ParcelFileDescriptor
lub parcelable. Na Androidzie 13
lub wyższym, T
może być dowolnym typem niepodstawowym z wyjątkiem tablic.
4. Typy przekazywane w kodzie Rust są różne w zależności od tego, czy są danymi wejściowymi (argumentami) lub danymi wyjściowymi (wartość zwrócona).
5. Typy sumowania są obsługiwane w Androidzie 12 oraz wyżej. .
6. Na Androidzie 13 i nowszych tablice o stałym rozmiarze są
obsługiwane. 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 jest własnością innego procesu. Tworzenie obiektu w inny sposób powoduje podwójne przypisanie własności.
Kierunek (wejście/wyjście/wyjście)
Określając typy argumentów funkcji, możesz określić
je jako in
, out
lub inout
. To ustawienie określa kierunek przekazywania informacji w ramach wywołania IPC. Domyślnym kierunkiem jest in
, który wskazuje, że dane są
przekazywane z rozmówcy do rozmówcy. out
oznacza, że dane są przekazywane z
do dzwoniącego. inout
to połączenie obu tych elementów. Jednak
Zespół Androida odradza korzystanie ze specyfikatora argumentów 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 języka Rust normalny typ inout
otrzymuje &mut Vec<T>
oraz
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);
}
UTF8/UTF16
W backendzie CPP możesz wybrać, czy ciągi mają być w formacie utf-8 czy utf-16. Zadeklaruj
ciągi znaków jako @utf8InCpp String
w AIDL, aby automatycznie konwertować je na 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 annotacjach nullable
znajdziesz w artykule Annotacje w pliku AIDL.
Obiekty parcelable 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ć.
Aby zadeklarować działkę niestandardową, o której wie AIDL, deklaracja parcelable wygląda w ten sposób:
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 deklaracja niestandardowego pakietu Rust w AIDL: 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
niestandardowe parcelable, aby używać ich w union
.
Wartości domyślne
Strukturalne obiekty z możliwością dzielenia na części 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.
Obsługa błędów
System operacyjny Android udostępnia wbudowane typy błędów, których usługi mogą używać do raportowania . Są one używane przez segregator i mogą być używane przez wszystkie usługi implementujące za pomocą interfejsu 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.
Jakich wartości błędów należy użyć
Wiele wbudowanych wartości błędów można zastosować w dowolnym interfejsie AIDL, ale niektóre
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. Błędy związane z poszczególnymi usługami są zwykle zdefiniowane w sekcji
interfejsu AIDL jako enum
wspierany przez const int
lub int
i nie są analizowane przez
separatora.
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 jeden z tych stanów, który reprezentuje stan metody. Backend Rust używa tych samych wartości kodu wyjątku co NDK, ale
konwertuje je na natywne błędy Rusta (StatusCode
, ExceptionCode
) przed
i dostarczaniu ich użytkownikowi. W przypadku błędów dotyczących konkretnej usługi zwracany jest błąd
Status
lub ScopedAStatus
używa EX_SERVICE_SPECIFIC
razem z
zdefiniowany przez użytkownika.
Typy błędów wbudowanych 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 dotyczące korzystania z back-endu Rust znajdziesz na stronie Rust AIDL example (przykład Rust AIDL) w sekcji Rust Patterns for Android (wzorce Rust dla Androida).
Typy importu
Niezależnie od tego, czy zdefiniowany typ to interfejs, parcelable czy union, to w Javie:
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żna zaimportować typ zagnieżdżony w Javie, w backendach CPP/NDK musisz
dołącz nagłówek dla typu katalogu głównego. Na przykład podczas importowania typu zagnieżdżonego
Bar
jest zdefiniowany w my/package/IFoo.aidl
(IFoo
to typ główny funkcji
), musisz dodać <my/package/IFoo.h>
dla backendu CPP (lub
<aidl/my/package/IFoo.h>
dla backendu NDK).
Wdrażanie usług
Aby wdrożyć usługę, musisz odziedziczyć ją z natywnej klasy wersji pośredniej. Ta klasa odczytuje polecenia z sterownika bindera i wykonuje zaimplementowane przez Ciebie metody. Wyobraź sobie, że masz plik AIDL podobny do tego:
package my.package;
interface IFoo {
int doFoo();
}
W języku Java musisz rozszerzyć tę klasę:
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 z asynkronicznym Rustem:
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(())
}
}
Zarejestruj się i pobierz usługi
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 wykonywać tylko podczas kompilowania danych bezpośrednio na platformie 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 ze środowiskiem wykonawczym jednowątkowym:
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 funkcji join_thread_pool
, gdy używamy asynchronicznego Rusta i jednowątkowego środowiska wykonawczego. To jest
ponieważ musisz dać Tokio wątek, w którym będzie mógł wykonywać wygenerowane zadania. W
w tym przykładzie, wątek główny będzie służyć do tego. 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 wielowątkowym środowisku wykonawczym Tokio wygenerowane zadania nie są wykonywane w głównym
w wątku. Dlatego lepiej wywoływać metodę join_thread_pool
w domenie głównej
aby wątek główny nie był tylko nieaktywny. 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łędów. Wykonuj te wywołania na obiektach serwera proxy powiązań.
- 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 ponieważ Wywołanie zwrotne należy doDeathRecipient
. Musisz pozostawić ten obiekt aktywny tak długo, chcesz otrzymywać powiadomienia.
Informacje o rozmówcy
Po otrzymaniu wywołania funkcji Binder jądra informacje o rozmówcy są dostępne w kilka interfejsów API. PID (lub identyfikator procesu) odnosi się do identyfikatora procesu Linux który wysyła transakcję. Identyfikator UID (lub User ID) odnosi się do Identyfikator użytkownika Linuksa. W przypadku połączenia jednokierunkowego identyfikator PID wywołania ma wartość 0. Kiedy poza kontekstem transakcji powiązanej, funkcje te zwracają PID i UID w bieżącym procesie.
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 korzystania z wartości domyślnych):
... = ThreadState::get_calling_pid();
... = ThreadState::get_calling_uid();
Raporty o błędach i interfejs API 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 zgłoszenia błędów używają pliku binarnego dumpsys
we wszystkich usługach
zarejestrowanych u menedżera usługi w celu umieszczania swoich danych w
raport o błędzie. 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 Javy
za pomocą dodatkowych argumentów może kontrolować kolejność, w jakiej usługi są zapisywane
do addService
. Za pomocą parametru dumpsys --pid SERVICE
możesz też uzyskać identyfikator PID
podczas debugowania.
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żywaj słabych wskaźników
Możesz przechowywać słabe odwołanie do obiektu wiązania.
Java obsługuje język WeakReference
, ale nie obsługuje słabych odwołań do powiązań
w warstwie natywnej.
W backendzie CPP typ słaby to wp<IFoo>
.
W backendzie NDK użyj polecenia 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. To jest przydatne podczas debugowania lub nieznanego powiązania.
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 (na przykład podczas rejestrowania usług @VintfStability
) trzeba
statyczny deskryptor interfejsu. 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ą domenę 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 typu wyliczeniowego. Ze względu na rozmiar kodu ta funkcja nie jest obsługiwana w Javie.
W przypadku typu wyliczenia 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 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 backendzie Java pula wątków może się tylko zwiększać (ponieważ już rozpoczęte):
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 asynchronicznym backendzie Rust potrzebujesz 2 pul wątków: Binder i Tokio.
Oznacza to, że aplikacje korzystające z asynchronicznego środowiska Rust wymagają szczególnych
zwłaszcza jeśli chodzi o korzystanie z tagu join_thread_pool
. Przeczytaj sekcję na temat
rejestracji usług, by dowiedzieć się więcej na ten temat.
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 wymusza ograniczeń na podstawie reguł językowych,
nazw pól lub typów, które pasują do zarezerwowanej nazwy, mogą spowodować kompilację
w przypadku języka C++ lub Javy. W przypadku języka Rust nazwa pola lub typu jest zmieniana za pomocą metody
„nieprzetworzony identyfikator” składnia 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ć: