Usar APIs mais recentes

Esta página explica como seu aplicativo pode usar as novas funcionalidades do sistema operacional ao ser executado em 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 carregado. Se os símbolos não forem encontrados, o app será cancelado. Isso é contrário à como o Java se comporta, em que uma exceção não será lançada até que a API ausente seja chamou.

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 envio acidental de código que funcionou durante o teste, mas não foi carregado (UnsatisfiedLinkError será gerado de System.loadLibrary()) em versões mais antigas dispositivos. Por outro lado, é mais difícil escrever códigos que usem APIs mais recente do que a minSdkVersion do app, porque é necessário 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. Um ponto fraco referência que não é encontrada quando a biblioteca carregou resultados no endereço de o 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 impedir chamadas a API quando ela não estiver disponível, o restante do código poderá ser executado, chame a API normalmente, sem precisar usar dlopen() e dlsym().

As referências fracas da API não exigem suporte adicional do vinculador dinâmico. para que possam ser usados com 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 seu build.gradle.kts (ou ao Equivalente no estilo 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 seu arquivo Application.mk:

APP_WEAK_API_DEFS := true

Se você ainda não tiver um arquivo Application.mk, crie-o no mesmo como o arquivo Android.mk. Outras mudanças nas build.gradle.kts (ou build.gradle) não são necessários 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 seu build para verificar se há uma maneira recomendada de ativar esse recurso. Se o build não oferece suporte nativo a essa opção, você pode ativar o recurso passando as seguintes sinalizações na 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 curva 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 é adiar um erro de tempo de carregamento para um erro de tempo de chamada. A vantagem é que você pode proteger essa chamada no tempo de execução e retornar adequadamente, seja usando uma implementação alternativa ou notificar o usuário de que esse recurso do aplicativo não está disponível no dispositivo ou evitando esse caminho de código.

O Clang pode emitir um aviso (unguarded-availability) quando você fizer uma chamada 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á automaticamente ativado e promovido para um erro ao ativar este recurso.

Aqui está um exemplo de código que faz uso condicional de uma API sem esse recurso foi 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á alguns nomes de funções duplicados (e, se estiver escrevendo C, as assinaturas também), ele será criado com êxito, mas sempre usar o substituto no ambiente de execução se você digitar acidentalmente o nome da função transmitida. como dlsym, e você precisa usar esse padrão para cada API.

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, o __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 construir sem a guarda (ou uma guarda de __builtin_available(android 1, *)) e faça o que a mensagem de erro diz. Por exemplo, uma chamada sem segurança 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 erro de compilação, significa que a API estará sempre disponível para seu minSdkVersion e nenhuma proteção é necessária ou seu build está configurado incorretamente e o O aviso unguarded-availability está desativado.

Como alternativa, a referência da API NDK dirá algo ao longo das linhas de "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.

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

Se estiver usando isso, você provavelmente terá seções de código em seu aplicativo que que só podem ser usadas em dispositivos novos o suficiente. Em vez de repetir __builtin_available() em cada função, é possível fazer anotações que exigem um determinado nível de API. Por exemplo, as APIs ImageDecoder foram adicionados na API 30, portanto, para funções que fazem uso intenso desses 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 específico sobre como o __builtin_available é usado. Apenas um literal (embora possivelmente substituído por macro) if (__builtin_available(...)) funciona. Uniforme operações triviais como if (!__builtin_available(...)) não vão funcionar (Clang) emitirá o aviso unsupported-availability-guard, assim como unguarded-availability). Isso pode melhorar em uma versão futura do Clang. Consulte 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 usadas. O Clang emitirá o aviso mesmo que a função com a chamada de API seja só pode ser chamado de dentro de um escopo protegido. Para evitar a repetição de guardas 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 usada corretamente, a diferença entre referências fortes e APIs fracas de referência é que a primeira vai falhar rápida e obviamente, enquanto O segundo método não falhará até que o usuário execute uma ação que cause a ausência da API que será chamado. Quando isso acontece, a mensagem de erro não é clara. Tempo de compilação: "AFoo_bar() não está disponível" será uma falha de segmentação. Com referências fortes, a mensagem de erro é muito mais clara e a falha rápido é uma mais seguro.

Como esse é um recurso novo, pouquíssimos códigos são criados para lidar esse comportamento com segurança. Código de terceiros que não foi criado pensando no Android provavelmente sempre terão esse problema. Por isso, não há planos para a comportamento padrão a mudar.

Recomendamos o uso desse recurso, mas, como ele cria mais problemas, difíceis de detectar e depurar, você deve aceitar esses riscos intencionalmente, em vez do que a mudança de comportamento sem seu conhecimento.

Avisos

Esse recurso funciona para a maioria das APIs, mas há alguns casos em que não funcionam.

As APIs da libc mais recentes têm menos probabilidade de serem problemáticas. Ao contrário do restante APIs do Android, elas são protegidas por #if __ANDROID_API__ >= X nos cabeçalhos. e não apenas __INTRODUCED_IN(X), o que impede até mesmo a declaração fraca de ser vistos. Como o suporte a NDKs modernos de nível mais antigo da API é r21, a as APIs da libc comumente necessárias já estão disponíveis. Novas APIs libc são adicionadas a cada (consulte status.md), mas quanto mais recentes elas forem, maior será a probabilidade pode ser um caso extremo que poucos desenvolvedores vão precisar. Dito isso, se você é um dos com esses desenvolvedores. Por enquanto, você vai precisar continuar usando dlsym() para chamá-los as 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 de todos os apps (qualquer código que contém polyfills de APIs libc não será compilada devido ao atributos availability incompatíveis nas declarações locais e de libc), então não temos certeza se ou quando o corrigiremos.

O caso que mais desenvolvedores provavelmente encontrarão é quando a biblioteca que contém a nova API é mais recente do que seu minSdkVersion. Somente este recurso ativa referências fracas de símbolos; não existe uma biblioteca fraca de referência. Por exemplo, se a minSdkVersion for 24, você poderá vincular libvulkan.so e fazer uma chamada protegida para vkBindBufferMemory2, porque O libvulkan.so está disponível em dispositivos a partir do nível 24 da API. Por outro lado, se minSdkVersion era 23, você precisa voltar para dlopen e dlsym porque a biblioteca não existe no dispositivo em dispositivos com suporte API 23. Não sabemos uma boa solução para corrigir este caso, mas, em longo ele se resolverá sozinho, pois (sempre que possível) não permitimos mais APIs para criar novas bibliotecas.

Para autores de bibliotecas

Se você estiver desenvolvendo uma biblioteca para ser usada em aplicativos Android, recomendamos evite usar esse recurso em cabeçalhos públicos. Ele pode ser usado com segurança em código fora de linha, mas se você depende de __builtin_available em qualquer código da cabeçalhos, como funções inline ou definições de modelo, você força todas as para ativar esse recurso. Pelos mesmos motivos que não ativamos essa por padrão no NDK, evite fazer essa escolha em nome de seus clientes.

Se você exigir esse comportamento em seus cabeçalhos públicos, documente para que os usuários saibam que precisam ativar o recurso e cientes dos riscos de fazer isso.