JNI è l'interfaccia nativa Java. Definisce un modo per consentire al bytecode compilato da Android dal codice gestito (scritto nei linguaggi di programmazione Java o Kotlin) di interagire con il codice nativo (scritto in C/C++). JNI è indipendente dal fornitore, supporta il caricamento del codice dalle librerie condivise dinamiche e, sebbene a volte sia complicato, è ragionevolmente efficiente.
Nota:dato che Android compila Kotlin in bytecode compatibili con ART in in modo simile al linguaggio di programmazione Java, puoi applicare le indicazioni in questa pagina sia i linguaggi di programmazione Kotlin e Java in termini di architettura JNI e costi associati. Per scoprire di più, consulta Kotlin e Android.
Se non lo hai già fatto, leggi attentamente il Specifica dell'interfaccia nativa Java per avere un'idea di come funziona JNI e di quali funzionalità sono disponibili. Alcune aspetti dell'interfaccia non sono immediatamente evidenti per prima cosa, per cui le sezioni successive potrebbero esserti utili.
Per sfogliare i riferimenti JNI globali e vedere dove vengono creati ed eliminati, utilizza la visualizzazione Heap JNI in Memory Profiler in Android Studio 3.2 e versioni successive.
Suggerimenti generali
Cerca di ridurre al minimo l'ingombro del livello JNI. Ci sono diverse dimensioni da considerare in questo caso. La soluzione JNI deve cercare di seguire queste linee guida (elencate di seguito in ordine di importanza, iniziando dalla più importante):
- Minimizza il marshalling delle risorse nel livello JNI. Marshalling attraverso lo strato JNI ha costi non banali. Prova a progettare un'interfaccia che riduca al minimo la quantità di dati da organizzare e la frequenza con cui devi farlo.
- Evita la comunicazione asincrona tra il codice scritto in una programmazione gestita linguaggio e codice scritti in C++ se possibile. In questo modo la gestione dell'interfaccia JNI sarà più semplice. In genere, puoi semplificare le operazioni Gli aggiornamenti dell'interfaccia utente mantengono l'aggiornamento asincrono nella stessa lingua dell'interfaccia utente. Ad esempio, invece di richiamando una funzione C++ dal thread dell'interfaccia utente nel codice Java tramite JNI, è preferibile eseguire un callback tra due thread nel linguaggio di programmazione Java, con uno di questi l'esecuzione di una chiamata C++ di blocco e poi una notifica al thread dell'interfaccia utente quando la chiamata di blocco viene completato.
- Riduci al minimo il numero di thread che devono essere toccati o toccati da JNI. Se devi utilizzare i pool di thread sia nei linguaggi Java che in C++, prova a mantenere JNI le comunicazioni tra i proprietari del pool anziché tra singoli thread worker.
- Mantieni il codice dell'interfaccia in un numero ridotto di codice sorgente C++ e Java facilmente identificabili località per facilitare i refactoring futuri. Valuta la possibilità di utilizzare una libreria di generazione automatica JNI, se opportuno.
JavaVM e JNIEnv
JNI definisce due strutture di dati chiave: "JavaVM" e "JNIEnv". Entrambi sono essenzialmente puntatori a puntatori a tabelle di funzioni. Nella versione C++, sono classi con un puntatore a una tabella di funzioni e una funzione membro per ogni funzione JNI che avviene indirettamente tramite la tabella. La JavaVM fornisce l'"interfaccia di chiamata" le funzioni, che ti consentono di creare ed eliminare una JavaVM. In teoria puoi avere più JavaVM per processo, ma Android ne consente solo una.
JNIEnv fornisce la maggior parte delle funzioni JNI. Tutte le tue funzioni native ricevono un JNIEnv come
sul primo argomento, tranne che per i metodi @CriticalNative
,
vedi le chiamate native più veloci.
JNIEnv viene utilizzato per l'archiviazione locale nei thread. Per questo motivo, non puoi condividere un JNIEnv tra i thread.
Se una porzione di codice non è in grado di recuperare il proprio JNIEnv, devi condividere
la JavaVM e usa GetEnv
per trovare JNIEnv del thread. Supponendo che ne abbia uno; consulta la sezione AttachCurrentThread
di seguito.
Le dichiarazioni C di JNIEnv e JavaVM sono diverse da quelle C++
dichiarazioni. Il file include "jni.h"
fornisce tipi di dati definiti diversi
a seconda che sia incluso in C o C++. Per questo motivo, non è consigliabile
includere gli argomenti JNIEnv nei file di intestazione inclusi da entrambi i linguaggi. In altre parole, se il file di intestazione richiede #ifdef __cplusplus
, potresti dover fare un po' di lavoro extra se qualcosa nell'intestazione fa riferimento a JNIEnv.
Thread
Tutti i thread sono thread Linux e pianificati dal kernel. In genere vengono avviati da codice gestito (utilizzando Thread.start()
), ma possono anche essere creati altrove e poi collegati al JavaVM
. Per
ad esempio, un thread iniziato con pthread_create()
o std::thread
può essere collegato utilizzando il AttachCurrentThread()
Funzioni di AttachCurrentThreadAsDaemon()
. Finché un thread non viene
collegato, non ha JNIEnv e non può effettuare chiamate JNI.
In genere, è meglio utilizzare Thread.start()
per creare qualsiasi thread che debba chiamare il codice Java. In questo modo avrai spazio di stack sufficiente, ti assicurerai di essere nel ThreadGroup
corretto e di utilizzare lo stesso ClassLoader
del codice Java. È anche più facile impostare il nome del thread per il debug in Java
codice nativo (vedi pthread_setname_np()
se hai un pthread_t
o
thread_t
e std::thread::native_handle()
se hai un
std::thread
e vogliono pthread_t
).
L'attacco di un thread creato in modo nativo fa sì che un oggetto java.lang.Thread
venga costruito e aggiunto al ThreadGroup
"principale",
rendendolo visibile al debugger. Chiamata a AttachCurrentThread()
su un thread già allegato è un'operazione nulla.
Android non sospende i thread che eseguono codice nativo. Se La garbage collection è in corso oppure il debugger ha emesso una sospensione Android metterà in pausa il thread la prossima volta che effettua una chiamata JNI.
I thread collegati tramite JNI devono chiamare
DetachCurrentThread()
prima di uscire.
Se la programmazione diretta è imbarazzante, in Android 2.0 (Eclair) e versioni successive è possibile
puoi usare pthread_key_create()
per definire un distruttore
funzione che verrà richiamata prima della chiusura del thread e
chiama DetachCurrentThread()
da lì. Utilizza questa chiave con pthread_setspecific()
per memorizzare JNIEnv nell'area di archiviazione locale del thread; in questo modo, verrà passata al distruttore come argomento.
jclass, jmethodID e jfieldID
Se vuoi accedere al campo di un oggetto dal codice nativo, procedi nel seguente modo:
- Ottieni il riferimento all'oggetto della classe per la classe con
FindClass
- Ottieni l'ID per il campo con
GetFieldID
- Ottieni i contenuti del campo con qualcosa di appropriato, come
GetIntField
Analogamente, per chiamare un metodo, devi prima ottenere un riferimento all'oggetto della classe e poi un ID metodo. Spesso gli ID sono solo alle strutture dati di runtime interne. La ricerca potrebbe richiedere diverse stringhe ma una volta ottenuti la chiamata effettiva per ottenere il campo o richiamare il metodo è molto veloce.
Se il rendimento è importante, è utile cercare i valori una volta e memorizzare nella cache i risultati nel codice nativo. Poiché esiste un limite di una JavaVM per processo, è ragionevole memorizzare questi dati in una struttura locale statica.
La validità dei riferimenti di classe, degli ID campo e degli ID metodo è garantita fino all'unload della classe. Classi
vengono scaricati solo se tutte le classi associate a un ClassLoader possono essere garbage collection,
il che è raro, ma non sarà impossibile su Android. Tuttavia, tieni presente
jclass
è un riferimento di classe e deve essere protetto con una chiamata
a NewGlobalRef
(vedi la sezione successiva).
Se vuoi memorizzare nella cache gli ID quando viene caricata una classe e rieseguire automaticamente la memorizzazione nella cache se la classe viene scaricata e ricaricata mai, il modo corretto gli ID consiste nell'aggiungere alla classe appropriata una porzione di codice simile alla seguente:
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(); }
Crea un metodo nativeClassInit
nel codice C/C++ che esegua le ricerche degli ID. Il codice
verrà eseguita una volta, quando la classe viene inizializzata. Se il corso viene scaricato
e poi ricaricato, verrà eseguito di nuovo.
Riferimenti locali e globali
Ogni argomento passato a un metodo nativo e quasi ogni oggetto restituito da una funzione JNI è un "riferimento locale". Ciò significa che è valido per la durata del metodo nativo corrente nel thread corrente. Anche se l'oggetto stesso continua a esistere dopo il ritorno del metodo nativo, il riferimento non è valido.
Questo vale per tutte le sottoclassi di jobject
, tra cui
jclass
, jstring
e jarray
.
Il runtime ti avviserà della maggior parte degli utilizzi impropri dei riferimenti quando sono abilitati i controlli JNI estesi.
L'unico modo per ottenere riferimenti non locali è tramite le funzioni
NewGlobalRef
e NewWeakGlobalRef
.
Se desideri conservare un riferimento per un periodo più lungo, devi utilizzare
"globale" riferimento. La funzione NewGlobalRef
utilizza
riferimento locale come argomento e restituisce uno globale.
La validità del riferimento globale è garantita fino alla chiamata
DeleteGlobalRef
.
Questo pattern viene comunemente utilizzato quando si memorizza nella cache una classe jclass restituita
da FindClass
, ad es.:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Tutti i metodi JNI accettano riferimenti sia locali che globali come argomenti.
È possibile che i riferimenti allo stesso oggetto abbiano valori diversi.
Ad esempio, i valori restituiti da chiamate consecutive a
NewGlobalRef
sullo stesso oggetto potrebbero essere diversi.
Per verificare se due riferimenti fanno riferimento allo stesso oggetto,
devi utilizzare la funzione IsSameObject
. Non confrontare mai
riferimenti con ==
nel codice nativo.
Una conseguenza di ciò è che
non deve presupporre che i riferimenti agli oggetti siano costanti o univoci
nel codice nativo. Il valore che rappresenta un oggetto può essere diverso da un'invocazione di un metodo all'altra ed è possibile che due oggetti diversi abbiano lo stesso valore in chiamate consecutive. Non utilizzare valori jobject
come chiavi.
I programmatori sono tenuti a "non allocare eccessivamente" riferimenti locali. In pratica, questo significa
che se si creano grandi quantità di riferimenti locali, magari durante l'esecuzione di un array di
dovresti liberarli manualmente usando
DeleteLocalRef
invece di lasciare che sia JNI a farlo per te. La
è necessaria solo per prenotare slot
16 riferimenti locali, quindi se te ne servono altri, devi eliminarli mentre procedi
EnsureLocalCapacity
/PushLocalFrame
per prenotarne altre.
Tieni presente che i valori jfieldID
e jmethodID
sono opachi.
tipi di oggetti, non riferimenti agli oggetti, e non devono essere trasmessi
NewGlobalRef
. I dati non elaborati
puntatori restituiti da funzioni come GetStringUTFChars
e GetByteArrayElements
non sono oggetti. (Potrebbero essere passati
tra thread e sono validi fino alla chiamata Release corrispondente.)
Un caso insolito merita una menzione a parte. Se colleghi un modello nativo
thread con AttachCurrentThread
, il codice che stai eseguendo
non vengono mai liberati automaticamente finché il thread non si scollega. Eventuali riferimenti locali che crei dovranno essere eliminati manualmente. In generale, qualsiasi codice nativo che crea riferimenti locali in un ciclo probabilmente richiede l'eliminazione manuale.
Fai attenzione a utilizzare riferimenti globali. I riferimenti globali possono essere inevitabili, ma sono difficili da eseguire il debug e possono causare comportamenti (mal)funzionanti della memoria difficili da diagnosticare. A parità di condizioni, con meno riferimenti globali è probabilmente migliore.
Stringhe UTF-8 e UTF-16
Il linguaggio di programmazione Java utilizza UTF-16. Per praticità, JNI offre metodi che funzionano Hai modificato anche la codifica UTF-8 modificata. La la codifica modificata è utile per il codice C perché codifica \u0000 come 0xc0 0x80 anziché 0x00. L'aspetto positivo è che si può contare su stringhe con terminazione zero in stile C, adatto all'uso con le funzioni stringa libc standard. Lo svantaggio è che non puoi passare dati UTF-8 arbitrari a JNI e aspettarti che funzionino correttamente.
Per ottenere la rappresentazione UTF-16 di un String
, utilizza GetStringChars
.
Tieni presente che le stringhe UTF-16 non terminano con uno zero e che è consentito il carattere \u0000, quindi devi conservare la lunghezza della stringa e il puntatore jchar.
Non dimenticare di Release
le stringhe che Get
. Le funzioni di stringa restituiscono jchar*
o jbyte*
, che sono puntatori in stile C a dati primitivi anziché riferimenti locali. La loro validità è garantita fino alla chiamata di Release
, il che significa che non vengono rilasciate quando il metodo nativo restituisce.
I dati trasmessi a NewStringUTF devono essere nel formato UTF-8 modificato. Un
errore comune è leggere i dati dei caratteri da uno stream di rete o da un file
e trasmetterli a NewStringUTF
senza filtrarli.
A meno che tu non sappia che i dati sono MUTF-8 validi (o ASCII a 7 bit, che è un sottoinsieme compatibile),
devi rimuovere i caratteri non validi o convertirli nel formato UTF-8 modificato corretto.
In caso contrario, è probabile che la conversione UTF-16 fornisca risultati imprevisti.
CheckJNI, attivo per impostazione predefinita per gli emulatori, scansiona le stringhe.
e interrompe la VM se riceve input non validi.
Prima di Android 8, in genere era più veloce lavorare con stringhe UTF-16 perché Android
non richiedeva una copia in GetStringChars
, mentre
GetStringUTFChars
richiedeva un'allocazione e una conversione in UTF-8.
Android 8 ha modificato la rappresentazione String
in modo da utilizzare 8 bit per carattere
per le stringhe ASCII (per risparmiare memoria) e ha iniziato a utilizzare
traslochi
garbage collector. Queste funzionalità riducono notevolmente il numero di casi in cui ART
può fornire un puntatore ai dati di String
senza crearne una copia,
per GetStringCritical
. Tuttavia, se la maggior parte delle stringhe elaborate dal codice
sono brevi, nella maggior parte dei casi è possibile evitare l'allocazione e la
utilizzando un buffer allocato in pila e GetStringRegion
oppure
GetStringUTFRegion
. Ad esempio:
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);
Array primitivi
JNI fornisce funzioni per l'accesso al contenuto degli oggetti array. Mentre gli array di oggetti devono essere accessibili una voce alla volta, le matrici di le primitive possono essere lette e scritte direttamente come se fossero dichiarate in C.
Per rendere l'interfaccia il più efficiente possibile senza vincoli
l'implementazione della VM, Get<PrimitiveType>ArrayElements
una famiglia di chiamate consente al runtime di restituire un puntatore agli elementi effettivi oppure
allocare un po' di memoria e creare una copia. In ogni caso, il puntatore non elaborato ha restituito
è garantita fino alla chiamata a Release
corrispondente
(il che implica che, se i dati non sono stati copiati, l'oggetto array
verrà bloccato e non potrà essere spostato come parte della compattazione dell'heap).
Devi Release
ogni array di Get
. Inoltre, se la chiamata Get
non va a buon fine, devi assicurarti che il codice non provi a Release
un puntatore NULL
in un secondo momento.
Puoi determinare se i dati sono stati copiati o meno trasmettendo un
puntatore non-NULL per l'argomento isCopy
. Questo accade raramente
utile.
La chiamata Release
accetta un argomento mode
che può avere uno dei tre valori. Le azioni eseguite dal runtime dipendono
se ha restituito un puntatore ai dati effettivi o a una copia:
0
- Effettivo: l'oggetto array non è più bloccato.
- Copia: i dati vengono copiati. Il buffer con la copia viene liberato.
JNI_COMMIT
- Effettivo: non fa nulla.
- Copia: i dati vengono copiati. Il buffer con la copia non viene liberato.
JNI_ABORT
- Effettivo: l'oggetto array non è più bloccato. Prima le scritture non vengono interrotte.
- Copia: il buffer con la copia viene liberato. e tutte le modifiche andranno perse.
Un motivo per controllare il flag isCopy
è sapere se
devi chiamare Release
con JNI_COMMIT
dopo aver apportato modifiche a un array, se in modo alternato
modifiche ed esecuzione di codice che utilizza i contenuti dell'array,
in grado di
ignora il commit autonomo. Un altro possibile motivo per controllare l'indicatore è per gestire in modo efficiente JNI_ABORT
. Ad esempio, potresti volere
per ottenere un array, modificarlo in posizione, passare pezzi ad altre funzioni e
quindi ignora le modifiche. Se sai che JNI sta creando una nuova copia per
non serve creare un altro file "modificabile" copia. Se JNI ti passa l'originale, devi creare una tua copia.
È un errore comune (ripetuto nel codice di esempio) dare per scontato che sia possibile saltare la chiamata a Release
se
*isCopy
è falso. Questo non è il caso. Se non è stato allocato alcun buffer di copia, la memoria originale deve essere bloccata e non può essere spostata dal garbage collector.
Tieni inoltre presente che il flag JNI_COMMIT
non rilascia l'array e, alla fine, dovrai chiamare di nuovo Release
con un flag diverso.
Chiamate alla regione
Esiste un'alternativa a chiamate come Get<Type>ArrayElements
e GetStringChars
, che potrebbero essere molto utili quando vuoi
è copiare i dati. Tieni in considerazione:
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
Questa operazione recupera l'array, copia il primo byte (len
)
e poi rilascia l'array. In base al
la chiamata a Get
fissa o copia l'array
contenuti.
Il codice copia i dati (forse una seconda volta), quindi chiama Release
; in questo caso,JNI_ABORT
garantisce che non ci sia alcuna possibilità di una terza copia.
È possibile ottenere lo stesso risultato in modo più semplice:
env->GetByteArrayRegion(array, 0, len, buffer);
Questo approccio offre diversi vantaggi:
- Richiede una chiamata JNI anziché 2, riducendo l'overhead.
- Non richiede blocchi o copie aggiuntive dei dati.
- Riduce il rischio di errori del programmatore: non c'è il rischio di dimenticare
di chiamare
Release
dopo un errore.
Analogamente, puoi utilizzare la chiamata Set<Type>ArrayRegion
per copiare i dati in un array e GetStringRegion
o GetStringUTFRegion
per copiare i caratteri da un String
.
Eccezioni
Non devi chiamare la maggior parte delle funzioni JNI mentre è in attesa un'eccezione.
Il codice dovrebbe rilevare l'eccezione (tramite il valore restituito della funzione, ExceptionCheck
o ExceptionOccurred
) e restituire, oppure cancellare l'eccezione e gestirla.
Le uniche funzioni JNI che puoi chiamare mentre è in attesa un'eccezione sono:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
Molte chiamate JNI possono rappresentare un'eccezione, ma spesso offrono un modo più semplice
di controllare gli errori. Ad esempio, se NewString
restituisce
un valore non NULL, non devi controllare se è presente un'eccezione. Tuttavia, se
chiami un metodo (utilizzando una funzione come CallObjectMethod
),
devi sempre verificare la presenza di un'eccezione, perché il valore restituito non è
sarà valida se viene generata un'eccezione.
Tieni presente che le eccezioni generate dal codice gestito non annullano lo stack nativo
i frame. Inoltre, le eccezioni C++, generalmente sconsigliate su Android, non devono essere generate oltre il confine di transizione JNI dal codice C++ al codice gestito.
Le istruzioni di JNI Throw
e ThrowNew
imposta un puntatore di eccezione nel thread corrente. Quando torni alla modalità gestita
dal codice nativo, l'eccezione sarà indicata e gestita in modo appropriato.
Il codice nativo può "intercettare" un'eccezione chiamando ExceptionCheck
o
ExceptionOccurred
e cancella con
ExceptionClear
. Come sempre,
Ignorare le eccezioni senza gestirle può causare dei problemi.
Non sono disponibili funzioni predefinite per manipolare l'oggetto Throwable
stesso, quindi se vuoi (ad esempio) ottenere la stringa di eccezione, dovrai trovare la classe Throwable
, cercare l'ID metodo per getMessage "()Ljava/lang/String;"
, invocarlo e, se il risultato è diverso da NULL, utilizzare GetStringUTFChars
per ottenere qualcosa che puoi passare a printf(3)
o a un'altra funzione equivalente.
Controllo esteso
JNI esegue pochissimi controlli di errore. In genere gli errori causano un arresto anomalo. Android offre anche una modalità chiamata CheckJNI, in cui i puntatori delle tabelle delle funzioni JavaVM e JNIEnv vengono convertiti in tabelle di funzioni che eseguono una serie estesa di controlli prima di chiamare l'implementazione standard.
I controlli aggiuntivi includono:
- Array: tentativo di allocare un array di dimensioni negative.
- Puntatori errati: passaggio di un jarray/jclass/jobject/jstring non valido a una chiamata JNI o passaggio di un puntatore NULL a una chiamata JNI con un argomento non nullo.
- Nomi di classi: passa qualsiasi cosa tranne lo stile "java/lang/String" del nome di classe a una chiamata JNI.
- Chiamate critiche: esecuzione di una chiamata JNI tra un get "critico" e la relativa release.
- Direct ByteBuffers: passaggio di argomenti non validi a
NewDirectByteBuffer
. - Eccezioni: si effettua una chiamata JNI se c'è un'eccezione in attesa.
- JNIEnv*s: viene utilizzato un JNIEnv* del thread sbagliato.
- jfieldIDs: utilizzo di jfieldID NULL o di jfieldID per impostare un campo su un valore del tipo errato (tentando di assegnare un StringBuilder a un campo String, ad esempio), oppure utilizzando un jfieldID per un campo statico per impostare un campo di istanza o viceversa oppure utilizzando un jfieldID di una classe con istanze di un'altra classe.
- jmethodIDs: utilizzo del tipo jmethodID sbagliato quando si effettua una chiamata JNI
Call*Method
: tipo restituito errato, mancata corrispondenza statica/non statica, tipo errato per "this" (per le chiamate non statiche) o classe errata (per le chiamate statiche). - Riferimenti: utilizzo di
DeleteGlobalRef
/DeleteLocalRef
per il tipo di riferimento sbagliato. - Modalità di rilascio: passaggio di una modalità di rilascio non valida a una chiamata di rilascio (qualcosa di diverso da
0
,JNI_ABORT
oJNI_COMMIT
). - Sicurezza dei tipi: restituzione di un tipo incompatibile dal metodo nativo (restituendo un StringBuilder da un metodo dichiarato per restituire una stringa, ad esempio).
- UTF-8: passaggio di una sequenza di byte Modificata UTF-8 non valida a una chiamata JNI.
L'accessibilità dei metodi e dei campi non è ancora selezionata: le limitazioni di accesso non si applicano al codice nativo.
Esistono diversi modi per abilitare CheckJNI.
Se utilizzi l'emulatore, CheckJNI è attivo per impostazione predefinita.
Se hai un dispositivo rooted, puoi utilizzare la seguente sequenza di comandi per riavviare il runtime con CheckJNI abilitato:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
In entrambi i casi, all'avvio del runtime nell'output di logcat verrà visualizzato qualcosa di simile:
D AndroidRuntime: CheckJNI is ON
Se hai un dispositivo normale, puoi utilizzare il seguente comando:
adb shell setprop debug.checkjni 1
Ciò non influirà sulle app già in esecuzione, ma qualsiasi app lanciata da quel momento in poi avrà CheckJNI abilitato. Se imposti la proprietà su un altro valore o se riavvii il dispositivo, CheckJNI verrà disattivato di nuovo. In questo caso, la prossima volta che verrà avviata un'app nell'output di logcat vedrai qualcosa di simile al seguente:
D Late-enabling CheckJNI
Puoi anche impostare l'attributo android:debuggable
nel file manifest della tua applicazione su
attiva CheckJNI solo per la tua app. Tieni presente che gli Strumenti di build Android eseguono questa operazione automaticamente
determinati tipi di build.
Librerie native
Puoi caricare il codice nativo dalle librerie condivise con il codice standard
System.loadLibrary
.
In pratica, le versioni precedenti di Android presentavano bug in PackageManager che causavano l'installazione e l'aggiornamento delle librerie native in modo che sia inaffidabile. Il progetto ReLinker offre soluzioni alternative per questo e altri problemi di caricamento delle librerie native.
Chiama System.loadLibrary
(o ReLinker.loadLibrary
) da un corso statico
come inizializzatore. L'argomento è "undecorated" nome della libreria,
quindi per caricare libfubar.so
devi passare in "fubar"
.
Se hai una sola classe con metodi nativi, ha senso che la chiamata a
System.loadLibrary
in un inizializzatore statico per quella classe. In caso contrario,
vuoi effettuare la chiamata da Application
, così sai che la libreria è sempre caricata,
e vengono sempre caricati in anticipo.
Esistono due modi in cui il runtime può trovare i tuoi metodi nativi. Puoi scegliere di
registrali con RegisterNatives
oppure puoi consentire al runtime di cercarli in modo dinamico
con dlsym
. Il vantaggio di RegisterNatives
è la visibilità
controllando l'esistenza dei simboli. Inoltre, puoi avere raccolte condivise più piccole e più veloci evitando
esportando qualsiasi cosa tranne JNI_OnLoad
. Il vantaggio di permettere al runtime di rilevare
è che c'è una quantità di codice leggermente inferiore da scrivere.
Per usare RegisterNatives
:
- Fornisci una funzione
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - In
JNI_OnLoad
, registra tutti i metodi nativi utilizzandoRegisterNatives
. - Esegui la compilazione con uno script di versione (opzione preferita) o utilizza
-fvisibility=hidden
in modo che soloJNI_OnLoad
venga esportato dalla raccolta. In questo modo viene prodotto codice più veloce e più piccolo ed evitate potenziali collisioni con altre librerie caricate nell'app (ma vengono create tracce dello stack meno utili se l'app si arresta in modo anomalo in codice nativo).
L'inizializzatore statico dovrebbe avere il seguente aspetto:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
La funzione JNI_OnLoad
dovrebbe avere il seguente aspetto se scritta in 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; }
Per utilizzare invece "discovery" metodi nativi, devi assegnare loro un nome specifico (vedi le specifiche JNI per maggiori dettagli). Ciò significa che se la firma di un metodo è errata, non lo saprai fino a quando la prima volta che il metodo viene effettivamente richiamato.
Qualsiasi chiamata FindClass
effettuata da JNI_OnLoad
risolverà i corsi in
contesto del caricatore classi utilizzato per caricare la libreria condivisa. Quando viene chiamato da altri
contesti, FindClass
utilizza il caricatore di classi associato al metodo nella parte superiore della
pila Java oppure, se non è presente (perché la chiamata proviene da un thread nativo appena collegato),
utilizza il caricatore di classi "system". Il caricatore delle classi di sistema non conosce le classi della tua applicazione, pertanto non potrai cercare le tue classi con FindClass
in questo contesto. In questo modo JNI_OnLoad
è una soluzione comoda per cercare e memorizzare nella cache le classi:
disponi di un riferimento globale jclass
valido
puoi usarlo da qualsiasi thread allegato.
Chiamate native più veloci con @FastNative
e @CriticalNative
I metodi nativi possono essere annotati con
@FastNative
oppure
@CriticalNative
(ma non entrambi) per velocizzare le transizioni tra codice gestito e codice nativo. Tuttavia, queste annotazioni
presentano alcuni cambiamenti nel comportamento, che devono essere valutati con attenzione prima dell'uso. Anche se
menziona brevemente queste modifiche di seguito, fai riferimento alla documentazione per i dettagli.
L'annotazione @CriticalNative
può essere applicata solo ai metodi nativi che non
utilizzare oggetti gestiti (nei parametri o nei valori restituiti o come this
implicito) e questo
l'annotazione cambia l'ABI di transizione JNI. L'implementazione nativa deve escludere
JNIEnv
e jclass
dalla firma della funzione.
Durante l'esecuzione di un metodo @FastNative
o @CriticalNative
, la garbage
raccolta non può sospendere il thread per lavoro essenziale e potrebbe essere bloccato. Non utilizzare queste annotazione per i metodi che richiedono molto tempo, inclusi i metodi generalmente rapidi, ma generalmente illimitati.
In particolare, il codice non deve eseguire importanti operazioni di I/O o acquisire blocchi nativi che
può essere mantenuto
a lungo termine.
Queste annotazioni sono state implementate per l'uso da parte del sistema
Android 8.
ed è diventata pubblica
sottoposta a test CTS
in Android 14. È probabile che queste ottimizzazioni funzionino anche sui dispositivi con Android 8-13 (sebbene
senza le forti garanzie CTS), ma la ricerca dinamica dei metodi nativi è supportata solo
Android 12 o versioni successive, è obbligatoria la registrazione esplicita con JNI RegisterNatives
per l'esecuzione su Android 8-11. Queste annotazioni vengono ignorate su Android 7-, a causa della mancata corrispondenza tra ABI
di @CriticalNative
comporterebbe un marshalling di argomenti errati e probabili arresti anomali.
Per i metodi critici per le prestazioni che richiedono queste annotazioni, ti consigliamo vivamente di
registrare esplicitamente i metodi con JNI RegisterNatives
anziché fare affidamento
"discovery" basata sul nome dei metodi nativi. Per ottenere prestazioni ottimali dall'avvio dell'app, ti consigliamo di
per includere i chiamanti dei metodi @FastNative
o @CriticalNative
nella
profilo di riferimento. Da Android 12,
una chiamata a un metodo nativo @CriticalNative
da un metodo gestito compilato è quasi
tanto economica quanto una chiamata non in linea in C/C++ purché tutti gli argomenti rientrino nei registri (ad esempio fino a
8 argomenti interi e fino a 8 argomenti in virgola mobile su arm64).
A volte può essere preferibile suddividere un metodo nativo in due, uno molto veloce che può non riuscire e un altro che gestisce i casi lenti. Ad esempio:
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);
Considerazioni relative al formato a 64 bit
Per supportare le architetture che utilizzano puntatori a 64 bit, utilizza un campo long
anziché un
int
quando archivi un puntatore a una struttura nativa in un campo Java.
Funzionalità non supportate/compatibilità con versioni precedenti
Sono supportate tutte le funzionalità di JNI 1.6, con la seguente eccezione:
DefineClass
non è implementato. Android non utilizza bytecode o file di classe Java, quindi la trasmissione di dati di classi binarie non funziona.
Per la compatibilità con le versioni precedenti di Android, potresti dover tenere conto di quanto segue:
- Ricerca dinamica delle funzioni native
Fino ad Android 2.0 (Eclair), il token "$" non era corretto convertito in "_00024" durante le ricerche dei nomi dei metodi. Operazione in corso... richiede l'uso di una registrazione esplicita o lo spostamento o metodi nativi fuori dalle classi interne.
- Scollegare i thread
Fino ad Android 2.0 (Eclair), non era possibile utilizzare una
pthread_key_create
funzione di distruttore per evitare il controllo "il thread deve essere scollegato prima dell'uscita". Il runtime usa anche una funzione distruttiva della chiave pthread, quindi sarebbe una gara per vedere chi viene chiamato per primo.) - Riferimenti globali deboli
Fino ad Android 2.2 (Froyo), non erano stati implementati i riferimenti globali deboli. Le versioni precedenti rifiuteranno categoricamente i tentativi di utilizzo. Puoi utilizzare la modalità le costanti di versione della piattaforma Android per testare il supporto.
Fino ad Android 4.0 (Ice Cream Sandwich), i riferimenti globali deboli potevano essere da passare a
NewLocalRef
,NewGlobalRef
eDeleteWeakGlobalRef
. (La specifica consiglia vivamente ai programmatori di creare riferimenti rigidi a globali deboli prima di farlo nulla, quindi questo non dovrebbe essere affatto limitante.)A partire da Android 4.0 (Ice Cream Sandwich), i riferimenti globali deboli possono essere utilizzati come qualsiasi altro riferimento JNI.
- Riferimenti locali
Fino ad Android 4.0 (Ice Cream Sandwich), i riferimenti locali erano in realtà puntatori diretti. Ice Cream Sandwich ha aggiunto l'indiretta per un miglior garbage collector, ma questo significa che dei bug JNI non sono rilevabili nelle release meno recenti. Consulta Modifiche ai riferimenti locali di JNI in ICS per ulteriori dettagli.
Nelle versioni di Android precedenti ad Android 8.0, il di riferimenti locali è limitato a un limite specifico della versione. A partire da Android 8.0, Android supporta riferimenti locali illimitati.
- Determinazione del tipo di riferimento con
GetObjectRefType
Fino ad Android 4.0 (Ice Cream Sandwich), in seguito all'utilizzo di puntatori diretti (vedi sopra), non è stato possibile implementare
GetObjectRefType
correttamente. Abbiamo invece utilizzato un'euristica che ha esaminato la tabella delle variabili globali deboli, gli argomenti, la tabella delle variabili locali e la tabella delle variabili globali in quest'ordine. La prima volta che ha trovato puntatore diretto, segnalerebbe che il riferimento era del tipo che stava esaminando. Ciò significa, ad esempio, che se hai chiamatoGetObjectRefType
in una jclass globale che si è verificata in modo che sia uguale alla classe jclass passata come argomento implicito all'elemento metodo nativo, riceverestiJNILocalRefType
anzichéJNIGlobalRefType
. @FastNative
e@CriticalNative
Fino ad Android 7, queste annotazioni di ottimizzazione sono state ignorate. La mancata corrispondenza dell'ABI per
@CriticalNative
causerebbe il marshalling errato dell'argomento e probabili arresti anomali.La ricerca dinamica di funzioni native per
@FastNative
e In Android 8-10 non sono stati implementati@CriticalNative
metodi e contiene bug noti in Android 11. Utilizzando queste ottimizzazioni senza è probabile che la registrazione esplicita con JNIRegisterNatives
potrebbero causare arresti anomali su Android 8-11.FindClass
lanciaClassNotFoundException
Per la compatibilità con le versioni precedenti, Android genera
ClassNotFoundException
anzichéNoClassDefFoundError
quando non trova un corsoFindClass
. Questo comportamento è coerente con l'API di riflessione JavaClass.forName(name)
.
Domande frequenti: perché ricevo UnsatisfiedLinkError
?
Quando si lavora sul codice nativo, non è raro riscontrare un errore come il seguente:
java.lang.UnsatisfiedLinkError: Library foo not found
In alcuni casi significa quello che dice: la biblioteca non è stata trovata. In altri casi, la raccolta esiste, ma non è stato possibile aprirla da dlopen(3)
e i dettagli dell'errore sono disponibili nel messaggio dettagliato dell'eccezione.
Motivi comuni per cui potresti visualizzare il messaggio "Libreria non trovata" eccezioni:
- La raccolta non esiste o non è accessibile all'app. Utilizza le funzionalità di
adb shell ls -l <path>
per verificarne la presenza e autorizzazioni. - La biblioteca non è stata creata con l'NDK. Ciò può comportare di dipendenze da funzioni o librerie che non esistono nel dispositivo.
Un'altra classe di errori UnsatisfiedLinkError
è simile a:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
In logcat vedrai:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
Ciò significa che il runtime ha tentato di trovare un metodo corrispondente, ma è stato non riuscito. Alcuni dei motivi più comuni sono:
- La raccolta non viene caricata. Controlla l'output logcat per messaggi sul caricamento della raccolta.
- Impossibile trovare il metodo a causa di una mancata corrispondenza del nome o della firma. Questo
è comunemente causato da:
- Per la ricerca di metodi lazy, mancata dichiarazione delle funzioni C++
con
extern "C"
e visibilità appropriata (JNIEXPORT
). Tieni presente che prima di Ice Cream Sandwich, la macro JNIEXPORT non era corretta, quindi l'utilizzo di un nuovo GCC con un vecchiojni.h
non funzionerà. Puoi utilizzarearm-eabi-nm
per vedere i simboli così come appaiono nella libreria. Se sembrano danneggiati (ad esempio_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
anzichéJava_Foo_myfunc
) o se il tipo di simbolo è una "t" minuscola anziché una "T" maiuscola, devi modificare la dichiarazione. - In caso di registrazione esplicita, si verificano errori minori durante l'inserimento del
la firma del metodo. Assicurati che i dati passati alla chiamata di registrazione corrispondano alla firma nel file di log.
Ricorda che "B" è
byte
e "Z" èboolean
. I componenti del nome della classe nelle firme iniziano con "L", terminano con ";'", utilizza "/" per separare i nomi dei pacchetti/classi e utilizza "$" per separare i nomi delle classi interne (ad es.Ljava/util/Map$Entry;
).
- Per la ricerca di metodi lazy, mancata dichiarazione delle funzioni C++
con
L'utilizzo di javah
per generare automaticamente intestazioni JNI potrebbe essere utile
evitare alcuni problemi.
Domande frequenti: perché FindClass
non ha trovato il mio corso?
(Gran parte di questi consigli si applica altrettanto bene al mancato rilevamento di metodi
con GetMethodID
o GetStaticMethodID
, o campi
con GetFieldID
o GetStaticFieldID
.
Assicurati che il formato della stringa del nome della classe sia corretto. lezione JNI
iniziano con il nome del pacchetto e sono separati da barre,
ad esempio java/lang/String
. Se stai cercando una classe array,
devi iniziare con il numero appropriato di parentesi quadre
deve anche aggregare il corso con la lettera "L" e ';", quindi un array unidimensionale
String
corrisponde a [Ljava/lang/String;
.
Se stai cercando una classe interna, utilizza "$" anziché ".". In generale,
l'utilizzo di javap
nel file .class è un buon modo per scoprire il
nome interno della classe.
Se attivi la riduzione del codice, assicurati di configurare il codice da conservare. Configurazione in corso... delle regole di conservazione è importante perché lo shrinker di codice potrebbe altrimenti rimuovere classi, metodi, o campi usati solo da JNI.
Se il nome della classe sembra corretto, potresti riscontrare un problema con il caricatore di classi. FindClass
vuole avviare la ricerca del corso nel
classloader associato al tuo codice. Esamina lo stack di chiamate,
che avrà il seguente aspetto:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
Il metodo più alto è Foo.myfunc
. FindClass
trova l'oggetto ClassLoader
associato all'oggetto Foo
e la utilizza.
Di solito fa quello che vuoi. Puoi incorrere in problemi se crei un thread autonomamente (ad esempio chiamando pthread_create
e poi collegandolo con AttachCurrentThread
). Ora non ci sono frame dello stack della tua applicazione.
Se chiami FindClass
da questo thread,
JavaVM verrà avviata nel "sistema" classloader, anziché quello associato
con la tua applicazione, quindi i tentativi di trovare classi specifiche dell'app non andranno a buon fine.
Ci sono alcuni modi per risolvere questo problema:
- Esegui la ricerca di
FindClass
una volta, nelJNI_OnLoad
e memorizza nella cache i riferimenti alla classe per un secondo momento per gli utilizzi odierni. Eventuali chiamate aFindClass
effettuate nell'ambito dell'esecuzione diJNI_OnLoad
utilizzeranno il caricatore di classi associato alla funzione che ha chiamatoSystem.loadLibrary
(si tratta di una regola speciale, fornita per semplificare l'inizializzazione della libreria). Se il codice dell'app carica la libreria,FindClass
utilizzerà il caricatore di classi corretto. - Passa un'istanza della classe alle funzioni che devono
dichiarando il tuo metodo nativo per prendere un argomento Class
e poi passare
Foo.class
. - Memorizza nella cache un riferimento all'oggetto
ClassLoader
da qualche parte ed emettere direttamente le chiamateloadClass
. Ciò richiede un po' di fatica.
Domande frequenti: come faccio a condividere dati non elaborati con il codice nativo?
Potresti trovarti in una situazione in cui hai bisogno di accedere a un un buffer di dati non elaborati da codice sia gestito che nativo. Esempi comuni includere la manipolazione delle bitmap o dei campioni audio. Esistono due approcci di base.
Puoi archiviare i dati in un byte[]
. Ciò consente un accesso molto rapido dal codice gestito. Dal punto di vista dei nativi, tuttavia,
non è garantito di poter accedere ai dati senza doverli copiare. In alcune implementazioni, GetByteArrayElements
e GetPrimitiveArrayCritical
restituiranno puntatori effettivi ai dati non elaborati nell'heap gestito, ma in altre verrà allocato un buffer nell'heap nativo e i dati verranno copiati.
L'alternativa è archiviare i dati in un buffer di byte diretto. Questi
possono essere creati con java.nio.ByteBuffer.allocateDirect
oppure
la funzione JNI NewDirectByteBuffer
. A differenza delle normali
buffer di byte, lo spazio di archiviazione non è allocato nell'heap gestito e può
essere sempre accessibile direttamente da codice nativo (recupera l'indirizzo
con GetDirectBufferAddress
). In base a quanto sono diretti
L'accesso al buffer di byte viene implementato e accede ai dati dal codice gestito
può essere molto lento.
La scelta di quale utilizzare dipende da due fattori:
- La maggior parte degli accessi ai dati avverrà da codice scritto in Java o in C/C++?
- Se i dati vengono infine passati a un'API di sistema, quale modulo
deve essere inserito? Ad esempio, se alla fine i dati vengono passati a un
che prende un byte[], eseguendo l'elaborazione in una
ByteBuffer
potrebbe non saperlo.
Se non c'è un vincitore chiaro, utilizza un buffer di byte diretto. Il supporto per queste funzionalità è integrato direttamente in JNI e il rendimento dovrebbe migliorare nelle release future.