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 a criação de referências fortes para
APIs mais recentes do que o 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 código que use APIs
mais recentes do que o minSdkVersion
do app, porque é necessário 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 pelo
externalNativeBuild
, adicione o seguinte ao build.gradle.kts
(ou o
equivalente do Groovy, se você 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 do arquivo Android.mk
. Outras mudanças no 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. Se o sistema de build não oferecer suporte a essa opção de forma nativa, ative o recurso transmitindo as seguintes flags ao compilar:
-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 torna as chamadas para novas APIs seguras. 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 uso condicional de uma API sem
ativar esse recurso, 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 de API fracas, 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 suas funções, você pode anotar seu
próprio código como exigindo um determinado nível de 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
se aplicam apenas ao escopo da função em que são
usadas. 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 novo recurso, muito pouco código existente foi escrito 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 dessa abordagem, mas, como ela dificulta a detecção e a depuração de problemas, é necessário aceitar esses riscos conscientemente, em vez de permitir que o comportamento mude 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.
O caso mais comum é quando a biblioteca que
contém a nova API é mais recente que o 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 o minSdkVersion
for 23, você precisará usar dlopen
e dlsym
porque a biblioteca não vai existir em dispositivos que só oferecem suporte à
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 off-line, mas se você usar __builtin_available
em qualquer código nos
cabeçalhos, como funções inline ou definições de modelo, você forçará todos os
consumidores a ativar esse recurso. Pelo mesmo motivo de não ativar esse
recurso por padrão no NDK, evite fazer essa escolha em nome
dos 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.