Um back-end AIDL é um destino para a geração de código stub. Ao usar arquivos AIDL, você sempre os usa em um idioma específico com um ambiente de execução específico. Dependendo do contexto, use back-ends AIDL diferentes.
Na tabela a seguir, a estabilidade da plataforma da API se refere à capacidade
de compilar o código em relação a essa plataforma de uma maneira que o código possa ser
entregue independentemente do binário libbinder.so
system.img
.
O AIDL tem os seguintes back-ends:
Back-end | Idioma | Superfície da API | Sistemas de build |
---|---|---|---|
Java | Java | SDK/SystemApi (estável*) | todas |
NDK | C++ | libbinder_ndk (estável*) | interface_aidl |
CPP | C++ | libbinder (instável) | todas |
Rust | Rust | libbinder_rs (estável*) | interface_aidl |
- Essas plataformas de API são estáveis, mas muitas delas, 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 o AIDL em apps, consulte a documentação para desenvolvedores.
- O back-end do Rust foi introduzido no Android 12. O back-end do NDK está disponível desde o Android 10.
- A caixa do Rust é criada com base na
libbinder_ndk
, o que permite que ela seja estável e portátil. Os APEXes usam o bloco de vinculação da mesma forma que qualquer outra pessoa no lado do sistema. A parte do Rust é agrupada em um APEX e enviada dentro dele. Depende delibbinder_ndk.so
da partição do sistema.
Sistemas de build
Dependendo do back-end, há duas maneiras de compilar o AIDL em código stub. Para mais detalhes sobre os sistemas de build, consulte a Referência do módulo Soong.
Sistema de build principal
Em qualquer módulo Android.bp cc_
ou java_
(ou nos equivalentes Android.mk
),
os arquivos .aidl
podem ser especificados como arquivos de origem. Nesse caso, os back-ends Java/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. Opções
como local_include_dirs
, que informa ao sistema de build o caminho raiz para
arquivos AIDL nesse módulo, podem ser especificadas nesses módulos em um grupo
aidl:
. O back-end do Rust é usado apenas com o Rust. Os módulos rust_
são
processados de forma 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 saber mais, consulte
o exemplo da AIDL do Rust.
aidl_interface
Os tipos usados com esse sistema de build precisam ser estruturados. Para serem estruturados, os elementos parceláveis precisam conter campos diretamente e não podem ser declarações de tipos definidas diretamente nos idiomas de destino. Para saber como a AIDL estruturada se encaixa na AIDL estável, consulte AIDL estruturada versus estável.
Tipos
Você pode considerar o compilador aidl
como uma implementação de referência para tipos.
Ao criar uma interface, invoque aidl --lang=<backend> ...
para conferir o
arquivo de interface resultante. Ao usar o módulo aidl_interface
, é possível ver
a saída em out/soong/.intermediates/<path to module>/
.
Tipo Java/AIDL | Tipo C++ | Tipo do NDK | Tipo de ferrugem |
---|---|---|---|
booleano | booleano | booleano | booleano |
byte8 | 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 | String |
android.os.Parcelable | android::Parcelable | N/A | N/A |
IBinder | android::IBinder | ndk::SpAIBinder | binder::SpIBinder |
T[] | std::vector<T> | std::vector<T> | In: &[T] Out: Vec<T> |
byte[] | std::vetor<uint8_t> | std::vector<int8_t>1 | In: &[u8] Saída: Vec<u8> |
List<T> | std::vector<T>2 | std::vector<T>3 | In: &[T]4 Saída: Vec<T> |
FileDescriptor | android::base::unique_fd | N/A | binder::parcel::ParcelFileDescriptor |
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 |
V[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++ oferece suporte a List<T>
, em que T
é um dos seguintes: String
,
IBinder
, ParcelFileDescriptor
ou parcelable. No Android
13 ou mais recente, T
pode ser qualquer tipo não primitivo
(incluindo tipos de interface), exceto matrizes. O AOSP recomenda
que você use tipos de matriz, como T[]
, já que elas funcionam em todos os back-ends.
3. O back-end do NDK tem suporte a List<T>
, em que T
é 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 de eles serem uma entrada (um argumento) ou uma saída (um valor retornado).
5. Os tipos de união têm suporte no Android 12 e versões mais recentes.
6. No Android 13 ou versões mais recentes, há suporte para matrizes de
tamanho fixo. As matrizes de tamanho fixo podem ter várias dimensões (por exemplo, int[3][4]
).
No back-end do Java, elas são representadas como tipos de matriz.
7. Para instanciar um objeto SharedRefBase
de vinculação, use
SharedRefBase::make\<My\>(... args ...)
. Essa função cria um
objeto std::shared_ptr\<T\>
que também é gerenciado internamente, caso o vinculador seja de outro
processo. Criar o objeto de outras maneiras causa dupla propriedade.
8. Consulte também o tipo byte[]
do Java/AIDL.
Direção (in/out/inout)
Ao definir os tipos de argumentos para as funções, você pode especificar
como in
, out
ou inout
. Isso controla em que direção as informações são
transmitidas para uma chamada de IPC. in
é a direção padrão e indica que os dados são
transmitidos do autor da chamada para o destinatário. out
significa que os dados são transmitidos do
autor da chamada para o autor da chamada. inout
é a combinação dos dois. No entanto, a
equipe do Android recomenda que você evite usar o especificador de argumento inout
.
Se você usar inout
com uma interface com controle de versão e um recebedor da chamada mais antigo, os
campos adicionais presentes apenas no autor da chamada serão redefinidos para os valores
padrão. Com relação ao Rust, um tipo inout
normal recebe &mut Vec<T>
, e
um tipo inout
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);
}
UTF8/UTF16
Com o back-end do CPP, você pode escolher se as strings são utf-8 ou utf-16. Declare
strings como @utf8InCpp String
na AIDL para convertê-las automaticamente para utf-8.
Os back-ends do NDK e do Rust sempre usam strings utf-8. Para mais informações sobre
a anotação utf8InCpp
, consulte Anotações na AIDL.
Nulidade
É possível anotar tipos que podem ser nulos com @nullable
.
Para mais informações sobre a anotação nullable
, consulte
Anotações no AIDL.
Parcelables personalizados
Um parcelável personalizado é um elemento parcelável implementado manualmente em um back-end de destino. Use parceláveis personalizados apenas quando você estiver tentando adicionar suporte a outros idiomas para um parcelável personalizado que não possa ser alterado.
Para declarar um parcelable personalizado para que a AIDL saiba sobre ele, a declaração parcelável da AIDL tem esta aparência:
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 back-end de CPP personalizado e parcelable na 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
fica 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 declarar um parcelable NDK personalizado na 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
fica assim:
#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
Rust personalizado na AIDL, use rust_type
:
package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";
A implementação do Rust em rust_crate/src/lib.rs
fica 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);
Em seguida, você pode usar esse parcelável como um tipo em arquivos AIDL, mas ele não será
gerado pelo AIDL. Fornecer operadores <
e ==
para Parcelables personalizados
de back-end CPP/NDK para usá-los em union
.
Valores padrão
Os parceláveis estruturados podem declarar valores padrão por campo para primitivos,
String
s 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
os valores padrão não são definidos. Por exemplo, no back-end em C++, os campos String
são inicializados como uma string vazia, e os campos List<T>
como um
vector<T>
vazio. Os campos @nullable
são inicializados como campos de valor nulo.
Sindicatos
As uniões AIDL são marcadas e os recursos delas são semelhantes em todos os back-ends. Por padrão, eles são criados com o valor padrão do primeiro campo e têm uma maneira específica da linguagem de 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.setSringField("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 de Rust
No Rust, as uniões são implementadas como enumerações 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 que os serviços usem ao informar erros. Elas são usadas pelo binder e podem ser usadas por qualquer serviço que implemente uma interface binder. O uso deles está bem documentado na definição da AIDL e não exigem 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 os parâmetros de saída. Especificamente, os parâmetros de saída poderão ser modificados se o
erro ocorrer durante a descompactação em vez de acontecer durante o processamento
da própria transação. 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 atua como um parâmetro out
em alguns back-ends) precisam ser considerados em 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. Verifique 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 sejam 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 a 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
. Cada
método gerado pelo AIDL retorna um deles, representando o status do
método. O back-end do 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 eles serem enviados 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 erro 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 do Rust,
consulte o exemplo de AIDL do Rust
na página Padrões do Rust no Android.
Tipos de importação
Se o tipo definido for uma interface, parcelável ou união, ele poderá ser importado 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 Rust:
use my_package::aidl::my::package::IFoo;
Embora seja possível importar um tipo aninhado em Java, nos back-ends CPP/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), é necessário incluir <my/package/IFoo.h>
para o back-end do CPP (ou
<aidl/my/package/IFoo.h>
para o back-end do NDK).
Implementar serviços
Para implementar um serviço, é necessário herdar da classe stub nativa. Essa classe lê comandos do driver de vinculação 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, é necessário estender esta classe:
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 o 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 abaixo, algumas APIs verificam o
serviço, ou seja, elas retornam imediatamente se o serviço não estiver disponível.
Confira os detalhes exatos na interface servicemanager
correspondente. Essas
operações só podem ser feitas 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 linha de execução única:
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
}
Uma diferença importante em relação às outras opções é que não chamamos
join_thread_pool
ao usar o Rust assíncrono e um ambiente de execução de linha única. Isso ocorre
porque você precisa fornecer ao Tokio uma linha de execução em que ele possa executar tarefas geradas. Neste
exemplo, a linha de execução principal vai servir a essa finalidade. Todas as tarefas geradas usando
tokio::spawn
serão executadas na linha de execução principal.
No back-end assíncrono do Rust, com um ambiente de execução de várias linhas de execução:
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 ambiente 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 morte
Você pode solicitar uma notificação quando um serviço que hospeda um vinculamento for encerrado. Isso pode ajudar a evitar o vazamento de proxies de callback ou auxiliar na recuperação de erros. Faça essas chamadas em objetos proxy de vinculação.
- 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 do Rust, crie um objeto
DeathRecipient
e chamemy_binder.link_to_death(&mut my_death_recipient)
. Como oDeathRecipient
é proprietário do callback, você precisa manter esse objeto ativo enquanto quiser receber notificações.
Informações do autor da chamada
Ao receber uma chamada de vinculação do kernel, as informações do autor da chamada estão disponíveis em várias APIs. O PID (ou ID do processo) se refere ao ID do processo do Linux que está enviando uma transação. O UID (ou ID do usuário) se refere ao ID do usuário do Linux. Ao receber uma chamada unidirecional, o PID de chamada é 0. Quando estão fora de um contexto de transação de binder, essas funções retornam o PID e o UID do processo atual.
No back-end do 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 o 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 dumpsys
binário em todos os serviços
registrados no gerenciador de serviços para despejar as informações deles no
relatório do bug. 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 serviço, substitua o método dump
no objeto do servidor como se você estivesse implementando qualquer outro método IPC
definido em um arquivo AIDL. Ao fazer isso, restrinja o despejo para a permissão
android.permission.DUMP
do app ou restrinja o despejo para 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 o 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 de vinculação.
Embora o Java ofereça suporte a WeakReference
, ele não oferece suporte a referências de binder fracas
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 Rust, use WpIBinder
ou Weak<IFoo>
:
let weak_interface = myIface.downgrade();
let weak_binder = myIface.as_binder().downgrade();
Receber descritor de interface de forma dinâmica
O descritor de interface identifica o tipo de uma interface. Isso é útil ao depurar ou quando você tem um vinculamento desconhecido.
Em Java, é possível acessar o descritor de interface com um código como este:
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 back-ends do NDK e do Rust não oferecem suporte a esse recurso.
Receber estaticamente o descritor da interface
Às vezes, como ao registrar serviços @VintfStability
, é necessário
saber qual é o descritor de interface estático. Em Java, é possível acessar o
descriptor adicionando um código como este:
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 tipo enumerado
Em back-ends nativos, é possível iterar sobre os valores possíveis que um tipo enumerado pode ter. Devido a considerações de tamanho do código, isso não é compatível com Java.
Para um tipo enumerado MyEnum
definido no 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 threadpool. Na maioria
dos casos de uso, é necessário que seja exatamente um pool de linhas de execução, compartilhado entre todos os back-ends.
A única exceção é quando o código do fornecedor pode carregar outra cópia de libbinder
para se comunicar com /dev/vndbinder
. Como isso está em um nó de vinculação separado, o
pool de threads não é compartilhado.
Para o back-end do Java, o tamanho do pool de linhas de execução só pode aumentar (porque ele 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 threadpools: binder e Tokio.
Isso significa que os apps que usam o Rust assíncrono precisam de considerações especiais,
especialmente quando se trata do uso de join_thread_pool
. Consulte a seção sobre
como registrar 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 o AIDL não aplique restrições com base nas regras de linguagem, o uso
de nomes de campo ou tipo que correspondem a um nome reservado pode resultar em uma falha
de compilação para C++ ou Java. No Rust, o campo ou tipo é renomeado usando a
sintaxe "identificador bruto", acessível usando o prefixo r#
.
Recomendamos que você evite usar nomes reservados nas definições de AIDL sempre que possível para evitar vinculações não ergonômicas ou falhas de compilação.
Se você já tiver nomes reservados nas definições de AIDL, poderá renomear os campos com segurança, mantendo a compatibilidade com o protocolo. Talvez seja necessário atualizar o código para continuar a criação, mas os programas já criados vão continuar a funcionar.
Nomes a evitar: