JNI é a sigla para Java Native Interface. Essa interface 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++). A JNI é independente de fornecedores, permite o carregamento de código usando bibliotecas compartilhadas dinâmicas e, embora, às vezes, seja pesada, é 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 ela, leia a Especificação da Java Native Interface (link em inglês) para ter uma noção de como a 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 mais recentes.
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 da 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, é possível simplificar as atualizações assíncronas da IU mantendo a atualização assíncrona na mesma linguagem da IU. Por exemplo, em vez de invocar uma função C++ na 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 uma delas fazendo uma chamada C++ de bloqueio e 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 identificáveis 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++, eles 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 por meio da tabela. O JavaVM fornece as funções de "interface de invocação", que permitem criar e destruir um JavaVM. Em teoria, 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
do primeiro argumento, exceto para os métodos @CriticalNative
,
confira chamadas nativas mais rápidas.
O JNIEnv é usado para armazenamento local de linhas de execução. Por esse motivo, não é possível 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 de JNIEnv e JavaVM são diferentes das declarações C++. O arquivo de inclusão "jni.h"
fornece diferentes typedefs, dependendo se ele está incluído em C ou C ++. Por esse motivo, não é uma boa ideia incluir os argumentos JNIEnv em arquivos principais incluídos por ambas as linguagens. Dito de outra forma: se o arquivo principal exige #ifdef __cplusplus
, talvez seja preciso fazer algum trabalho extra se algo 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()
ou std::thread
pode ser anexada usando as funções 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.
Em geral, é melhor usar Thread.start()
para criar qualquer linha de execução que precise chamar o código Java. Isso garantirá que você tenha espaço de pilha suficiente, que esteja no ThreadGroup
correto e que esteja usando o mesmo ClassLoader
do código Java. Também é mais fácil definir o nome da linha de execução para depuração em Java do que em código nativo. Consulte pthread_setname_np()
, se você tiver um pthread_t
ou thread_t
, e std::thread::native_handle()
, se tiver um std::thread
e quiser um pthread_t
.
Anexar uma linha de execução criada de maneira nativa 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.
As linhas de execução anexadas por meio de JNI precisam chamar DetachCurrentThread()
antes de sair.
Se codificar isso diretamente for estranho, no Android 2.0 (Eclair) e versões mais recentes, use 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 código do campo com
GetFieldID
- Consiga o conteúdo do campo com algo adequado, 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 têm, 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ó serã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 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 é adicionar à classe adequada 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 transmitido 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 próprio objeto 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 estendidas da JNI estiverem ativadas.
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 poderá presumir que as referências de objeto sejam constantes ou exclusivas no código nativo. O valor que representa um objeto pode ser diferente
de uma invocação de um método para a próxima, e é possível que duas
objetos diferentes podem ter o mesmo valor em chamadas consecutivas. Não usar
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 durante a execução de uma matriz de objetos, será preciso liberá-los de forma manual com DeleteLocalRef
em vez de permitir que a JNI faça isso por você. Como a implementação só é necessária para reservar slots para 16 referências locais, se precisar de mais do que isso, exclua conforme avança ou utilize EnsureLocalCapacity
/PushLocalFrame
para reservar mais.
Observe que jfieldID
s e jmethodID
s são tipos opacos, não referências a objetos, e não precisam ser transmitidos para NewGlobalRef
. Os pontos 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 criadas por você 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 não é possível transmitir dados arbitrários UTF-8 para o JNI e esperar que ele funcione corretamente.
Para conseguir a representação UTF-16 de um String
, use GetStringChars
.
As strings UTF-16 não terminam em zero, e \u0000 é permitido.
então você precisa manter o comprimento da string, bem como o ponteiro jchar.
Não esqueça de Release
as strings que você 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é Release
ser 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 no 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.
Antes do Android 8, geralmente era mais rápido operar com strings UTF-16 como o Android
não exigia uma cópia em GetStringChars
, enquanto
GetStringUTFChars
exigiu uma alocação e uma conversão para UTF-8.
O Android 8 mudou a representação String
para usar 8 bits por caractere.
para strings ASCII (para economizar memória) e começou a usar uma
mudando
coletor de lixo. Esses recursos reduzem bastante o número de casos em que o ART
podem fornecer um ponteiro para os dados de String
sem fazer uma cópia, mesmo
para GetStringCritical
. No entanto, se a maioria das strings processadas pelo código
são curtas, é possível evitar a alocação e a desalocação, na maioria dos casos,
usando um buffer alocado em pilha e GetStringRegion
ou
GetStringUTFRegion
. Exemplo:
constexpr size_t kStackBufferSize = 64u; jchar stack_buffer[kStackBufferSize]; std::unique_ptr<jchar[]> heap_buffer; jchar* buffer = stack_buffer; jsize length = env->GetStringLength(str); if (length > kStackBufferSize) { heap_buffer.reset(new jchar[length]); buffer = heap_buffer.get(); } env->GetStringRegion(str, 0, length, buffer); process_data(buffer, length);
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 um pouco de memória e faça uma cópia. De qualquer forma, o ponteiro bruto retornado tem a garantia de ser válido até a chamada de Release
correspondente ser emitida (o que implica que, se os dados não forem copiados, o objeto de matriz será fixado e não poderá ser realocado como parte da compactação do heap).
Você precisa Release
a cada matriz que Get
. Além disso, se a chamada Get
falhar, você precisa garantir que seu código não tentará liberar (Release
) um ponteiro NULL mais tarde.
É possível determinar se os dados foram ou não copiados transmitindo um ponteiro não NULL para o argumento isCopy
. Isso raramente é útil.
A chamada Release
usa um argumento mode
que pode ter um dos 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 modificações nele são perdidas.
Um motivo para verificar a sinalização isCopy
é saber se você precisa chamar Release
com JNI_COMMIT
após modificar uma matriz. Se estiver alternando entre modificar e executar o código que usa o conteúdo da matriz, talvez seja possível ignorar a confirmação do ambiente autônomo. Outro motivo possível para verificar a sinalização é o uso eficiente de JNI_ABORT
. Por exemplo, você pode querer conseguir uma matriz, modificá-la no lugar, transmitir as peças para outras funções e descartar as modificações. Se você sabe que a JNI está fazendo uma nova cópia para você, não é necessário criar outra cópia "editável". Se a JNI estiver transmitindo 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 a sinalização JNI_COMMIT
não libera a matriz, e você precisará chamar Release
novamente com outra sinalização.
Chamadas de região
Há uma alternativa para chamadas como Get<Type>ArrayElements
e GetStringChars
que podem ser muito úteis quando você só quer copiar ou remover dados. Considere 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 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, é possível usar a chamada Set<Type>ArrayRegion
para copiar dados em uma matriz, e GetStringRegion
ou
GetStringUTFRegion
para copiar caracteres de uma
String
.
Exceções
Você não precisa 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 você chamar um método (usando uma função como CallObjectMethod
), sempre será necessário verificar uma exceção, porque o valor de retorno não será válido se uma exceção tiver sido gerada.
As exceções geradas pelo código gerenciado não liberam a pilha nativa
frames. (E exceções de C++, geralmente não recomendadas no Android, não podem ser
gerada no limite de transição JNI do código C++ para o código gerenciado.
As instruções JNI Throw
e ThrowNew
só 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.
O código nativo pode "capturar" uma exceção chamando ExceptionCheck
ou ExceptionOccurred
e eliminá-la com ExceptionClear
. Como de costume, descartar exceções sem processá-las pode causar problemas.
Não há funções integradas para manipular o objeto Throwable
em si. Portanto, se você quiser (digamos) receber a string de exceção, precisará encontrar a classe Throwable
, procurar a código do método para getMessage "()Ljava/lang/String;"
, invocá-la e, se o resultado não for NULL, usar GetStringUTFChars
para conseguir algo que possa entregar a 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: transmitir 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: transmitir um modo de liberação incorreto para uma chamada de liberação (algo diferente de
0
,JNI_ABORT
ouJNI_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
Também é possível definir o atributo android:debuggable
no manifesto do aplicativo como
ative o CheckJNI apenas para seu app. Observe que as ferramentas de compilação do Android fazem isso automaticamente para
certos tipos de build.
Bibliotecas nativas
Você pode carregar o código nativo a partir 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". Então, para carregar libfubar.so
, transmita "fubar"
.
Se você tem apenas uma classe com métodos nativos, convém que a chamada para System.loadLibrary
esteja em um inicializador estático nessa classe. Caso contrário, é interessante fazer a chamada do Application
, assim você saberá que a biblioteca está sempre carregada e que isso será feito antecipadamente.
Existem duas maneiras para 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 de forma antecipada se os símbolos existem, além de poder ter bibliotecas compartilhadas menores e mais rápidas sem exportar 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
JNI_OnLoad
, registre todos os métodos nativos usandoRegisterNatives
. - Crie com
-fvisibility=hidden
para que apenas oJNI_OnLoad
seja exportado da sua biblioteca. Isso produz um código menor e mais rápido e evita possíveis colisões com outras bibliotecas carregadas no aplicativo, mas cria rastreamentos de pilha menos úteis caso seu app falhe 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 estiver 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 JNI_ERR; } // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. jclass c = env->FindClass("com/example/app/package/MyClass"); if (c == nullptr) return JNI_ERR; // Register your class' native methods. static const JNINativeMethod methods[] = { {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)}, {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)}, }; int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod)); if (rc != JNI_OK) return rc; 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.
Todas as chamadas FindClass
feitas de JNI_OnLoad
resolverão as classes no contexto do carregador de classes que foi usado para carregar a biblioteca compartilhada. Quando chamada de outros contextos, a FindClass
usa o carregador de classes associado ao método na parte superior da pilha Java ou, se não existir um (porque a chamada é de uma linha de execução nativa que acabou de ser anexada), ela usará o carregador de classes "system". Como o carregador de classes do sistema não conhece as classes do seu app, não será possível procurar suas próprias classes com FindClass
nesse contexto. Isso torna o JNI_OnLoad
um local conveniente para procurar e armazenar em cache as classes: uma vez
você tem uma referência global válida do jclass
você pode usá-lo a partir de qualquer conversa anexada.
Chamadas nativas mais rápidas com o @FastNative
e o @CriticalNative
Os métodos nativos podem ser anotados com
@FastNative
ou
@CriticalNative
(mas não ambos) para acelerar as transições entre código gerenciado e nativo. No entanto, essas anotações
apresentam certas mudanças de comportamento que precisam ser consideradas cuidadosamente antes do uso. Embora
mencione brevemente essas mudanças abaixo. Consulte a documentação para mais detalhes.
A anotação @CriticalNative
só pode ser aplicada a métodos nativos que não
objetos gerenciados (em parâmetros, valores de retorno ou como um this
implícito), e isso
muda a ABI de transição JNI. A implementação nativa precisa excluir o
Parâmetros JNIEnv
e jclass
da assinatura da função.
Ao executar um método @FastNative
ou @CriticalNative
, o lixo
A coleção não pode suspender a linha de execução para trabalho essencial e pode ser bloqueada. Não usar
anotações para métodos de longa duração, incluindo métodos geralmente rápidos, mas geralmente ilimitados.
Em particular, o código não deve executar operações de E/S significativas nem adquirir bloqueios nativos que
podem ser mantidos por muito tempo.
Essas anotações foram implementadas para uso do sistema,
Android 8
e passou para o público com teste de CTS
API no Android 14. É provável que essas otimizações também funcionem em dispositivos Android 8 a 13,
sem as fortes garantias do CTS, mas a pesquisa dinâmica de métodos nativos só tem suporte
No Android 12 ou versões mais recentes, o registro explícito com RegisterNatives
da JNI é estritamente obrigatório
para as versões 8 a 11 do Android. Essas anotações são ignoradas no Android 7-, a incompatibilidade de ABI.
para @CriticalNative
resultaria em um marshalling de argumentos errado e em prováveis falhas.
Para métodos críticos de desempenho que precisam dessas anotações, é altamente recomendável
registre explicitamente os métodos com a RegisterNatives
da JNI em vez de depender da
"descoberta" baseada em nome de métodos nativos. Para ter o desempenho ideal de inicialização do app, recomendamos
incluir autores de chamadas dos métodos @FastNative
ou @CriticalNative
na
perfil de referência. Desde o Android 12,
uma chamada para um método nativo @CriticalNative
de um método gerenciado compilado é quase tão
é mais barata que uma chamada não inline em C/C++, desde que todos os argumentos se encaixem nos registros (por exemplo, até
integral 8 e até 8 argumentos de ponto flutuante no arm64).
Às vezes, pode ser melhor dividir um método nativo em dois, um método muito rápido que pode e outra que lida com os casos lentos. Exemplo:
Kotlin
fun writeInt(nativeHandle: Long, value: Int) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value) } } @CriticalNative external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean external fun nativeWriteInt(nativeHandle: Long, value: Int)
Java
void writeInt(long nativeHandle, int value) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value); } } @CriticalNative static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value); static native void nativeWriteInt(long nativeHandle, int value);
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 da JNI 1.6 são compatíveis, exceto o seguinte:
DefineClass
não foi implementado. O Android não usa bytecodes ou arquivos de classe do Java; portanto, 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
eDeleteWeakGlobalRef
. Como a especificação incentiva fortemente os programadores a criar referências concretas para globais fracos antes de fazer qualquer coisa com eles, isso não será 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 coletores de lixo melhores, 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.
- Como 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ê chamasseGetObjectRefType
em uma jclass global que fosse a mesma que a jclass transmitida como um argumento implícito para seu método nativo estático, você teriaJNILocalRefType
em vez deJNIGlobalRefType
. @FastNative
e@CriticalNative
Até o Android 7, essas anotações de otimização eram ignoradas. A ABI incompatibilidade de
@CriticalNative
levaria a um argumento errado no gerenciamento e na probabilidade de falhas.A pesquisa dinâmica de funções nativas para
@FastNative
e Métodos@CriticalNative
não foram implementados no Android 8-10 e contém bugs conhecidos no Android 11. Usar essas otimizações sem registro explícito comRegisterNatives
da JNI provavelmente levar a falhas no Android 8 a 11.FindClass
geraClassNotFoundException
Para compatibilidade com versões anteriores, o Android gera
ClassNotFoundException
em vez deNoClassDefFoundError
quando uma classe não é encontrada peloFindClass
Esse comportamento é consistente com a API de reflexão do Java.Class.forName(name)
:
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 pode ser acessada pelo app. Use
adb shell ls -l <path>
para verificar a presença 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:
- Por pesquisa preguiçosa de método, ao falhar na declaração de funções de C++ com
extern "C"
e visibilidade adequada (JNIEXPORT
). Observe que, antes do Ice Cream Sandwich, a macro JNIEXPORT estava incorreta. Portanto, usar um novo GCC com umajni.h
antiga não funcionará. Você pode usararm-eabi-nm
para ver os símbolos como aparecem na biblioteca. Se eles parecerem corrompidos (algo como_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
em vez deJava_Foo_myfunc
) ou se o tipo de símbolo for "t" em vez de "T", você precisará ajustar a declaração. - Para registro explícito, pequenos erros ao inserir a assinatura do método. Verifique se o que você está transmitindo 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;
, por exemplo).
- Por pesquisa preguiçosa de método, ao falhar na declaração de funções de C++ com
O uso de javah
para gerar automaticamente cabeçalhos JNI pode ajudar a evitar alguns problemas.
Perguntas frequentes: por que o FindClass
não encontrou minha classe?
A maioria dessas recomendações se aplica igualmente a falhas em 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 das classes JNI começam com o nome do pacote e são separados por barras, como java/lang/String
. Se você estiver procurando uma classe de matriz, será necessário começar com o número apropriado de colchetes e também precisará colocar a classe entre "L" e ";". Assim, uma matriz unidimensional de String
seria [Ljava/lang/String;
.
Se você estiver procurando uma classe interna, use "$" em vez de ".". Em geral, o uso de javap
no arquivo .class é uma boa maneira de descobrir o nome interno da classe.
Se você ativar a redução de código, configure qual código manter. A configuração de regras de manutenção adequadas é importante porque o redutor de código pode remover classes, métodos ou campos usados apenas pela 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 mais acima é Foo.myfunc
. FindClass
encontra o objeto ClassLoader
associado à classe Foo
e o usa.
Isso geralmente faz o que você quer. Você pode ter problemas se criar uma linha de execução (talvez chamando pthread_create
e anexando-a com 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 app. 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, emJNI_OnLoad
, e armazene em cache as referências de classe para uso posterior. Toda chamadaFindClass
feita como parte da execução deJNI_OnLoad
usará o carregador de classes associado à função que chamouSystem.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, oFindClass
usará o carregador de classes correto. - Transmita 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, transmitindo
Foo.class
. - Armazene em cache uma referência ao objeto
ClassLoader
em algum lugar prático e emita chamadasloadClass
de forma direta. 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. Eles podem ser criados com java.nio.ByteBuffer.allocateDirect
ou com a função JNI NewDirectByteBuffer
. Ao contrário dos buffers de byte regulares, 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 direto ao buffer de bytes é implementado, o acesso aos dados do código gerenciado pode ser muito lento.
A escolha de qual deles usar depende de dois fatores:
- A maioria dos acessos a dados acontecerá a partir do código escrito em Java ou em C/C++?
- Se os dados estão sendo transmitidos para uma API do sistema, em qual formato eles precisam estar? Por exemplo, se os dados forem transmitidos para uma função que toma um byte[], fazer o processamento em um
ByteBuffer
direto pode não ser uma boa ideia.
Se não houver um vencedor claro, use um buffer de bytes direto. A compatibilidade com eles é integrada diretamente ao JNI, e o desempenho será melhor em versões futuras.