Usar APIs mais recentes

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.