Usar APIs mais recentes

Esta página explica como seu app pode usar as novas funcionalidades do SO ao ser executado em novas versões do SO, preservando a compatibilidade com dispositivos mais antigos.

Por padrão, as referências às APIs do NDK no seu app são referências fortes. O carregador dinâmico do Android os resolverá prontamente quando a biblioteca for carregada. Se os símbolos não forem encontrados, o app será cancelado. Isso vai contra a forma como o Java se comporta, em que uma exceção não será 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ê contra o envio acidental de código que funcionou durante o teste, mas não será carregado (UnsatisfiedLinkError será gerado de 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 ao uso de referências fortes é usar referências fracas. Uma referência fraca que não é encontrada quando a biblioteca carregada faz com que o endereço desse símbolo seja definido como nullptr em vez de falhar no carregamento. Eles ainda não podem ser chamados com segurança, mas, contanto que os sites de chamadas sejam protegidos para evitar a chamada à API quando ela não está disponível, o restante do seu código pode ser executado, e você pode chamar a API normalmente sem precisar usar dlopen() e dlsym().

As referências fracas de API não exigem suporte adicional do vinculador dinâmico. Portanto, elas podem ser usadas em qualquer versão do Android.

Como ativar referências fracas da API 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 seu 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 usa o CMake ou o ndk-build, consulte a documentação do sistema de build para conferir se há uma maneira recomendada de ativar esse recurso. Caso seu sistema de build não ofereça suporte a essa opção de forma nativa, 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 de sistema de compilação 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 tempo de carregamento para um erro de tempo de chamada. A vantagem é que você pode proteger essa chamada no momento da execução e retornar sem problemas, 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 completamente.

O Clang pode emitir um aviso (unguarded-availability) quando você faz uma chamada desprotegida para uma API que não está disponível para o minSdkVersion do app. Se você estiver usando o ndk-build ou nosso arquivo do conjunto de ferramentas do CMake, esse aviso será ativado automaticamente e promovido a erro ao ativar o 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á algumas duplicações de nomes de função (e, se você estiver escrevendo C, as assinaturas também), ela será criada, mas sempre vai usar o substituto no ambiente de execução se você digitar acidentalmente o nome da função transmitida para dlsym e precisar 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;
    }
}

Internamente, __builtin_available(android 31, *) chama android_get_device_api_level(), armazena o resultado em cache 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 a proteção (ou uma proteção de __builtin_available(android 1, *)) e fazer o que a mensagem de erro diz. Por exemplo, uma chamada sem proteção para AImageDecoder_createFromAAsset() com minSdkVersion 24 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 erro de build, isso significa que a API está sempre disponível para seu minSdkVersion e nenhuma proteção é necessária ou que seu build está configurado incorretamente e o aviso unguarded-availability está desativado.

Como alternativa, a referência da API NDK vai mostrar algo nas linhas de "Introduzido no nível 30 da API" para cada API. Se esse texto não estiver presente, significa que a API está disponível para todos os níveis de API compatíveis.

Evitar a repetição de proteções da API

Se você estiver usando esse recurso, provavelmente terá seções de código no seu app que só poderão ser usadas em dispositivos novos o suficiente. 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 próprias APIs ImageDecoder foram adicionadas na API 30. Portanto, para funções que fazem uso intenso dessas APIs, você pode fazer algo como:

#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();
    }
}

Características dos guardas de API

O Clang é muito particular sobre o uso do __builtin_available. Apenas um if (__builtin_available(...)) literal (embora possivelmente substituído por macro) funciona. Mesmo operações triviais como if (!__builtin_available(...)) não vão funcionar (o Clang emite o aviso unsupported-availability-guard e unguarded-availability). Isso pode melhorar em uma versão futura do Clang. Consulte o problema 33161 do LLVM (link em inglês) para saber mais.

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 se a função com a chamada de API for chamada apenas de dentro de um escopo protegido. Para evitar a repetição de proteções no seu próprio código, consulte Como evitar a repetição de proteções de API.

Por que esse não é o padrão?

A menos que sejam usadas corretamente, a diferença entre referências de API fortes e referências fracas é que a primeira falha rápida e obviamente, enquanto a última não falhará até que o usuário execute uma ação que faça com que a API ausente seja chamada. Quando isso acontecer, a mensagem de erro não será um erro claro "AFoo_bar() is not available" no momento da compilação, será uma falha de segmentação. Com referências fortes, a mensagem de erro é muito mais clara, e fail-fast é um padrão mais seguro.

Como esse é um recurso novo, há muito pouco 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 vai ter esse problema. Por isso, não há planos para o comportamento padrão mudar no momento.

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 na maioria das APIs, mas em alguns casos não funciona.

As APIs da libc mais recentes têm menos probabilidade de serem problemáticas. 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 suporte a NDKs modernos de nível mais antigo da API é r21, as APIs de libc mais necessárias já estão disponíveis. Novas APIs da libc são adicionadas a cada versão (consulte status.md). No entanto, quanto mais recentes elas forem, maior será a probabilidade de serem um caso extremo que poucos desenvolvedores precisarão. Dito isso, se você for um desses desenvolvedores, por enquanto, vai precisar continuar usando dlsym() para chamar essas APIs se a minSdkVersion for mais antiga que a API. Esse é um problema solucionável, mas isso traz o risco de corromper a compatibilidade de origem para todos os apps (qualquer código que contenha polyfills de APIs da libc não seja compilado devido à incompatibilidade de atributos availability nas declarações locais e libc). Por isso, não temos certeza se ou quando isso será corrigido.

Mais desenvolvedores provavelmente encontrarão quando a biblioteca que contém a nova API for mais recente do que seu minSdkVersion. Esse recurso ativa apenas referências fracas de símbolos. Não existe uma referência de biblioteca fraca. Por exemplo, se a minSdkVersion for 24, você poderá vincular libvulkan.so e fazer uma chamada protegida para vkBindBufferMemory2, já que libvulkan.so está disponível em dispositivos com a API 24 em diante. 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 qual é uma boa solução para corrigir esse caso, mas isso será resolvido em longo prazo, porque (sempre que possível) não permitimos mais que novas APIs criem novas bibliotecas.

Para autores de bibliotecas

Se você estiver desenvolvendo uma biblioteca para ser usada em aplicativos Android, evite usar esse recurso em 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-o para que os usuários saibam que precisarão ativar o recurso e estejam cientes dos riscos de fazer isso.