Esta página explica como o app pode usar a nova funcionalidade do SO ao ser executado em novas versões do SO, preservando a compatibilidade com dispositivos mais antigos.
Por padrão, as referências a APIs do NDK no seu app são referências fortes. O carregador dinâmico do Android vai resolvê-los quando a biblioteca for carregada. Se os símbolos não forem encontrados, o app será encerrado. Isso é contrário ao comportamento do Java, em que uma exceção não é gerada até que a API ausente seja chamada.
Por esse motivo, o NDK impede que você crie referências fortes a
APIs mais recentes do que a minSdkVersion
do app. Isso protege você de
enviar acidentalmente um código que funcionou durante os testes, mas não será carregado
(UnsatisfiedLinkError
será gerado pelo System.loadLibrary()
) em dispositivos
mais antigos. Por outro lado, é mais difícil escrever um código que use APIs
mais recentes do que a minSdkVersion
do app, porque você precisa chamar as APIs usando
dlopen()
e dlsym()
em vez de uma chamada de função normal.
A alternativa para usar referências fortes é usar referências fracas. Uma referência
fraca que não é encontrada quando a biblioteca carregada resulta no endereço de
esse símbolo sendo definido como nullptr
em vez de falhar no carregamento. Elas ainda
não podem ser chamadas com segurança, mas, contanto que os locais de chamada sejam protegidos para evitar a chamada
da API quando ela não está disponível, o restante do código pode ser executado e você pode
chamar a API normalmente sem precisar usar dlopen()
e dlsym()
.
As referências de API fracas não exigem suporte adicional do vinculador dinâmico, portanto, podem ser usadas com qualquer versão do Android.
Como ativar referências de API fracas no build
CMake
Transmita -DANDROID_WEAK_API_DEFS=ON
ao executar o CMake. Se você estiver usando o CMake via
externalNativeBuild
, adicione o seguinte ao build.gradle.kts
(ou ao
equivalente do Groovy, se ainda estiver usando build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
Adicione o seguinte ao arquivo Application.mk
:
APP_WEAK_API_DEFS := true
Se você ainda não tiver um arquivo Application.mk
, crie-o no mesmo
diretório que o arquivo Android.mk
. Outras mudanças no seu arquivo
build.gradle.kts
(ou build.gradle
) não são necessárias para o ndk-build.
Outros sistemas de build
Se você não estiver usando o CMake ou o ndk-build, consulte a documentação do sistema de build para saber se há uma maneira recomendada de ativar esse recurso. Caso seu sistema de build não ofereça suporte nativo a essa opção, ative o recurso transmitindo as flags abaixo durante a compilação:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
O primeiro configura os cabeçalhos do NDK para permitir referências fracas. A segunda transforma o aviso de chamadas de API não seguras em um erro.
Consulte o Guia de mantenedores do sistema de build para mais informações.
Chamadas de API protegidas
Esse recurso não faz chamadas seguras para novas APIs em um passe de mágica. A única coisa que ele faz é adiar um erro de carregamento para um erro de chamada. O benefício é que você pode proteger essa chamada no tempo de execução e fazer a substituição com êxito, seja usando uma implementação alternativa ou notificando o usuário de que esse recurso do app não está disponível no dispositivo ou evitando esse caminho de código por completo.
O Clang pode emitir um aviso (unguarded-availability
) quando você faz uma chamada
não protegida para uma API que não está disponível para o minSdkVersion
do app. Se você
estiver usando o ndk-build ou o arquivo do conjunto de ferramentas do CMake, esse aviso será ativado
automaticamente e promovido a um erro ao ativar esse recurso.
Confira um exemplo de código que faz o uso condicional de uma API sem esse recurso ativado, usando dlopen()
e dlsym()
:
void LogImageDecoderResult(int result) {
void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
dlsym(lib, "AImageDecoder_resultToString")
);
if (func == nullptr) {
LOG(INFO) << "cannot stringify result: " << result;
} else {
LOG(INFO) << func(result);
}
}
A leitura é um pouco confusa, há alguma duplicação de nomes de função (e, se
você estiver escrevendo C, as assinaturas também), ele será criado com sucesso, mas sempre
usará o substituto no momento da execução se você digitar acidentalmente o nome da função
transmitido para dlsym
, e você precisa usar esse padrão para todas as APIs.
Com referências fracas da API, a função acima pode ser reescrita como:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
Por trás das cortinas, __builtin_available(android 31, *)
chama
android_get_device_api_level()
, armazena em cache o resultado e o compara com 31
, que é o nível da API que introduziu AImageDecoder_resultToString()
.
A maneira mais simples de determinar qual valor usar para __builtin_available
é
tentar criar sem o guard (ou um guard de
__builtin_available(android 1, *)
) e fazer o que a mensagem de erro informa.
Por exemplo, uma chamada não protegida para AImageDecoder_createFromAAsset()
com
minSdkVersion 24
vai produzir:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
Nesse caso, a chamada precisa ser protegida por __builtin_available(android 30, *)
.
Se não houver um erro de build, a API estará sempre disponível para o
minSdkVersion
e nenhum guard será necessário, ou o build está configurado incorretamente e o
aviso unguarded-availability
está desativado.
Como alternativa, a referência da API NDK vai mostrar algo como "Introduzido na API 30" para cada API. Se esse texto não estiver presente, significa que a API está disponível para todos os níveis de API com suporte.
Como evitar a repetição de guards de API
Se você estiver usando isso, provavelmente terá seções de código no app que
só podem ser usadas em dispositivos novos. Em vez de repetir a
verificação __builtin_available()
em cada uma das funções, é possível anotar seu
próprio código para exigir um determinado nível da API. Por exemplo, as APIs ImageDecoder
foram adicionadas na API 30. Portanto, para funções que usam muito essas
APIs, faça o seguinte:
#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)
void DecodeImageWithImageDecoder() REQUIRES_API(30) {
// Call any APIs that were introduced in API 30 or newer without guards.
}
void DecodeImageFallback() {
// Pay the overhead to call the Java APIs via JNI, or use third-party image
// decoding libraries.
}
void DecodeImage() {
if (API_AT_LEAST(30)) {
DecodeImageWithImageDecoder();
} else {
DecodeImageFallback();
}
}
Peculiaridades dos guards de API
O Clang é muito específico sobre como o __builtin_available
é usado. Somente um if (__builtin_available(...))
literal
(possivelmente substituído por macro) funciona. Mesmo
operações triviais, como if (!__builtin_available(...))
, não vão funcionar. O Clang
emitirá o aviso unsupported-availability-guard
, bem como
unguarded-availability
. Isso pode melhorar em uma versão futura do Clang. Consulte o
Problema 33161 do LLVM (link em inglês) para mais informações.
As verificações de unguarded-availability
só se aplicam ao escopo da função em que
são usados. O Clang vai emitir o aviso mesmo que a função com a chamada de API seja
chamada apenas em um escopo protegido. Para evitar a repetição de guards no
seu código, consulte Evitar a repetição de guards de API.
Por que isso não é o padrão?
A menos que seja usada corretamente, a diferença entre referências de API fortes e fracas é que a primeira falha rapidamente e obviamente, enquanto a segunda não falha até que o usuário realize uma ação que faça com que a API ausente seja chamada. Quando isso acontece, a mensagem de erro não é um erro claro de tempo de compilação "AFoo_bar() não está disponível", é um erro de segfault. Com referências fortes, a mensagem de erro fica muito mais clara, e a falha rápida é um padrão mais seguro.
Como esse é um recurso novo, pouquíssimo código é criado para processar esse comportamento com segurança. O código de terceiros que não foi escrito pensando no Android provavelmente sempre terá esse problema. Portanto, não há planos para que o comportamento padrão mude.
Recomendamos o uso desse recurso, mas, como ele torna os problemas mais difíceis de detectar e depurar, aceite esses riscos intencionalmente, em vez de o comportamento mudar sem seu conhecimento.
Avisos
Esse recurso funciona para a maioria das APIs, mas há alguns casos em que ele não funciona.
As APIs libc mais recentes são as menos propensas a problemas. Ao contrário do restante das
APIs do Android, elas são protegidas com #if __ANDROID_API__ >= X
nos cabeçalhos
e não apenas __INTRODUCED_IN(X)
, o que impede que até mesmo a declaração fraca seja
vista. Como o NDK moderno com suporte ao nível de API mais antigo é o r21, as APIs libc
mais usadas já estão disponíveis. Novas APIs libc são adicionadas a cada
versão (consulte status.md), mas quanto mais recentes elas forem, maior será a probabilidade de
ser um caso extremo que poucos desenvolvedores vão precisar. No entanto, se você for um desses
desenvolvedores, por enquanto vai precisar continuar usando dlsym()
para chamar essas
APIs se o minSdkVersion
for mais antigo que a API. Esse é um problema que pode ser resolvido,
mas isso envolve o risco de interromper a compatibilidade de origem de todos os apps. Qualquer
código que contenha polyfills de APIs libc não será compilado devido aos
atributos availability
incompatíveis nas declarações libc e locais. Portanto,
não temos certeza se ou quando vamos corrigir isso.
Mais desenvolvedores provavelmente encontrarão quando a biblioteca que
contém a nova API for mais recente do que seu minSdkVersion
. Esse recurso só
permite referências de símbolo fraco. Não existe uma referência de biblioteca
fraca. Por exemplo, se o minSdkVersion
for 24, você poderá vincular
libvulkan.so
e fazer uma chamada protegida para vkBindBufferMemory2
, porque
libvulkan.so
está disponível em dispositivos a partir da API 24. Por outro lado,
se a minSdkVersion
era 23, use dlopen
e dlsym
,
porque a biblioteca não vai existir no dispositivo em dispositivos com suporte apenas
à API 23. Não sabemos de uma boa solução para corrigir esse caso, mas, a longo
prazo, ele será resolvido porque não permitimos mais que novas
APIs criem novas bibliotecas (sempre que possível).
Para autores de bibliotecas
Se você estiver desenvolvendo uma biblioteca para ser usada em apps Android, evite
usar esse recurso nos cabeçalhos públicos. Ele pode ser usado com segurança em
códigos fora de linha, mas se você depende de __builtin_available
em qualquer código nos
cabeçalhos, como funções in-line ou definições de modelo, força todos os
consumidores a ativar esse recurso. Pelos mesmos motivos que não ativamos esse
recurso por padrão no NDK, evite fazer essa escolha em nome
dos seus consumidores.
Se você exigir esse comportamento nos cabeçalhos públicos, documente isso para que os usuários saibam que precisam ativar o recurso e estão cientes dos riscos.