Dicas de JNI

O Java Native Interface (JNI), define um caminho para o bytecode que o Android compila a partir do código gerenciado (escrito nas linguagens de programação Java ou Kotlin) para interagir com o código nativo (escrito em C/C++). O JNI é independente de fornecedores, permite o carregamento de código de bibliotecas compartilhadas dinâmicas e, embora às vezes seja pesado, é razoavelmente eficiente.

Observação: como o Android compila Kotlin para bytecode compatível com ART de uma maneira semelhante à linguagem de programação Java, você pode aplicar as orientações desta página às linguagens de programação Kotlin e Java em termos de arquitetura JNI e custos associados. Para saber mais, consulte Kotlin e Android.

Se você ainda não está familiarizado com ele, leia a Especificação da interface nativa do Java (link em inglês) para ter uma noção de como o JNI funciona e quais recursos estão disponíveis. Alguns aspectos da interface não são imediatamente óbvios na primeira leitura, então você pode achar as próximas seções úteis.

Para procurar referências JNI globais e ver onde essas referências são criadas e excluídas, use a visualização de heap JNI no Memory Profiler no Android Studio 3.2 e versões posteriores.

Dicas gerais

Tente minimizar a pegada da sua camada de JNI. Existem várias dimensões a serem consideradas aqui. Sua solução JNI precisa tentar seguir estas diretrizes (listadas abaixo por ordem de importância, começando com as mais importantes):

  • Minimize o gerenciamento de recursos na camada de JNI. O gerenciamento por meio da camada de JNI tem custos nada triviais. Tente criar uma interface que minimize a quantidade de dados que você precisa para gerenciar e a frequência com que os dados precisam ser gerenciados.
  • Quando possível, evite comunicação assíncrona entre código escrito em uma linguagem de programação gerenciada e código escrito em C++. Isso fará com que sua interface JNI seja mais fácil de manter. Normalmente, você pode simplificar as atualizações da IU assíncrona mantendo a atualização assíncrona na mesma linguagem da IU. Por exemplo, em vez de invocar uma função C++ da linha de execução de IU no código Java via JNI, é melhor fazer um callback entre duas linhas de execução na linguagem de programação Java, com um deles fazendo uma chamada C++ de bloqueio e depois notificando a linha de execução de IU quando a chamada de bloqueio for concluída.
  • Minimize o número de segmentos que precisam tocar ou ser tocados pelo JNI. Se você precisar utilizar pools de linhas de execução nas linguagens Java e C++, tente manter a comunicação JNI entre os proprietários do pool, e não entre linhas de execução de workers individuais.
  • Mantenha seu código de interface em um número baixo de locais de origem C++ e Java facilmente identificados para facilitar futuras refatorações. Conforme apropriado, considere usar uma biblioteca de geração automática de JNI.

JavaVM e JNIEnv

O JNI define duas estruturas de dados principais: "JavaVM" e "JNIEnv". Ambos são essencialmente ponteiros para tabelas de função. Na versão C++, são classes com um ponteiro para uma tabela de função e uma função de membro para cada função JNI que faz referência indireta através da tabela. O JavaVM fornece as funções de "interface de invocação", que permitem criar e destruir um JavaVM. Teoricamente, você pode ter vários JavaVMs por processo, mas o Android permite apenas um.

O JNIEnv fornece a maioria das funções JNI. Todas as suas funções nativas recebem um JNIEnv como o primeiro argumento.

O JNIEnv é usado para armazenamento local de linhas de execução. Por esse motivo, você não pode compartilhar um JNIEnv entre linhas de execução. Se uma parte do código não tiver outra maneira de conseguir seu JNIEnv, você precisará compartilhar o JavaVM e usar o GetEnv para descobrir o JNIEnv da linha de execução. Supondo que tenha um, veja o AttachCurrentThread abaixo.

As declarações C do JNIEnv e JavaVM são diferentes das declarações C++. O arquivo de inclusão "jni.h" fornece diferentes typedefs, dependendo se está incluído em C ou C++. Por essa razão, é uma má ideia incluir argumentos JNIEnv em arquivos de cabeçalho adicionados por ambas as linguagens. Dito de outra forma: se o arquivo de cabeçalho requer #ifdef __cplusplus, você pode ter que fazer algum trabalho extra se alguma coisa nesse cabeçalho se referir ao JNIEnv.

Linhas de execução

Todas as linhas de execução são do Linux, agendadas pelo kernel. Elas geralmente são iniciadas a partir do código gerenciado (usando Thread.start), mas também podem ser criadas em outro lugar e, em seguida, anexadas ao JavaVM. Por exemplo, uma linha de execução iniciada com pthread_create pode ser anexada às funções de JNI AttachCurrentThread ou AttachCurrentThreadAsDaemon. Até que uma linha de execução seja anexada, ela não possui um JNIEnv e não pode fazer chamadas JNI.

Anexar uma linha de execução criada nativamente faz com que um objeto java.lang.Thread seja criado e adicionado ao ThreadGroup "principal", tornando-o visível para o depurador. Chamar AttachCurrentThread em uma linha de execução já anexada é um ambiente autônomo.

O Android não suspende linhas de execução que executam código nativo. Se a coleta de lixo estiver em andamento ou o depurador tiver emitido uma solicitação de suspensão, o Android pausará a linha de execução na próxima vez que fizer uma chamada JNI.

Linhas de execução anexadas por meio de JNI precisam chamar DetachCurrentThread antes de saírem. Se codificar isso diretamente for estranho, no Android 2.0 (Eclair) e versões posteriores, você pode usar pthread_key_create para definir uma função de destruição que será chamada antes da saída da linha de execução e chamar DetachCurrentThread de lá. Use essa chave com pthread_setspecific para armazenar o JNIEnv no armazenamento local da linha de execução. Dessa forma, ele será passado para seu destrutor como o argumento.

jclass, jmethodID e jfieldID

Se você quiser acessar o campo de um objeto do código nativo, faça o seguinte:

  • Consiga a referência de objeto de classe para a classe com FindClass.
  • Consiga o ID do campo para o campo com GetFieldID.
  • Consiga o conteúdo do campo com algo apropriado, como GetIntField.

Da mesma forma, para chamar um método, você precisa primeiro conseguir uma referência de objeto de classe e, em seguida, um ID de método. Os IDs geralmente são apenas ponteiros para estruturas internas de dados de ambiente de execução. Pesquisá-los pode exigir várias comparações de strings, mas uma vez que você os tenha, a chamada para conseguir o campo ou invocar o método é muito rápida.

Se o desempenho for importante, recomendamos procurar os valores uma vez e armazenar em cache os resultados no seu código nativo. Como há um limite de um JavaVM por processo, é razoável armazenar esses dados em uma estrutura local estática.

As referências de classe, IDs de campo e IDs de método são válidos até que a classe seja descarregada. As classes só são descarregadas se todas as classes associadas a um ClassLoader puderem ser coletadas como lixo, o que é raro, mas não será impossível no Android. Observe, entretanto, que o jclass é uma referência de classe e precisa ser protegido com uma chamada para NewGlobalRef (veja a próxima seção).

Se você quiser armazenar os IDs em cache quando uma classe for carregada e armazená-los novamente em cache de forma automática se a classe for descarregada e recarregada, a maneira correta de inicializar os IDs é adicionando à classe apropriada uma parte do código que se pareça com isto:

Kotlin

    companion object {
        /*
         * We use a static class initializer to allow the native code to cache some
         * field offsets. This native function looks up and caches interesting
         * class/field/method IDs. Throws on failure.
         */
        private external fun nativeInit()

        init {
            nativeInit()
        }
    }
    

Java

        /*
         * We use a class initializer to allow the native code to cache some
         * field offsets. This native function looks up and caches interesting
         * class/field/method IDs. Throws on failure.
         */
        private static native void nativeInit();

        static {
            nativeInit();
        }
    

Crie um método nativeClassInit no seu código C/C++ que executa as pesquisas de ID. O código será executado uma vez, quando a classe for inicializada. Se a classe for descarregada e depois recarregada, ela será executada novamente.

Referências locais e globais

Todo argumento passado para um método nativo, e quase todo objeto retornado por uma função JNI é uma "referência local". Isso significa que ele é válido pela duração do método nativo atual na linha de execução atual. Mesmo que o objeto em si continue ativo depois que o método nativo for retornado, a referência não será válida.

Isso se aplica a todas as subclasses de jobject, incluindo jclass, jstring e jarray. O ambiente de execução avisará sobre a maioria dos usos indevidos de referência quando as verificações de JNI estendidas estiverem habilitadas.

A única maneira de conseguir referências não locais é por meio das funções NewGlobalRef e NewWeakGlobalRef.

Se você quiser manter uma referência por um período mais longo, use uma referência "global". A função NewGlobalRef usa a referência local como argumento e retorna uma referência global. A referência global é garantida como válida até você chamar DeleteGlobalRef.

Esse padrão costuma ser usado ao armazenar em cache uma jclass retornada de FindClass, por exemplo:

jclass localClass = env->FindClass("MyClass");
    jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

Todos os métodos JNI aceitam referências locais e globais como argumentos. É possível que referências ao mesmo objeto tenham valores diferentes. Por exemplo, os valores de retorno de chamadas consecutivas para NewGlobalRef no mesmo objeto podem ser diferentes. Para ver se duas referências se referem ao mesmo objeto, você precisa usar a função IsSameObject. Nunca compare referências com == em código nativo.

Uma consequência disso é que você não deve presumir que as referências de objeto sejam constantes ou exclusivas no código nativo. O valor de 32 bits que representa um objeto pode ser diferente de uma invocação de um método para o próximo, e é possível que dois objetos diferentes tenham o mesmo valor de 32 bits em chamadas consecutivas. Não use valores de jobject como chaves.

É necessário que os programadores "não aloquem excessivamente" referências locais. Em termos práticos, isso significa que se você estiver criando um grande número de referências locais, talvez ao executar uma matriz de objetos, você precisa liberá-los manualmente com DeleteLocalRef em vez de permitir que a JNI faça isso por você. A implementação só é obrigatória para reservar slots para 16 referências locais; então, se precisar de mais do que isso, exclua conforme avança ou utilize EnsureLocalCapacity/PushLocalFrame para reservar mais.

Observe que jfieldIDs e jmethodIDs são tipos opacos, não referências a objetos, e não devem ser passados para NewGlobalRef. Os ponteiros de dados brutos retornados por funções como GetStringUTFChars e GetByteArrayElements também não são objetos. Eles podem ser transmitidos entre as linhas de execução e são válidos até a chamada de liberação correspondente.

Um caso incomum merece menção separada. Se você anexar uma linha de execução nativa com AttachCurrentThread, o código que você está executando nunca liberará automaticamente as referências locais até que a linha de execução seja desconectada. Todas as referências locais que você criar precisarão ser excluídas manualmente. No geral, qualquer código nativo que crie referências locais em um loop provavelmente precisará fazer alguma exclusão manual.

Tenha cuidado ao usar referências globais. Elas podem ser inevitáveis, mas têm depuração árdua e podem causar comportamentos de memória difíceis de diagnosticar. Se todo o restante for igual, uma solução com menos referências globais provavelmente será melhor.

Strings UTF-8 e UTF-16

A linguagem de programação Java usa o UTF-16. Por conveniência, o JNI também fornece métodos que funcionam com UTF-8 modificado. A codificação modificada é útil para código C porque codifica \u0000 como 0xc0 0x80 em vez de 0x00. O bom disso é que você pode contar com strings com terminação zero no estilo C, adequadas para uso com funções de string da libc padrão. O lado negativo é que você não pode passar dados arbitrários UTF-8 para o JNI e esperar que ele funcione corretamente.

Se possível, geralmente é mais rápido operar com strings UTF-16. Atualmente, o Android não exige uma cópia em GetStringChars, enquanto que GetStringUTFChars requer uma alocação e uma conversão para UTF-8. Observe que as strings UTF-16 não são terminadas com zero, e \u0000 é permitido, então você precisa manter o tamanho da string, assim como o ponteiro jchar.

Não se esqueça de liberar (Release) as cadeias que você usar (Get). As funções de string retornam jchar* ou jbyte*, que são ponteiros no estilo C para dados primitivos em vez de referências locais. Eles são garantidos como válidos até que Release seja chamado, o que significa que eles não são liberados quando o método nativo é retornado.

Os dados passados para o NewStringUTF precisam estar no formato UTF-8 modificado. Um erro comum é ler dados de caracteres de um arquivo ou fluxo de rede e entregá-los a NewStringUTF sem filtrá-los. A menos que você saiba que os dados são MUTF-8 válidos (ou ASCII de 7 bits, que é um subconjunto compatível), é necessário remover os caracteres inválidos ou convertê-los para o formato UTF-8 modificado. Caso contrário, a conversão UTF-16 provavelmente fornecerá resultados inesperados. CheckJNI, que está ativado por padrão para emuladores, verifica strings e cancela a VM se receber uma entrada inválida.

Matrizes primitivas

O JNI oferece funções para acessar o conteúdo de objetos de matriz. Enquanto matrizes de objetos precisam ser acessadas uma entrada por vez, matrizes de primitivos podem ser lidas e escritas diretamente como se fossem declaradas em C.

Para tornar a interface o mais eficiente possível sem restringir a implementação da VM, a família de chamadas Get<PrimitiveType>ArrayElements permite que o ambiente de execução retorne um ponteiro para os elementos reais ou aloque alguma memória e faça uma cópia. De qualquer forma, o ponteiro bruto retornado tem a garantia de ser válido até que a chamada de Release correspondente seja emitida (o que implica que, se os dados não foram copiados, o objeto de matriz será fixado e não poderá ser realocado como parte da compactação do heap). Você precisa liberar (Release) todas as matrizes que usar (Get). Além disso, se a chamada Get falhar, você precisa garantir que seu código não tente liberar (Release) um ponteiro NULL posteriormente.

Você pode determinar se os dados foram ou não copiados passando um ponteiro não NULL para o argumento isCopy. Isso raramente é útil.

A chamada Release usa um argumento de mode, que pode ter um de três valores. As ações executadas pelo ambiente de execução dependem se ele retornou um ponteiro para os dados reais ou uma cópia dele:

  • 0
    • Real: o objeto da matriz é liberado.
    • Cópia: os dados são copiados de volta. O buffer com a cópia é liberado.
  • JNI_COMMIT
    • Real: não faz nada.
    • Cópia: os dados são copiados de volta. O buffer com a cópia não é liberado.
  • JNI_ABORT
    • Real: o objeto da matriz é liberado. As gravações anteriores não são canceladas.
    • Cópia: o buffer com a cópia é liberado, e todas as alterações nele são perdidas.

Um motivo para verificar o sinalizador isCopy é saber se você precisa chamar Release com JNI_COMMIT depois de fazer alterações em uma matriz. Se estiver alternando entre fazer alterações e executar o código que usa o conteúdo da matriz, talvez seja possível ignorar a confirmação de ambiente autônomo. Outro motivo possível para verificar o sinalizador é o manuseio eficiente de JNI_ABORT. Por exemplo, você pode querer conseguir uma matriz, modificá-la no lugar, passar as peças para outras funções e descartar as alterações. Se você sabe que o JNI está fazendo uma nova cópia para você, não é necessário criar outra cópia "editável". Se o JNI estiver passando o original, você precisará fazer sua própria cópia.

É um erro comum (repetido no código de exemplo) presumir que você pode ignorar a chamada Release se *isCopy for falso. Esse não é o caso. Se nenhum buffer de cópia foi alocado, então a memória original precisa ser fixada, e não pode ser movida pelo coletor de lixo.

Observe também que o sinalizador JNI_COMMIT não libera a matriz, e você precisará chamar Release novamente com um sinalizador diferente.

Chamadas de região

Há uma alternativa para chamadas como Get<Type>ArrayElements e GetStringChars que pode ser muito útil quando você quer apenas copiar dados. Recomendamos o seguinte:

    jbyte* data = env->GetByteArrayElements(array, NULL);
        if (data != NULL) {
            memcpy(buffer, data, len);
            env->ReleaseByteArrayElements(array, data, JNI_ABORT);
        }

Isso pega a matriz, copia os primeiros elementos de byte len e, em seguida, libera a matriz. Dependendo da implementação, a chamada Get fixará ou copiará o conteúdo da matriz. O código copia os dados (talvez uma segunda vez) e, em seguida, chama Release. Nesse caso, o JNI_ABORT garante que não possa haver uma terceira cópia.

O mesmo resultado pode ser alcançado de uma maneira mais simples:

    env->GetByteArrayRegion(array, 0, len, buffer);

Isso tem muitas vantagens:

  • Requer uma chamada JNI em vez de duas, reduzindo a sobrecarga.
  • Não requer fixação ou cópias de dados extras.
  • Reduz o risco de erro do programador. Não há risco de esquecer de chamar Release depois que algo falhar.

Da mesma forma, você pode usar a chamada Set<Type>ArrayRegion para copiar dados para uma matriz, e GetStringRegion ou GetStringUTFRegion para copiar caracteres de uma String.

Exceções

Você não deve chamar a maioria das funções JNI enquanto uma exceção estiver pendente. Espera-se que seu código observe a exceção (por meio do valor de retorno da função, ExceptionCheck ou ExceptionOccurred) e retorne, ou limpe a exceção e processe-a.

As únicas funções JNI que você pode chamar enquanto uma exceção está pendente são:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

Muitas chamadas JNI podem acionar uma exceção, mas geralmente oferecem uma maneira mais simples de verificar falhas. Por exemplo, se NewString retornar um valor não NULL, você não precisará verificar uma exceção. No entanto, se chamar um método (usando uma função como CallObjectMethod), você sempre precisará verificar uma exceção, porque o valor de retorno não será válido se uma exceção tiver sido acionada.

Observe que exceções acionadas por código interpretado não liberam frames de pilha nativos, e o Android ainda não é compatível com exceções do C++. As instruções JNI Throw e ThrowNew apenas definem um ponteiro de exceção na linha de execução atual. Ao retornar para gerenciada a partir do código nativo, a exceção será anotada e processada adequadamente.

Um código nativo pode "capturar" uma exceção chamando ExceptionCheck ou ExceptionOccurred e excluí-la com ExceptionClear. Como de costume, descartar exceções sem processá-las pode causar problemas.

Não há funções incorporadas para manipular o objeto Throwable. Assim, se quiser, digamos, conseguir a string da exceção, você precisará encontrar a classe Throwable, procurar o ID do método para getMessage "()Ljava/lang/String;", invocá-lo e, se o resultado for não NULL, usar GetStringUTFChars para conseguir algo que você possa entregar para printf(3) ou equivalente.

Verificação estendida

O JNI faz muito pouca verificação de erros. Os erros geralmente resultam em uma falha. O Android também oferece um modo chamado CheckJNI, em que os ponteiros da tabela de funções JavaVM e JNIEnv são trocados por tabelas de funções que executam uma série estendida de verificações antes de chamar a implementação padrão.

As verificações extras incluem:

  • Matrizes: tentar alocar uma matriz de tamanho negativo.
  • Ponteiros incorretos: passar um jarray/jclass/jobject/jstring incorreto para uma chamada JNI ou passar um ponteiro NULL para uma chamada JNI com um argumento não anulável.
  • Nomes de classe: passar tudo, exceto o estilo “java/lang/String” do nome da classe para uma chamada JNI.
  • Chamadas críticas: fazer uma chamada JNI entre um get "crítico" e o release correspondente.
  • ByteBuffers diretos: passar argumentos incorretos para NewDirectByteBuffer.
  • Exceções: fazer uma chamada JNI enquanto houver uma exceção pendente.
  • JNIEnv*s: usar um JNIEnv* da linha de execução errada.
  • jfieldIDs: usar um jfieldID NULL, usar um jfieldID para definir um campo para um valor do tipo errado (tentando atribuir um StringBuilder a um campo String, digamos), usar um jfieldID para um campo estático para definir um campo de instância ou vice-versa, ou usar um jfieldID de uma classe com instâncias de outra classe.
  • jmethodIDs: usar o tipo errado de jmethodID ao fazer uma chamada JNI Call*Method: tipo de retorno incorreto, incompatibilidade estática/não estática, tipo errado para "this" (para chamadas não estáticas) ou classe errada (para chamadas estáticas).
  • Referências: usar DeleteGlobalRef/DeleteLocalRef no tipo errado de referência.
  • Modos de liberação: passar um modo de liberação incorreto para uma chamada de liberação (algo diferente de 0, JNI_ABORT ou JNI_COMMIT).
  • Segurança de tipo: retornar um tipo incompatível do seu método nativo (retornando um StringBuilder de um método declarado para retornar uma String, por exemplo).
  • UTF-8: passar uma sequência de byte UTF-8 modificado inválida para uma chamada JNI.

A acessibilidade de métodos e campos ainda não é verificada: as restrições de acesso não se aplicam ao código nativo.

Existem várias maneiras de ativar o CheckJNI.

Se você estiver usando o emulador, o CheckJNI estará ativado por padrão.

Se você tiver um dispositivo com acesso root, poderá usar a seguinte sequência de comandos para reiniciar o ambiente de execução com o CheckJNI ativado:

adb shell stop
    adb shell setprop dalvik.vm.checkjni true
    adb shell start

Em qualquer um desses casos, você verá algo assim na sua saída do logcat quando o ambiente de execução for iniciado:

D AndroidRuntime: CheckJNI is ON

Se você tiver um dispositivo normal, poderá usar o seguinte comando:

adb shell setprop debug.checkjni 1

Isso não afetará os apps já em execução, mas todo app iniciado a partir desse ponto terá o CheckJNI ativado. Mudar a propriedade para qualquer outro valor ou simplesmente reinicializar desativará o CheckJNI novamente. Nesse caso, você verá algo assim na sua saída do logcat na próxima vez em que um app for iniciado:

D Late-enabling CheckJNI

Você também pode definir o atributo android:debuggable no manifesto do seu aplicativo para ativar o CheckJNI apenas para seu app. Observe que as ferramentas de criação do Android farão isso automaticamente para determinados tipos de criação.

Bibliotecas nativas

Você pode carregar código nativo de bibliotecas compartilhadas com o System.loadLibrary padrão.

Na prática, versões mais antigas do Android tinham erros no PackageManager que faziam com que a instalação e a atualização de bibliotecas nativas não fossem confiáveis. O projeto ReLinker oferece soluções alternativas para esse e outros problemas de carregamento de bibliotecas nativas.

Chame System.loadLibrary (ou ReLinker.loadLibrary) a partir de um inicializador de classe estática. O argumento é o nome da biblioteca "não decorado"; portanto, para carregar libfubar.so, você passaria "fubar".

Existem duas maneiras de o ambiente de execução encontrar seus métodos nativos. Você pode registrá-los explicitamente com RegisterNatives ou pode permitir que o ambiente de execução procure-os dinamicamente com dlsym. As vantagens do RegisterNatives são que você verifica antecipadamente se os símbolos existem, além de poder ter bibliotecas compartilhadas menores e mais rápidas não exportando nada além de JNI_OnLoad. A vantagem de deixar o ambiente de execução descobrir suas funções é que há um pouco menos de código para escrever.

Para usar RegisterNatives:

  • forneça uma função JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
  • no seu JNI_OnLoad, registre todos os seus métodos nativos usando RegisterNatives;
  • crie com -fvisibility=hidden para que apenas o seu JNI_OnLoad seja exportado da sua biblioteca. Isso produz um código mais rápido e menor, além de evitar possíveis colisões com outras bibliotecas carregadas no app, mas cria rastreamentos de pilha menos úteis se o app falhar em código nativo.

O inicializador estático será como este:

Kotlin

    companion object {
        init {
            System.loadLibrary("fubar")
        }
    }
    

Java

    static {
        System.loadLibrary("fubar");
    }
    

A função JNI_OnLoad será semelhante a esta se escrita em C++:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        JNIEnv* env;
        if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
            return -1;
        }

        // Get jclass with env->FindClass.
        // Register methods with env->RegisterNatives.

        return JNI_VERSION_1_6;
    }

Para usar a "descoberta" de métodos nativos, você precisa nomeá-los de uma maneira específica. Consulte a especificação JNI (link em inglês) para ver detalhes. Isso significa que se uma assinatura de método estiver errada, você não saberá até a primeira vez em que o método for invocado.

Se você tem apenas uma classe com métodos nativos, faz sentido que a chamada para System.loadLibrary esteja nessa classe. Caso contrário, você provavelmente terá que fazer a chamada do Application para saber que ele está sempre carregado e sempre carregado antecipadamente.

Qualquer chamada FindClass feita a partir de JNI_OnLoad resolverá as classes no contexto do carregador de classes que foi usado para carregar a biblioteca compartilhada. Normalmente, o FindClass usa o carregador associado ao método na parte superior da pilha Java ou, se não houver um (porque a linha de execução acabou de ser anexada), ele usa o carregador de classes "system". Isso torna o JNI_OnLoad um local conveniente para procurar e armazenar em cache as referências de objeto de classe.

Considerações quanto a 64 bits

Para compatibilidade com arquiteturas que usam ponteiros de 64 bits, use um campo long em vez de um int ao armazenar um ponteiro para uma estrutura nativa em um campo Java.

Recursos não compatíveis/compatibilidade com versões anteriores

Todos os recursos do JNI 1.6 são compatíveis, exceto o seguinte:

  • DefineClass não está implementado. O Android não usa bytecodes ou arquivos de classe do Java, então a transmissão de dados da classe binária não funciona.

Para compatibilidade com versões mais antigas do Android, você precisa conhecer:

  • Pesquisa dinâmica de funções nativas

    Até o Android 2.0 (Eclair), o caractere "$" não era convertido corretamente para "_00024" durante as pesquisas por nomes de métodos. Contornar isso requer o uso de registro explícito ou a remoção dos métodos nativos de classes internas.

  • Linhas de execução desconectadas

    Até o Android 2.0 (Eclair), não era possível usar uma função destruidora pthread_key_create para evitar a verificação "a linha de execução precisa ser desconectada antes de sair". O ambiente de execução também usa uma função destruidora de chave pthread, o que resultaria em uma corrida para ver qual é chamado primeiro.

  • Referências globais fracas

    Até o Android 2.2 (Froyo), as referências globais fracas não tinham sido implementadas. Versões mais antigas rejeitarão vigorosamente as tentativas de usá-las. Você pode usar as constantes de versão da plataforma Android para testar a compatibilidade.

    Até o Android 4.0 (Ice Cream Sandwich), referências globais fracas só podiam ser passadas para NewLocalRef, NewGlobalRef e DeleteWeakGlobalRef. A especificação incentiva fortemente os programadores a criar referências concretas para globais fracos antes de fazer qualquer coisa com eles, então isso não deve algo limitador.

    A partir do Android 4.0 (Ice Cream Sandwich), referências globais fracas podem ser usadas como qualquer outra referência JNI.

  • Referências locais

    Até o Android 4.0 (Ice Cream Sandwich), as referências locais eram, na verdade, ponteiros diretos. O Ice Cream Sandwich adicionou a indireção necessária para aceitar melhores coletores de lixo, mas isso significa que muitos erros JNI são indetectáveis em versões mais antigas. Consulte Mudanças na referência local de JNI em ICS (link em inglês) para ver mais detalhes.

    Nas versões do Android anteriores ao Android 8.0, o número de referências locais era restrito a um limite específico da versão. A partir da versão 8.0, o Android passou a ser compatível com referências locais ilimitadas.

  • Determinar o tipo de referência com GetObjectRefType

    Até o Android 4.0 (Ice Cream Sandwich), como consequência do uso de ponteiros diretos (veja acima), era impossível implementar GetObjectRefType corretamente. Em vez disso, usávamos uma heurística que examinava a tabela de globais fracos, os argumentos, a tabela de locais e a tabela de globais, nessa ordem. Ao encontrar o ponteiro direto pela primeira vez, ela relatava que sua referência era do tipo que estava sendo examinado. Isso significava, por exemplo, que se você chamasse GetObjectRefType em uma jclass global que fosse a mesma que a jclass passou como um argumento implícito para seu método nativo estático, você teria JNILocalRefType em vez de JNIGlobalRefType.

Perguntas frequentes: por que recebo UnsatisfiedLinkError?

Ao trabalhar com código nativo, não é incomum ver uma falha como esta:

java.lang.UnsatisfiedLinkError: Library foo not found

Em alguns casos, isso significa o que é dito: a biblioteca não foi encontrada. Em outros, a biblioteca existe, mas não pôde ser aberta por dlopen(3), e os detalhes da falha podem ser encontrados na mensagem detalhada da exceção.

Razões comuns pelas quais você pode encontrar exceções de "biblioteca não encontrada":

  • A biblioteca não existe ou não está acessível para o app. Use o adb shell ls -l <path> para verificar a presença dela e as permissões.
  • A biblioteca não foi criada com o NDK. Isso pode resultar em dependências de funções ou bibliotecas que não existem no dispositivo.

Outra classe de falhas UnsatisfiedLinkError tem esta aparência:

java.lang.UnsatisfiedLinkError: myfunc
            at Foo.myfunc(Native Method)
            at Foo.main(Foo.java:10)

No logcat, você verá:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

Isso significa que o ambiente de execução tentou localizar um método correspondente, mas não conseguiu. Algumas razões comuns para isso são as seguintes:

  • A biblioteca não está sendo carregada. Verifique a saída do logcat para receber mensagens sobre o carregamento da biblioteca.
  • O método não está sendo encontrado devido a uma incompatibilidade de nome ou assinatura. Isso é comumente causado por estes fatores:
    • Para pesquisa de método lento, há uma falha ao declarar funções C++ com extern "C" e visibilidade apropriada (JNIEXPORT). Antes do Ice Cream Sandwich, a macro JNIEXPORT estava incorreta, então usar um novo GCC com um jni.h antigo não funcionará. Você pode usar arm-eabi-nm para ver os símbolos que aparecem na biblioteca. Se eles parecerem errados (algo como _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass em vez de Java_Foo_myfunc), ou se o tipo de símbolo for um "t" minúsculo em vez de um "T" maiúsculo, então você precisa ajustar a declaração.
    • Para registro explícito, pequenos erros ao inserir a assinatura do método. Verifique se o que você está passando para a chamada de registro corresponde à assinatura no arquivo de registros. Lembre-se de que "B" é byte e "Z" é boolean. Os componentes de nome de classe nas assinaturas começam com "L", terminam com ";", usam "/" para separar nomes de pacotes/classes e usam "$" para separar nomes de classes internas (Ljava/util/Map$Entry;).

Usar o javah para gerar automaticamente cabeçalhos JNI pode ajudar a evitar alguns problemas.

Perguntas frequentes: por que o FindClass não encontrou minha classe?

No geral, esta orientação se aplica igualmente bem às falhas ao encontrar métodos com GetMethodID ou GetStaticMethodID ou campos com GetFieldID ou GetStaticFieldID.

Verifique se a string do nome da classe tem o formato correto. Os nomes de classes JNI começam com o nome do pacote e são separados por barras, como java/lang/String. Se estiver procurando uma classe de matriz, você precisa começar com o número apropriado de colchetes e também precisa envolver a classe com "L" e ";". Então, uma matriz unidimensional de String seria [Ljava/lang/String;. Se você está procurando uma classe interna, use "$" em vez de ".". No geral, usar javap no arquivo .class é uma boa maneira de descobrir o nome interno da sua classe.

Se você estiver usando o ProGuard, verifique se o ProGuard não removeu sua classe. Isso pode acontecer se seu método/classe/campo for usado apenas a partir do JNI.

Se o nome da classe parece estar certo, pode estar ocorrendo um problema de carregador de classe. FindClass quer iniciar a pesquisa de classe no carregador de classes associado ao seu código. Ele examina a pilha de chamadas, que terá esta aparência:

    Foo.myfunc(Native Method)
        Foo.main(Foo.java:10)

O método principal é o Foo.myfunc. O FindClass encontra o objeto ClassLoader associado à classe Foo e usa isso.

Isso geralmente faz o que você quer. Você pode ter problemas se criar uma linha de execução por conta própria, talvez chamando pthread_create e, em seguida, anexando-o a AttachCurrentThread. Agora, não há frames de pilha do seu app. Se você chamar FindClass a partir dessa linha de execução, o JavaVM iniciará no carregador de classes "system" em vez daquele associado ao seu aplicativo. Portanto, as tentativas de localizar classes específicas do app falharão.

Existem algumas maneiras de contornar isso:

  • Faça suas pesquisas de FindClass uma vez, em JNI_OnLoad, e armazene em cache as referências de classe para uso posterior. Toda chamada FindClass feita como parte da execução de JNI_OnLoad usará o carregador de classes associado à função que chamou System.loadLibrary. Essa é uma regra especial, fornecida para tornar a inicialização da biblioteca mais conveniente. Se o código do app estiver carregando a biblioteca, o FindClass usará o carregador de classes correto.
  • Passe uma instância da classe para as funções que precisam dela, declarando seu método nativo para conseguir um argumento Class e, em seguida, passando Foo.class.
  • Armazene em cache uma referência ao objeto ClassLoader em algum lugar prático e emita chamadas loadClass diretamente. Isso requer certo esforço.

Perguntas frequentes: como eu compartilho dados brutos com código nativo?

Você pode se deparar com uma situação em que precisa acessar um grande buffer de dados brutos do código gerenciado e nativo. Exemplos comuns incluem a manipulação de bitmaps ou amostras de som. Existem duas abordagens básicas.

Você pode armazenar os dados em um byte[]. Isso permite acesso muito rápido a partir do código gerenciado. No entanto, no lado nativo, você não tem a garantia de poder acessar os dados sem precisar copiá-los. Em algumas implementações, GetByteArrayElements e GetPrimitiveArrayCritical retornarão ponteiros reais para os dados brutos no heap gerenciado. Em outras, eles alocarão um buffer no heap nativo e copiarão os dados.

A alternativa é armazenar os dados em um buffer de bytes direto. Esses podem ser criados com o java.nio.ByteBuffer.allocateDirect ou a função JNI NewDirectByteBuffer. Ao contrário dos buffers de bytes normais, o armazenamento não é alocado no heap gerenciado e sempre pode ser acessado diretamente do código nativo (consiga o endereço com GetDirectBufferAddress). Dependendo de como o acesso ao buffer de bytes direto é implementado, o acesso aos dados do código gerenciado pode ser muito lento.

A escolha de qual deles usar depende de dois fatores:

  1. A maioria dos acessos a dados acontecerá a partir do código escrito em Java ou em C/C++?
  2. Se os dados estão sendo passados para uma API do sistema, em qual formato eles devem estar? Por exemplo, se os dados forem passados para uma função que toma um byte[], fazer o processamento em um ByteBuffer direto pode ser desaconselhável.

Se não houver um vencedor claro, use um buffer de bytes direto. A compatibilidade com eles é integrada diretamente ao JNI, e o desempenho deve melhorar em versões futuras.