Um back-end AIDL é um destino para geração de código stub. Sempre use arquivos AIDL em uma linguagem específica com um ambiente de execução específico. Dependendo do contexto, use backends AIDL diferentes.
Na tabela a seguir, a estabilidade da superfície da API se refere à capacidade de compilar código com essa superfície de API de forma que o código possa ser entregue de forma independente do binário system.img
libbinder.so
.
A AIDL tem os seguintes back-ends:
Back-end | Idioma | Superfície da API | Sistemas de build |
---|---|---|---|
Java | Java | SDK ou SystemApi (estável*) |
Tudo |
NDK | C++ | libbinder_ndk (estável*) |
aidl_interface |
CPP | C++ | libbinder (instável) |
Tudo |
Rust | Rust | libbinder_rs (estável*) |
aidl_interface |
- Essas superfícies de API são estáveis, mas muitas das APIs, como as de gerenciamento de serviços, são reservadas para uso interno da plataforma e não estão disponíveis para apps. Para mais informações sobre como usar a AIDL em apps, consulte Linguagem de definição de interface do Android (AIDL).
- O back-end Rust foi introduzido no Android 12, e o back-end NDK está disponível desde o Android 10.
- O crate do Rust é criado com base em
libbinder_ndk
, o que o torna estável e portátil. Os APEXs usam o crate binder da maneira padrão no lado do sistema. A parte do Rust é agrupada em um APEX e enviada dentro dele. Essa parte depende dolibbinder_ndk.so
na partição do sistema.
Sistemas de build
Dependendo do back-end, há duas maneiras de compilar AIDL em código stub. Para mais detalhes sobre os sistemas de build, consulte Referência de módulos do Soong.
Sistema de build principal
Em qualquer cc_
ou java_
Android.bp module
(ou nos equivalentes Android.mk
), é possível especificar arquivos AIDL (.aidl
) como arquivos de origem. Nesse caso, os back-ends Java ou CPP do AIDL são usados (não o back-end do NDK), e as classes para usar os arquivos AIDL correspondentes são adicionadas ao módulo automaticamente. Você pode especificar opções como local_include_dirs
(que informa ao sistema de build o caminho raiz para arquivos AIDL nesse módulo) nesses módulos em um grupo aidl:
.
O back-end Rust é usado apenas com
Rust. Os módulos rust_
são processados de maneira diferente porque os arquivos AIDL não são especificados como arquivos de origem. Em vez disso, o módulo aidl_interface
produz um
rustlib
chamado aidl_interface_name-rust
, que pode ser
vinculado. Para mais detalhes, consulte o exemplo de AIDL em Rust.
aidl_interface
Os tipos usados com o sistema de build aidl_interface
precisam ser estruturados. Para serem estruturados, os parcelables precisam conter campos diretamente e não ser declarações de tipos definidos diretamente em linguagens de destino. Para saber como a AIDL estruturada se encaixa na AIDL estável, consulte AIDL estruturada comparada à AIDL estável.
Tipos
Considere o compilador aidl
como uma implementação de referência para tipos.
Ao criar uma interface, invoque aidl --lang=<backend> ...
para ver o arquivo de interface resultante. Ao usar o módulo aidl_interface
, é possível conferir a saída em out/soong/.intermediates/<path to module>/
.
Tipo Java ou AIDL | Tipo C++ | Tipo de NDK | Tipo de ferrugem |
---|---|---|---|
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 |
Entrada: &str Saída: String |
android.os.Parcelable |
android::Parcelable |
N/A | N/A |
IBinder |
android::IBinder |
ndk::SpAIBinder |
binder::SpIBinder |
T[] |
std::vector<T> |
std::vector<T> |
Entrada: &[T] Saída: Vec<T> |
byte[] |
std::vector |
std::vector 1 |
Entrada: &[u8] Saída: Vec<u8> |
List<T> |
std::vector<T> 2 |
std::vector<T> 3 |
Em: In: &[T] 4Saída: Vec<T> |
FileDescriptor |
android::base::unique_fd |
N/A | N/A |
ParcelFileDescriptor |
android::os::ParcelFileDescriptor |
ndk::ScopedFileDescriptor |
binder::parcel::ParcelFileDescriptor |
Tipo de interface (T ) |
android::sp<T> |
std::shared_ptr<T> 7 |
binder::Strong |
Tipo Parcelable (T ) |
T |
T |
T |
Tipo de união (T )5 |
T |
T |
T |
T[N] 6 |
std::array<T, N> |
std::array<T, N> |
[T; N] |
1. No Android 12 ou versões mais recentes, as matrizes de bytes usam
uint8_t
em vez de int8_t
por motivos de compatibilidade.
2. O back-end C++ é compatível com List<T>
, em que T
é um dos
String
, IBinder
, ParcelFileDescriptor
ou parcelable. No Android 13 ou versões mais recentes, T
pode ser qualquer tipo não primitivo (incluindo tipos de interface), exceto matrizes. O AOSP recomenda usar tipos de matriz como T[]
, porque eles
funcionam em todos os back-ends.
3. O back-end do NDK é compatível com List<T>
, em que T
é um dos String
, ParcelFileDescriptor
ou parcelable. No Android 13
ou mais recente, T
pode ser qualquer tipo não primitivo, exceto matrizes.
4. Os tipos são transmitidos de maneira diferente para o código Rust, dependendo se eles são uma entrada (um argumento) ou uma saída (um valor retornado).
5. Os tipos de união são compatíveis com o Android 12 e versões mais recentes.
6. No Android 13 ou versões mais recentes, há suporte para matrizes de tamanho fixo. Matrizes de tamanho fixo podem ter várias dimensões (por exemplo, int[3][4]
). No back-end Java, elas são representadas como tipos de matriz.
7. Para instanciar um objeto binder SharedRefBase
, use
SharedRefBase::make\<My\>(... args ...)
. Essa função cria um objeto std::shared_ptr\<T\>
, que também é gerenciado internamente, caso o binder seja de outro processo. Criar o objeto de outras maneiras causa
dupla propriedade.
8. Consulte também o tipo Java ou AIDL byte[]
.
Direção (in, out e inout)
Ao especificar os tipos de argumentos para funções, você pode especificá-los como in
, out
ou inout
. Isso controla a direção em que as informações são
transmitidas para uma chamada de IPC.
O especificador de argumento
in
indica que os dados são transmitidos do chamador para o chamado. O especificadorin
é a direção padrão, mas se os tipos de dados também puderem serout
, especifique a direção.O especificador de argumento
out
significa que os dados são transmitidos do destinatário para o autor da chamada.O especificador de argumento
inout
é a combinação dos dois. No entanto, recomendamos evitar o uso do especificador de argumentoinout
. Se você usarinout
com uma interface versionada e um destinatário de chamada mais antigo, os campos adicionais presentes apenas no chamador serão redefinidos para os valores padrão. Em relação ao Rust, um tipoinout
normal recebe&mut T
, e um tipoinout
de lista recebe&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 e UTF-16
Com o back-end do CPP, você pode escolher se as strings são UTF-8 ou UTF-16.
Declare strings como @utf8InCpp String
em AIDL para convertê-las automaticamente
em UTF-8. Os backends do NDK e do Rust sempre usam strings UTF-8. Para mais informações sobre a anotação utf8InCpp
, consulte utf8InCpp.
Nulidade
É possível anotar tipos que podem ser nulos com @nullable
.
Para mais informações sobre a anotação nullable
, consulte
nullable.
Parcelables personalizados
Um parcelable personalizado é um parcelable implementado manualmente em um back-end de destino. Use parcelables personalizados apenas quando você estiver tentando adicionar suporte a outros idiomas para um parcelable personalizado que não pode ser mudado.
Confira um exemplo de declaração parcelable AIDL:
package my.pack.age;
parcelable Foo;
Por padrão, isso declara um parcelable Java em que my.pack.age.Foo
é uma classe Java que implementa a interface Parcelable
.
Para uma declaração de um parcelable de back-end CPP personalizado em AIDL, use cpp_header
:
package my.pack.age;
parcelable Foo cpp_header "my/pack/age/Foo.h";
A implementação em C++ em my/pack/age/Foo.h
é assim:
#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);
};
Para uma declaração de um parcelable NDK personalizado em AIDL, use ndk_header
:
package my.pack.age;
parcelable Foo ndk_header "android/pack/age/Foo.h";
A implementação do NDK em android/pack/age/Foo.h
tem esta aparência:
#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);
};
No Android 15, para declarar um parcelable personalizado do Rust em AIDL,
use rust_type
:
package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";
A implementação em Rust no rust_crate/src/lib.rs
é assim:
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);
Depois, você pode usar esse parcelable como um tipo em arquivos AIDL, mas ele não será
gerado pelo AIDL. Forneça operadores <
e ==
para parcelables personalizados de back-end do CPP e do NDK para usá-los em union
.
Valores padrão
Parcelables estruturados podem declarar valores padrão por campo para primitivos, campos String
e matrizes desses tipos.
parcelable Foo {
int numField = 42;
String stringField = "string value";
char charValue = 'a';
...
}
No back-end Java, quando os valores padrão estão ausentes, os campos são inicializados como valores zero para tipos primitivos e null
para tipos não primitivos.
Em outros back-ends, os campos são inicializados com valores padrão quando
não há valores padrão definidos. Por exemplo, no back-end C++, os campos String
são inicializados como uma string vazia, e os campos List<T>
são inicializados como um vector<T>
vazio. Os campos @nullable
são inicializados como campos de valor nulo.
União
As uniões da AIDL são marcadas, e os recursos são semelhantes em todos os back-ends. Eles são construídos com o valor padrão do primeiro campo e têm uma maneira específica de linguagem para interagir com eles:
union Foo {
int intField;
long longField;
String stringField;
MyParcelable parcelableField;
...
}
Exemplo em Java
Foo u = Foo.intField(42); // construct
if (u.getTag() == Foo.intField) { // tag query
// use u.getIntField() // getter
}
u.setStringField("abc"); // setter
Exemplo de C++ e 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)
Exemplo do Rust
Em Rust, as uniões são implementadas como enums e não têm getters e setters explícitos.
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
Tratamento de erros
O SO Android oferece tipos de erros integrados para os serviços usarem ao informar erros. Eles são usados por vinculadores e podem ser usados por qualquer serviço que implemente uma interface de vinculador. O uso delas está bem documentado na definição de AIDL e não exige nenhum status ou tipo de retorno definido pelo usuário.
Parâmetros de saída com erros
Quando uma função AIDL informa um erro, ela pode não inicializar ou
modificar parâmetros de saída. Especificamente, os parâmetros de saída podem ser modificados se o erro ocorrer durante a descompactação, em vez de acontecer durante o processamento da transação em si. Em geral, ao receber um erro de uma função
AIDL, todos os parâmetros inout
e out
, bem como o valor de retorno
(que funciona como um parâmetro out
em alguns back-ends), devem ser considerados
em um estado indefinido.
Quais valores de erro usar
Muitos dos valores de erro integrados podem ser usados em qualquer interface AIDL, mas alguns
são tratados de maneira especial. Por exemplo, EX_UNSUPPORTED_OPERATION
e EX_ILLEGAL_ARGUMENT
podem ser usados quando descrevem a condição de erro, mas EX_TRANSACTION_FAILED
não pode ser usado porque é tratado de maneira especial pela infraestrutura subjacente. Confira as definições específicas do back-end para mais informações sobre esses valores integrados.
Se a interface AIDL exigir valores de erro adicionais que não são cobertos pelos tipos de erro integrados, ela poderá usar o erro integrado especial específico do serviço, que permite a inclusão de um valor de erro específico do serviço definido pelo usuário. Esses erros específicos do serviço geralmente são definidos
na interface AIDL como um enum
com suporte de const int
ou int
e não são analisados
pelo binder.
Em Java, os erros são mapeados para exceções, como android.os.RemoteException
. Para exceções específicas do serviço, o Java usa android.os.ServiceSpecificException
com o erro definido pelo usuário.
O código nativo no Android não usa exceções. O back-end do CPP usa
android::binder::Status
. O back-end do NDK usa ndk::ScopedAStatus
. Todo
método gerado pela AIDL retorna um deles, representando o status do
método. O back-end Rust usa os mesmos valores de código de exceção que o NDK, mas
os converte em erros nativos do Rust (StatusCode
, ExceptionCode
) antes de
entregá-los ao usuário. Para erros específicos do serviço, o Status
ou ScopedAStatus
retornado usa EX_SERVICE_SPECIFIC
com o erro definido pelo usuário.
Os tipos de erros integrados podem ser encontrados nos seguintes arquivos:
Back-end | Definição |
---|---|
Java | android/os/Parcel.java |
CPP | binder/Status.h |
NDK | android/binder_status.h |
Rust | android/binder_status.h |
Usar vários back-ends
Estas instruções são específicas para o código da plataforma Android. Esses exemplos usam um tipo definido, my.package.IFoo
. Para instruções sobre como usar o back-end
Rust, consulte o exemplo de AIDL
do Rust
em Padrões do Rust
no Android.
Tipos de importação
Se o tipo definido é uma interface, um parcelable ou uma união, é possível importá-lo em Java:
import my.package.IFoo;
Ou no back-end do CPP:
#include <my/package/IFoo.h>
Ou no back-end do NDK (observe o namespace aidl
extra):
#include <aidl/my/package/IFoo.h>
Ou no back-end do Rust:
use my_package::aidl::my::package::IFoo;
Embora seja possível importar um tipo aninhado em Java, nos back-ends CPP e NDK, é necessário incluir o cabeçalho do tipo raiz. Por exemplo, ao importar um tipo aninhado
Bar
definido em my/package/IFoo.aidl
(IFoo
é o tipo raiz do
arquivo), inclua <my/package/IFoo.h>
para o back-end CPP ou
<aidl/my/package/IFoo.h>
para o back-end NDK.
Implementar uma interface
Para implementar uma interface, é necessário herdar da classe stub nativa. Uma
implementação de uma interface é geralmente chamada de serviço quando é registrada
com o gerenciador de serviços ou android.app.ActivityManager
e chamada de
callback quando é registrada por um cliente de um serviço. No entanto, vários nomes são usados para descrever implementações de interface, dependendo do uso exato. A classe stub lê comandos do driver binder e executa os
métodos que você implementa. Imagine que você tem um arquivo AIDL como este:
package my.package;
interface IFoo {
int doFoo();
}
Em Java, você precisa estender da classe Stub
gerada:
import my.package.IFoo;
public class MyFoo extends IFoo.Stub {
@Override
int doFoo() { ... }
}
No back-end do CPP:
#include <my/package/BnFoo.h>
class MyFoo : public my::package::BnFoo {
android::binder::Status doFoo(int32_t* out) override;
}
No back-end do NDK (observe o namespace aidl
extra):
#include <aidl/my/package/BnFoo.h>
class MyFoo : public aidl::my::package::BnFoo {
ndk::ScopedAStatus doFoo(int32_t* out) override;
}
No back-end do 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(())
}
}
Ou com Rust assíncrono:
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(())
}
}
Registrar e receber serviços
Os serviços na plataforma Android geralmente são registrados com o processo servicemanager
. Além das APIs a seguir, algumas verificam o
serviço (ou seja, retornam imediatamente se ele não estiver disponível).
Confira os detalhes exatos na interface servicemanager
correspondente. Você só pode
realizar essas operações ao compilar para a plataforma Android.
Em Java:
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"));
No back-end do 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"));
No back-end do NDK (observe o namespace aidl
extra):
#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")));
No back-end do 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()
}
No back-end assíncrono do Rust, com um ambiente de execução de uma única linha de execução:
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
}
Uma diferença importante das outras opções é que você não chama
join_thread_pool
ao usar Rust assíncrono e um tempo de execução de uma única linha de execução. Isso ocorre
porque você precisa dar ao Tokio uma linha de execução em que ele possa executar tarefas geradas. No exemplo a seguir, a linha de execução principal atende a essa finalidade. Todas as tarefas geradas usando tokio::spawn
são executadas na linha de execução principal.
No back-end assíncrono do Rust, com um ambiente de execução multithread:
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();
});
}
Com o tempo de execução multithread do Tokio, as tarefas geradas não são executadas na linha de execução
principal. Portanto, faz mais sentido chamar join_thread_pool
na linha de execução principal para que ela não fique ociosa. É necessário unir a chamada em
block_in_place
para sair do contexto assíncrono.
Link para a morte
Você pode pedir para receber uma notificação quando um serviço que hospeda um binder for encerrado. Isso pode ajudar a evitar vazamentos de proxies de callback ou auxiliar na recuperação de erros. Faça essas chamadas em objetos proxy de binder.
- Em Java, use
android.os.IBinder::linkToDeath
. - No back-end do CPP, use
android::IBinder::linkToDeath
. - No back-end do NDK, use
AIBinder_linkToDeath
. - No back-end Rust, crie um objeto
DeathRecipient
e chamemy_binder.link_to_death(&mut my_death_recipient)
. ComoDeathRecipient
é proprietário do callback, mantenha esse objeto ativo enquanto quiser receber notificações.
Informações do autor da chamada
Ao receber uma chamada de vinculador do kernel, as informações do autor da chamada estão disponíveis em várias APIs. O ID do processo (PID) se refere ao ID do processo do Linux que está enviando uma transação. O ID do usuário (UI) se refere ao ID do usuário do Linux. Ao receber uma chamada unidirecional, o PID de chamada é 0. Fora de um contexto de transação de binder, essas funções retornam o PID e o UID do processo atual.
No back-end Java:
... = Binder.getCallingPid();
... = Binder.getCallingUid();
No back-end do CPP:
... = IPCThreadState::self()->getCallingPid();
... = IPCThreadState::self()->getCallingUid();
No back-end do NDK:
... = AIBinder_getCallingPid();
... = AIBinder_getCallingUid();
No back-end do Rust, ao implementar a interface, especifique o seguinte (em vez de permitir que ela seja padrão):
... = ThreadState::get_calling_pid();
... = ThreadState::get_calling_uid();
Relatórios de bugs e API de depuração para serviços
Quando os relatórios de bugs são executados (por exemplo, com adb bugreport
), eles coletam informações de todo o sistema para ajudar na depuração de vários problemas.
Para serviços AIDL, os relatórios de bugs usam o binário dumpsys
em todos os serviços
registrados com o gerenciador de serviços para despejar as informações no
relatório de bugs. Também é possível usar dumpsys
na linha de comando para receber informações
de um serviço com dumpsys SERVICE [ARGS]
. Nos back-ends C++ e Java, é possível controlar a ordem em que os serviços são despejados usando argumentos adicionais para addService
. Também é possível usar dumpsys --pid SERVICE
para receber o PID de um
serviço durante a depuração.
Para adicionar uma saída personalizada ao seu serviço, substitua o método dump
no objeto do servidor como se estivesse implementando qualquer outro método IPC
definido em um arquivo AIDL. Ao fazer isso, restrinja o despejo à permissão
do app android.permission.DUMP
ou a UIDs específicos.
No back-end Java:
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
@Nullable String[] args) {...}
No back-end do CPP:
status_t dump(int, const android::android::Vector<android::String16>&) override;
No back-end do NDK:
binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;
No back-end do Rust, ao implementar a interface, especifique o seguinte (em vez de permitir que ela seja padrão):
fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>
Usar ponteiros fracos
É possível manter uma referência fraca a um objeto binder.
Embora o Java seja compatível com WeakReference
, ele não é compatível com referências de binder fraco
na camada nativa.
No back-end do CPP, o tipo fraco é wp<IFoo>
.
No back-end do NDK, use ScopedAIBinder_Weak
:
#include <android/binder_auto_utils.h>
AIBinder* binder = ...;
ScopedAIBinder_Weak myWeakReference = ScopedAIBinder_Weak(AIBinder_Weak_new(binder));
No back-end do Rust, use WpIBinder
ou Weak<IFoo>
:
let weak_interface = myIface.downgrade();
let weak_binder = myIface.as_binder().downgrade();
Receber dinamicamente o descritor de interface
O descritor de interface identifica o tipo de uma interface. Isso é útil ao depurar ou quando você tem um binder desconhecido.
Em Java, é possível receber o descritor de interface com um código como:
service = /* get ahold of service object */
... = service.asBinder().getInterfaceDescriptor();
No back-end do CPP:
service = /* get ahold of service object */
... = IInterface::asBinder(service)->getInterfaceDescriptor();
Os backends NDK e Rust não são compatíveis com esse recurso.
Receber descritor de interface de forma estática
Às vezes (como ao registrar serviços @VintfStability
), é necessário saber qual é o descritor de interface estaticamente. Em Java, é possível adicionar um código como este para receber o descritor:
import my.package.IFoo;
... IFoo.DESCRIPTOR
No back-end do CPP:
#include <my/package/BnFoo.h>
... my::package::BnFoo::descriptor
No back-end do NDK (observe o namespace aidl
extra):
#include <aidl/my/package/BnFoo.h>
... aidl::my::package::BnFoo::descriptor
No back-end do Rust:
aidl::my::package::BnFoo::get_descriptor()
Intervalo de enumeração
Em back-ends nativos, é possível iterar os valores possíveis que uma enumeração pode assumir. Devido a considerações sobre o tamanho do código, isso não é compatível com Java.
Para uma enumeração MyEnum
definida em AIDL, a iteração é fornecida da seguinte maneira.
No back-end do CPP:
::android::enum_range<MyEnum>()
No back-end do NDK:
::ndk::enum_range<MyEnum>()
No back-end do Rust:
MyEnum::enum_values()
Gerenciamento de linhas de execução
Cada instância de libbinder
em um processo mantém um pool de linhas de execução. Na maioria dos casos de uso, esse valor deve ser exatamente um pool de linhas de execução compartilhado entre todos os back-ends.
A única exceção é se o código do fornecedor carregar outra cópia de libbinder
para se comunicar com /dev/vndbinder
. Isso está em um nó de binder separado, então o
threadpool não é compartilhado.
Para o back-end Java, o pool de linhas de execução só pode aumentar de tamanho (porque já foi iniciado):
BinderInternal.setMaxThreads(<new larger value>);
Para o back-end do CPP, as seguintes operações estão disponíveis:
// 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();
Da mesma forma, no back-end do NDK:
bool success = ABinderProcess_setThreadPoolMaxThreadCount(numThreads);
ABinderProcess_startThreadPool();
ABinderProcess_joinThreadPool();
No back-end do Rust:
binder::ProcessState::start_thread_pool();
binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
binder::ProcessState::join_thread_pool();
Com o back-end assíncrono do Rust, você precisa de dois pools de linhas de execução: binder e Tokio.
Isso significa que os apps que usam Rust assíncrono precisam de considerações especiais,
principalmente quando se trata do uso de join_thread_pool
. Consulte a seção sobre
registro de serviços para mais informações.
Nomes reservados
C++, Java e Rust reservam alguns nomes como palavras-chave ou para uso específico da linguagem. Embora a AIDL não imponha restrições com base em regras de linguagem, usar
nomes de campos ou tipos que correspondam a um nome reservado pode resultar em uma falha
de compilação para C++ ou Java. Para Rust, o campo ou tipo é renomeado usando a
sintaxe de identificador bruto, acessível com o prefixo r#
.
Recomendamos evitar o uso de nomes reservados nas definições de AIDL sempre que possível para evitar vinculações não ergonômicas ou falha total na compilação.
Se você já tiver nomes reservados nas suas definições de AIDL, poderá renomear campos com segurança sem perder a compatibilidade com o protocolo. Talvez seja necessário atualizar o código para continuar criando, mas todos os programas já criados continuam interoperando.
Nomes a evitar: