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 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.