JNI è l'interfaccia nativa Java. Definisce un modo per il bytecode compilato da Android a partire dal codice gestito (scritto nei linguaggi di programmazione Java o Kotlin) per interagire con il codice nativo (scritto in C/C++). JNI è indipendente dal fornitore, supporta il caricamento di codice da librerie condivise dinamiche e, anche se a volte ingombrante, è ragionevolmente efficiente.
Nota: poiché Android compila Kotlin in bytecode compatibile con ART in modo simile al linguaggio di programmazione Java, puoi applicare le indicazioni presenti in questa pagina sia ai linguaggi di programmazione Kotlin che a Java in termini di architettura JNI e relativi costi. Per scoprire di più, consulta Kotlin e Android.
Se non la conosci, leggi le specifiche della Java Native Interface Specification per avere un'idea di come funziona JNI e delle funzionalità disponibili. Alcuni aspetti dell'interfaccia non sono immediatamente evidenti alla prima lettura, pertanto potresti trovare a portata di mano le sezioni successive.
Per sfogliare i riferimenti JNI globali e vedere dove vengono creati ed eliminati i riferimenti JNI globali, utilizza la visualizzazione heap JNI in Memory Profiler in Android Studio 3.2 e versioni successive.
Suggerimenti generali
Cerca di ridurre al minimo l'impatto ambientale del livello JNI. In questo caso, ci sono diverse dimensioni da considerare. La tua soluzione JNI deve seguire queste linee guida (elencate di seguito in ordine di importanza, a partire dal più importante):
- Riduci al minimo il marshalling delle risorse nel livello JNI. Il marshalling attraverso il livello JNI ha costi non banali. Prova a progettare un'interfaccia che minimizzi la quantità di dati necessari per il marshalling e la frequenza con cui eseguirlo.
- Evita la comunicazione asincrona tra il codice scritto in un linguaggio di programmazione gestito e il codice scritto in C++, se possibile. In questo modo la tua interfaccia JNI sarà più facile da gestire. In genere, puoi semplificare gli aggiornamenti asincroni dell'interfaccia utente mantenendo l'aggiornamento asincrono nella stessa lingua dell'interfaccia utente. Ad esempio, invece di richiamare una funzione C++ dal thread dell'interfaccia utente nel codice Java tramite JNI, è meglio eseguire un callback tra due thread nel linguaggio di programmazione Java, con uno di questi che effettua una chiamata C++ che blocca e quindi invia una notifica al thread dell'interfaccia utente quando la chiamata di blocco è completata.
- Riduci al minimo il numero di thread che devono essere toccati o toccati da JNI. Se devi utilizzare pool di thread in entrambi i linguaggi Java e C++, prova a mantenere la comunicazione JNI tra i proprietari del pool anziché tra i singoli thread di worker.
- Mantieni il codice dell'interfaccia in un numero ridotto di posizioni di origine C++ e Java facilmente identificate per facilitare i refactoring futuri. Valuta la possibilità di utilizzare una libreria di generazione automatica JNI, a seconda dei casi.
JavaVM e JNIEnv
JNI definisce due strutture di dati chiave: "JavaVM" e "JNIEnv". Entrambi sono essenzialmente puntatori a puntatori alle tabelle di funzione. (Nella versione C++, sono classi con un puntatore a una tabella di funzione e una funzione membro per ogni funzione JNI che indirizza tramite la tabella). JavaVM fornisce le funzioni di "interfaccia di chiamata" che consentono di creare ed eliminare una JavaVM. In teoria si possono avere più JavaVM per processo, ma Android ne consente una sola.
JNIEnv fornisce la maggior parte delle funzioni JNI. Tutte le tue funzioni native ricevono un JNIEnv come primo argomento, tranne che per i metodi @CriticalNative
, vedi chiamate native più veloci.
JNIEnv viene utilizzato per l'archiviazione locale dei thread. Per questo motivo, non puoi condividere un file JNIEnv tra thread.
Se una porzione di codice non ha un altro modo per ottenere la sua JNIEnv, devi condividere
la JavaVM e usare GetEnv
per scoprire la JNIEnv del thread. (Supponendo che ne abbia uno; vedi AttachCurrentThread
sotto.)
Le dichiarazioni C di JNIEnv e JavaVM sono diverse dalle dichiarazioni C++. Il file di inclusione "jni.h"
fornisce typedef diversi a seconda che sia incluso in C o C++. Per questo motivo non è una buona idea includere argomenti JNIEnv nei file di intestazione inclusi da entrambe le lingue. In altri termini, se il file di intestazione richiede #ifdef __cplusplus
, potresti dover svolgere un po' di lavoro extra se l'intestazione fa riferimento a JNIEnv.
Thread
Tutti i thread sono thread Linux, pianificati dal kernel. Di solito
vengono iniziate da codice gestito (utilizzando Thread.start()
),
ma possono anche essere create altrove e poi associate a JavaVM
. Ad esempio, un thread iniziato con pthread_create()
o std::thread
può essere collegato utilizzando le funzioni AttachCurrentThread()
o AttachCurrentThreadAsDaemon()
. Finché un thread non è allegato, non ha JNIEnv e non può effettuare chiamate JNI.
In genere è preferibile usare Thread.start()
per creare thread che devono effettuare una chiamata al codice Java. In questo modo ti assicuri di avere spazio sufficiente per lo stack, di essere nel file ThreadGroup
corretto e di utilizzare lo stesso ClassLoader
del codice Java. Inoltre, è più facile impostare il nome del thread per il debug in Java anziché nel 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 vuoi un pthread_t
).
Il collegamento di un thread creato in modo nativo comporta la creazione e l'aggiunta di un oggetto java.lang.Thread
al ThreadGroup
"principale", rendendolo visibile al debugger. Chiamare AttachCurrentThread()
in un thread già allegato è un gioco da ragazzi.
Android non sospende i thread che eseguono codice nativo. Se la garbage collection è in corso o il debugger ha inviato una richiesta di sospensione, Android metterà in pausa il thread la volta successiva che effettuerà una chiamata JNI.
I thread collegati tramite JNI devono chiamare DetachCurrentThread()
prima di uscire.
Se programmare questa procedura direttamente è complicato, in Android 2.0 (Eclair) e versioni successive
puoi utilizzare pthread_key_create()
per definire una funzione
distruttore che verrà chiamata prima dell'uscita del thread e
chiamare DetachCurrentThread()
da lì. Utilizza questa
chiave con pthread_setspecific()
per archiviare JNIEnv in
thread-local-storage; in questo modo verrà passata al tuo distruttore come
argomento.)
jclass, jmethodID e jfieldID
Per accedere al campo di un oggetto dal codice nativo:
- Recupera il riferimento dell'oggetto di classe per la classe con
FindClass
- Recupera l'ID campo per il campo con
GetFieldID
- Ottieni i contenuti del campo con qualcosa di appropriato, come
GetIntField
Allo stesso modo, per chiamare un metodo, ottieni prima un riferimento all'oggetto di classe e poi un ID metodo. Gli ID sono spesso solo puntatori alle strutture di dati del runtime interno. La ricerca potrebbe richiedere diversi confronti di stringhe, ma una volta ottenuta la chiamata per ottenere il campo o richiamare il metodo è molto veloce.
Se le prestazioni sono importanti, è 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 archiviare questi dati in una struttura locale statica.
I riferimenti alla classe, gli ID campo e gli ID metodo sono garantiti e validi fino all'unload della classe. L'unload delle classi viene eseguito solo se tutte le classi associate a un ClassLoader possono essere garbage collection, il che è raro, ma non sarà impossibile in Android. Tuttavia, tieni presente che jclass
è un riferimento di classe e deve essere protetto con una chiamata a NewGlobalRef
(vedi la sezione successiva).
Se vuoi memorizzare gli ID nella cache al caricamento di una classe e memorizzarli automaticamente nella cache se la classe viene scaricata e ricaricata, il modo corretto per inizializzare 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 di ID. Il codice verrà eseguito una sola volta, quando la classe viene inizializzata. Se la classe viene scaricata e
ricaricata, verrà eseguita di nuovo.
Riferimenti locali e globali
Ogni argomento passato a un metodo nativo e quasi tutti gli oggetti restituiti da una funzione JNI sono un "riferimento locale". Ciò significa che è valido per la durata del metodo nativo attuale nel thread corrente. Anche se l'oggetto stesso continua a essere attivo dopo che il metodo nativo è stato restituito, il riferimento non è valido.
Questo vale per tutte le sottoclassi di jobject
, tra cui
jclass
, jstring
e jarray
.
(Il runtime ti avvisa della maggior parte degli usi 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 vuoi conservare un riferimento per un periodo più lungo, devi utilizzare un riferimento "globale". La funzione NewGlobalRef
prende il riferimento locale come argomento e restituisce uno globale.
La validità del riferimento globale è garantita fino a quando non chiami
DeleteGlobalRef
.
Questo pattern viene comunemente usato per la memorizzazione nella cache di una jclass restituita da FindClass
, ad esempio:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Tutti i metodi JNI accettano come argomenti sia riferimenti locali che globali.
È possibile che i riferimenti allo stesso oggetto abbiano valori diversi.
Ad esempio, i valori restituiti di chiamate consecutive a NewGlobalRef
per lo stesso oggetto potrebbero essere diversi.
Per vedere se due riferimenti fanno riferimento allo stesso oggetto, devi usare la funzione IsSameObject
. Non confrontare mai i riferimenti con ==
nel codice nativo.
Una conseguenza è che non devi presumere che i riferimenti agli oggetti siano costanti o univoci nel codice nativo. Il valore che rappresenta un oggetto può essere diverso da una chiamata di un metodo a quella successiva ed è possibile che due oggetti diversi possano avere lo stesso valore in chiamate consecutive. Non utilizzare i valori jobject
come chiavi.
I programmatori sono tenuti a "non allocare in modo eccessivo" i riferimenti locali. In termini pratici, questo significa
che se stai creando un numero elevato di riferimenti locali, magari mentre scorri un array di oggetti,
dovresti liberarli manualmente con
DeleteLocalRef
invece di lasciare che sia JNI a farlo per te. L'implementazione è necessaria solo per prenotare slot per 16 riferimenti locali; pertanto, se te ne servono più, devi eliminarli man mano che procedi o utilizzare EnsureLocalCapacity
/PushLocalFrame
per prenotarne altri.
Tieni presente che gli elementi jfieldID
e jmethodID
sono di tipi opachi, non riferimenti a oggetti, e non devono essere passati a NewGlobalRef
. Anche i puntatori di dati non elaborati restituiti da funzioni come GetStringUTFChars
e GetByteArrayElements
non sono oggetti. Possono essere trasmessi da un thread all'altro e sono validi fino alla chiamata di release corrispondente.
Un caso insolito merita una menzione separata. Se colleghi un thread nativo con AttachCurrentThread
, il codice in esecuzione non svincolerà mai automaticamente i riferimenti locali finché il thread non si scollega. Eventuali riferimenti locali creati dovranno essere eliminati manualmente. In generale, è probabile che qualsiasi codice nativo che crei riferimenti locali in un loop debba eseguire l'eliminazione manuale.
Fai attenzione quando utilizzi i riferimenti globali. I riferimenti globali sono inevitabili, ma sono difficili da eseguire il debug e possono causare comportamenti scorretti della memoria difficili da diagnosticare. A parità di altri fattori, probabilmente una soluzione con meno riferimenti globali è la soluzione migliore.
Stringhe UTF-8 e UTF-16
Il linguaggio di programmazione Java utilizza la codifica UTF-16. Per praticità, JNI fornisce anche metodi compatibili con Modified UTF-8. La codifica modificata è utile per il codice C perché codifica \u0000 come 0xc0 0x80 anziché 0x00. L'aspetto positivo è che puoi contare su stringhe con terminazione zero in stile C, adatte all'uso con le funzioni stringa libc standard. Il lato negativo è che non puoi passare i dati UTF-8 arbitrari a JNI e aspettarti che funzionino correttamente.
Per ottenere la rappresentazione UTF-16 di String
, utilizza GetStringChars
.
Tieni presente che le stringhe UTF-16 non hanno terminazione zero e \u0000 è consentito, quindi devi dipendere dalla lunghezza della stringa e dal puntatore jchar.
Non dimenticare di Release
le stringhe che Get
. Le funzioni stringa restituiscono jchar*
o jbyte*
, che sono puntatori di stile C ai dati primitivi anziché a riferimenti locali. Sono garantiti e validi fino alla chiamata di Release
, il che significa che non vengono rilasciati quando viene restituito il metodo nativo.
I dati passati a NewStringUTF devono essere nel formato UTF-8 modificato. Un errore comune è leggere i dati dei caratteri da un file o uno stream di rete e passarli 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 eliminare i caratteri non validi o convertirli nel formato UTF-8 modificato appropriato.
In caso contrario, è probabile che la conversione UTF-16 generi 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, di solito era più veloce usare stringhe UTF-16 in quanto 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 un garbage collector in movimento. Queste funzionalità riducono notevolmente il numero di casi in cui ART
può fornire un puntatore ai dati String
senza creare una copia, anche
per GetStringCritical
. Tuttavia, se la maggior parte delle stringhe elaborate dal codice è breve, nella maggior parte dei casi è possibile evitare l'allocazione e la deallocation utilizzando un buffer allo stack e GetStringRegion
o GetStringUTFRegion
. Ecco alcuni esempi:
constexpr size_t kStackBufferSize = 64u; jchar stack_buffer[kStackBufferSize]; std::unique_ptrheap_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 le funzioni per accedere ai contenuti degli oggetti array. Mentre è necessario accedere agli array di oggetti una voce alla volta, gli array di primitivi possono essere letti e scritti direttamente come se fossero dichiarati in C.
Per rendere l'interfaccia il più efficiente possibile senza limitare l'implementazione della VM, la famiglia di chiamate Get<PrimitiveType>ArrayElements
consente al runtime di restituire un puntatore agli elementi effettivi oppure di allocare una parte di memoria e creare una copia. In ogni caso, la validità del puntatore non elaborato restituito è garantita fino all'emissione della chiamata Release
corrispondente, il che implica che, se i dati non sono stati copiati, l'oggetto dell'array verrà bloccato e non potrà essere spostato come parte della compattazione dell'heap.
Devi Release
ogni array che 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 passando un puntatore non NULL per l'argomento isCopy
. Questo è raramente
utile.
La chiamata Release
richiede un argomento mode
che può
avere uno di tre valori. Le azioni eseguite dal runtime dipendono
dalla restituzione di un puntatore ai dati effettivi o a una copia dei dati:
0
- Effettivo: l'oggetto array non è 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 è bloccato. Le scritture precedenti non vengono interrotte.
- Copia: il buffer con la copia viene liberato; tutte le modifiche apportate 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 si alternano modifiche ed esecuzione di codice che utilizza i contenuti dell'array, potresti essere in grado di saltare il commit no-op. Un altro motivo possibile per controllare il flag è per una gestione efficiente di JNI_ABORT
. Ad esempio, potresti voler ottenere un array, modificarlo, passare parti ad altre funzioni e quindi ignorare le modifiche. Se sai che JNI sta creando una nuova copia per te, non c'è bisogno di crearne un'altra "modificabile". Se JNI passa
l'originale, devi crearne una copia personalizzata.
È un errore comune (ripetuto nel codice di esempio) presumere che tu possa saltare la chiamata Release
se
*isCopy
è falso. Non è così. Se non è stato allocato alcun buffer della 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 a livello di regione
Esiste un'alternativa a chiamate come Get<Type>ArrayElements
e GetStringChars
, che può essere molto utile quando vuoi soltanto
copiare i dati in entrata o in uscita. Tieni in considerazione:
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
Questo recupera l'array, copia i primi len
elementi di byte
dall'interno e rilascia l'array. A seconda dell'implementazione, la chiamata Get
blocca o copia i contenuti dell'array.
Il codice copia i dati (forse per la seconda volta) e poi chiama Release
; in questo caso
JNI_ABORT
garantisce che non venga inviata una terza copia.
Puoi eseguire la stessa operazione in modo più semplice:
env->GetByteArrayRegion(array, 0, len, buffer);
Questo comporta diversi vantaggi:
- Richiede una chiamata JNI invece di 2, riducendo l'overhead.
- Non richiede blocco o copie dei dati aggiuntive.
- Riduce il rischio di errore del programmatore, senza rischiare di dimenticare di chiamare
Release
in caso di errore.
Analogamente, puoi utilizzare la chiamata Set<Type>ArrayRegion
per copiare dati in un array e GetStringRegion
o
GetStringUTFRegion
per copiare caratteri da un
String
.
Eccezioni
Non devi chiamare la maggior parte delle funzioni JNI mentre c'è un'eccezione in sospeso.
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 sospeso un'eccezione sono:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
Molte chiamate JNI possono generare un'eccezione, ma spesso offrono un modo più semplice
per verificare la presenza di errori. Ad esempio, se NewString
restituisce
un valore non NULL, non è necessario cercare un'eccezione. Tuttavia, se chiami un metodo (utilizzando una funzione come CallObjectMethod
), devi sempre verificare se esiste un'eccezione, poiché il valore restituito non è valido se è stata generata un'eccezione.
Tieni presente che le eccezioni generate dal codice gestito non annullano i frame dello stack nativi. (Inoltre, le eccezioni C++, generalmente sconsigliate su Android, non devono essere
limitate al limite di transizione JNI dal codice C++ al codice gestito.)
Le istruzioni Throw
e ThrowNew
per JNI impostano
un puntatore di eccezione nel thread corrente. Quando torni a gestito dal codice nativo, l'eccezione viene annotata e gestita in modo appropriato.
Il codice nativo può "acquisire" un'eccezione chiamando ExceptionCheck
o
ExceptionOccurred
e cancellarla con
ExceptionClear
. Come di consueto,
eliminare le eccezioni senza gestirle può causare problemi.
Non ci sono funzioni integrate per manipolare l'oggetto Throwable
in sé, quindi se vuoi (ad esempio) ottenere la stringa di eccezione dovrai trovare la classe Throwable
, cercare l'ID metodo per getMessage "()Ljava/lang/String;"
, chiamarlo e, se il risultato è non NULL, usa GetStringUTFChars
per ottenere qualcosa che puoi inviare a printf(3)
o equivalente.
Verifica estesa
JNI esegue un controllo degli errori minimo. Generalmente gli errori causano un arresto anomalo. Android offre anche una modalità chiamata CheckJNI, in cui i puntatori della tabella delle funzioni JavaVM e JNIEnv vengono spostati 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: passare un jarray/jclass/jobject/jstring non valido a una chiamata JNI o passare un puntatore NULL a una chiamata JNI con un argomento non nullable.
- Nomi di classi: passare qualsiasi cosa tranne lo stile "java/lang/String" del nome della classe a una chiamata JNI.
- Chiamate critiche: effettuare una chiamata JNI tra un get "critico" e una release corrispondente.
- ByteBuffers diretto: passaggio di argomenti non validi a
NewDirectByteBuffer
. - Eccezioni: effettuare una chiamata JNI mentre è in sospeso un'eccezione.
- JNIEnv*s: utilizzo di un JNIEnv* dal thread sbagliato.
- jfieldID: utilizzo di un jfieldID NULL o di un jfieldID per impostare un campo su un valore del tipo sbagliato (ad esempio per tentare di assegnare un StringBuilder a un campo String) o utilizzo di un jfieldID per un campo statico per impostare un campo di istanza o viceversa oppure utilizzo di un jfieldID di una classe con istanze di un'altra classe.
- jmethodIDs: utilizzo del tipo sbagliato di jmethodID quando si effettua una chiamata JNI
Call*Method
: tipo restituito errato, mancata corrispondenza statica/non statica, tipo errato per "questo" (per le chiamate non statiche) o classe errata (per le chiamate statiche). - Riferimenti: utilizzo di
DeleteGlobalRef
/DeleteLocalRef
con il tipo di riferimento sbagliato. - Modalità di rilascio: passaggio di una modalità di rilascio non valida a una chiamata di rilascio (a un valore diverso da
0
,JNI_ABORT
oJNI_COMMIT
). - Sicurezza del tipo: restituisce un tipo incompatibile dal tuo metodo nativo (restituendo un StringBuilder da un metodo dichiarato per restituire una stringa, ad esempio).
- UTF-8: passare una sequenza di byte Modified UTF-8 non valida a una chiamata JNI.
L'accessibilità di metodi e campi non è ancora stata selezionata: le limitazioni di accesso non vengono applicate 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 logcat verrà visualizzato qualcosa di simile al seguente:
D AndroidRuntime: CheckJNI is ON
Se hai un dispositivo standard, puoi utilizzare il seguente comando:
adb shell setprop debug.checkjni 1
Questa operazione non interessa le app già in esecuzione, ma per tutte le app avviate da quel momento in poi CheckJNI sarà abilitato. Modifica la proprietà impostandola su un altro valore o il semplice riavvio disabiliterà di nuovo CheckJNI.) In questo caso, all'avvio successivo di un'app, nell'output logcat verrà visualizzato un risultato simile al seguente:
D Late-enabling CheckJNI
Puoi anche impostare l'attributo android:debuggable
nel file manifest della tua applicazione per attivare CheckJNI solo per la tua app. Tieni presente che gli strumenti di build Android eseguono questa operazione automaticamente per determinati tipi di build.
Librerie native
Puoi caricare il codice nativo da librerie condivise con lo standard System.loadLibrary
.
In pratica, le versioni precedenti di Android presentavano bug in PackageManager che rendevano inaffidabili l'installazione e l'aggiornamento delle librerie native. Il progetto ReLinker offre soluzioni alternative per questo e per altri problemi di caricamento delle librerie native.
Richiama System.loadLibrary
(o ReLinker.loadLibrary
) da un inizializzatore
di classi statico. L'argomento è il nome della libreria "undecorated",
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
sia in un inizializzatore statico per quella classe. In caso contrario, potresti voler effettuare la chiamata da Application
, in modo da sapere che la libreria è sempre caricata e sempre caricata in anticipo.
Esistono due modi in cui il runtime può trovare i metodi nativi. Puoi registrarli esplicitamente con RegisterNatives
oppure consentire al runtime di cercarli in modo dinamico con dlsym
. Il vantaggio di RegisterNatives
è che puoi verificare fin dall'inizio l'esistenza dei simboli. In più, puoi disporre di librerie condivise più piccole e veloci perché puoi esportare solo JNI_OnLoad
. Il vantaggio di far scoprire le tue funzioni al runtime
è che la quantità di codice da scrivere è leggermente inferiore.
Per usare RegisterNatives
:
- Specifica una funzione
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - Nel tuo
JNI_OnLoad
, registra tutti i tuoi metodi nativi utilizzandoRegisterNatives
. - Crea con
-fvisibility=hidden
per esportare solo il tuoJNI_OnLoad
dalla raccolta. Questo produce codice più veloce e più piccolo ed evita potenziali collisioni con altre librerie caricate nella tua app (ma crea analisi dello stack meno utili se la tua app ha un arresto anomalo nel codice nativo).
L'inizializzatore statico dovrebbe essere simile al seguente:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
Se scritta in C++, la funzione JNI_OnLoad
dovrebbe avere il seguente aspetto:
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; }
Se invece vuoi utilizzare la "scoperta" dei metodi nativi, devi assegnare loro un nome specifico (per i dettagli, consulta le specifiche JNI). Ciò significa che se la firma di un metodo è errata, non ne sarai a conoscenza fino alla prima volta in cui il metodo viene effettivamente richiamato.
Qualsiasi chiamata a FindClass
effettuata da JNI_OnLoad
risolverà le classi nel
contesto del caricatore di classi utilizzato per caricare la libreria condivisa. Quando viene chiamato da altri contesti, FindClass
utilizza il caricatore di classi associato al metodo in cima allo stack di 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, quindi non potrai cercare le tue classi con FindClass
in quel contesto. Questo rende JNI_OnLoad
un posto pratico per cercare e memorizzare nella cache le classi: una volta che hai un riferimento globale jclass
valido, puoi utilizzarlo da qualsiasi thread allegato.
Chiamate native più veloci con @FastNative
e @CriticalNative
Puoi annotare i metodi nativi con @FastNative
o @CriticalNative
(ma non con entrambi) per velocizzare le transizioni tra codice gestito e nativo. Tuttavia, queste annotazioni
comportano alcuni cambiamenti di comportamento che devono essere considerati attentamente prima dell'uso. Anche se di seguito
menzioniamo brevemente queste modifiche, ti invitiamo a fare riferimento alla documentazione per i dettagli.
L'annotazione @CriticalNative
può essere applicata solo ai metodi nativi che non utilizzano oggetti gestiti (nei parametri o nei valori restituiti oppure come this
implicito) e questa annotazione modifica l'ABI di transizione JNI. L'implementazione nativa deve escludere i parametri JNIEnv
e jclass
dalla sua firma della funzione.
Durante l'esecuzione di un metodo @FastNative
o @CriticalNative
, la garbage collection non può sospendere il thread per attività essenziali e potrebbe essere bloccata. Non utilizzare queste annotazioni per i metodi a lunga esecuzione, inclusi quelli solitamente veloci, ma generalmente illimitati.
In particolare, il codice non deve eseguire operazioni di I/O significative o acquisire blocchi nativi che possono essere conservati per molto tempo.
Queste annotazioni sono state implementate per l'uso del sistema da Android 8 e sono diventate API pubbliche testate da CTS in Android 14. È probabile che queste ottimizzazioni funzionino anche sui dispositivi Android 8-13 (sebbene senza le solide garanzie CTS), ma la ricerca dinamica dei metodi nativi è supportata solo su Android 12 e versioni successive. È severamente richiesta la registrazione esplicita con JNI RegisterNatives
per l'esecuzione su Android 8-11. Queste annotazioni vengono ignorate su Android 7. La mancata corrispondenza delle ABI per @CriticalNative
porterebbe a un marshalling di argomenti sbagliato e a probabili arresti anomali.
Per i metodi critici per le prestazioni che richiedono queste annotazioni, è vivamente consigliato registrare esplicitamente i metodi con JNI RegisterNatives
anziché fare affidamento sulla "scoperta" dei metodi nativi basata sui nomi. Per ottenere prestazioni ottimali all'avvio dell'app, ti consigliamo di includere i chiamanti dei metodi @FastNative
o @CriticalNative
nel profilo di riferimento. A partire da Android 12,
una chiamata a un metodo nativo @CriticalNative
da un metodo gestito compilato è quasi
economica di una chiamata non in linea in C/C++, purché tutti gli argomenti rientrino nei registri (ad esempio fino a
8 argomenti integrali e fino a 8 argomenti in virgola mobile su arm64).
A volte è preferibile suddividere un metodo nativo in due: un metodo molto veloce non riuscito e un altro che gestisce i casi lenti. Ecco alcuni esempi:
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 sui 64 bit
Per supportare architetture che utilizzano puntatori a 64 bit, usa un campo long
anziché un int
quando memorizzi un puntatore a una struttura nativa in un campo Java.
Funzionalità non supportate/compatibilità con le versioni precedenti
Tutte le funzionalità di JNI 1.6 sono supportate, con la seguente eccezione:
DefineClass
non implementato. Android non utilizza bytecode o file di classe Java, quindi il trasferimento di dati sulle classi binarie non funziona.
Per la compatibilità con le versioni precedenti di release di Android, potresti dover conoscere:
- Ricerca dinamica di funzioni native
Fino ad Android 2.0 (Eclair), il carattere "$" non è stato convertito correttamente in "_00024" durante la ricerca dei nomi dei metodi. Per risolvere questo problema è necessario utilizzare una registrazione esplicita o spostare i metodi nativi dalle classi interne.
- Scollegamento dei thread
Fino ad Android 2.0 (Eclair), non è stato possibile utilizzare una funzione di distruzione
pthread_key_create
per evitare il controllo "Il thread deve essere scollegato prima dell'uscita". Il runtime utilizza anche una funzione di distruzione della chiave pthread, quindi sarebbe una gara per vedere chi viene chiamato per primo. - Riferimenti globali deboli
Fino ad Android 2.2 (Froyo), i riferimenti globali deboli non sono stati implementati. Le versioni precedenti rifiuteranno con decisione i tentativi di utilizzo. Puoi utilizzare le costanti di versione della piattaforma Android per testare l'assistenza.
Fino ad Android 4.0 (Ice Cream Sandwich), i riferimenti globali deboli potevano essere trasmessi solo a
NewLocalRef
,NewGlobalRef
eDeleteWeakGlobalRef
. (la specifica incoraggia vivamente i programmatori a creare riferimenti a livello globale deboli prima di fare qualsiasi cosa, quindi questo non dovrebbe essere affatto limitato.)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'indirezione necessaria per supportare i garbage collector migliori, ma questo significa che molti bug JNI non sono rilevabili nelle release precedenti. Per maggiori dettagli, consulta Modifiche ai riferimenti locali di JNI in ICS.
Nelle versioni di Android precedenti ad Android 8.0, il numero di riferimenti locali è limitato a un limite specifico per la 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 dei puntatori diretti (vedi sopra), era impossibile implementare correttamente
GetObjectRefType
. Abbiamo invece utilizzato un'euristica che ha esaminato la tabella dei globali deboli, gli argomenti, la tabella dei dati locali e la tabella dei globali in questo ordine. La prima volta che trovava il puntatore diretto, veniva indicato che il riferimento era del tipo che stava esaminando. Ciò significa che, ad esempio, se hai chiamatoGetObjectRefType
su una jclass globale che corrispondeva alla jclass passata come argomento implicito al tuo metodo nativo statico, avresti ricevutoJNILocalRefType
anzichéJNIGlobalRefType
. @FastNative
e@CriticalNative
Fino ad Android 7, queste annotazioni di ottimizzazione venivano ignorate. La mancata corrispondenza dell'ABI per
@CriticalNative
causerebbe il marshalling di argomenti errati e probabili arresti anomali.La ricerca dinamica delle funzioni native per i metodi
@FastNative
e@CriticalNative
non è stata implementata in Android 8-10 e contiene bug noti in Android 11. L'utilizzo di queste ottimizzazioni senza una registrazione esplicita con JNIRegisterNatives
potrebbe causare arresti anomali su Android 8-11.FindClass
lanciaClassNotFoundException
Per la compatibilità con le versioni precedenti, Android genera
ClassNotFoundException
anzichéNoClassDefFoundError
quando una classe non viene trovata daFindClass
. Questo comportamento è coerente con l'API Java riflessioneClass.forName(name)
.
Domande frequenti: perché ricevo UnsatisfiedLinkError
?
Quando si lavora sul codice nativo, non è raro vedere un errore come il seguente:
java.lang.UnsatisfiedLinkError: Library foo not found
In alcuni casi significa quello che dice: la raccolta non è stata trovata. In altri casi, la libreria esiste, ma dlopen(3)
non può aprirla. I dettagli dell'errore sono disponibili nel messaggio dettagliato dell'eccezione.
Motivi comuni per cui potresti riscontrare eccezioni "Libreria non trovata":
- La libreria non esiste o non è accessibile all'app. Usa
adb shell ls -l <path>
per verificarne la presenza e le autorizzazioni. - La raccolta non è stata creata con l'NDK. Ciò può determinare dipendenze per funzioni o librerie che non esistono sul dispositivo.
Un'altra classe di errori UnsatisfiedLinkError
ha il seguente aspetto:
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 di corrispondenza, ma l'operazione non è riuscita. Ecco alcuni motivi comuni:
- La libreria non viene caricata. Controlla l'output di logcat per verificare la presenza di messaggi sul caricamento della libreria.
- Il metodo non è stato trovato a causa di una mancata corrispondenza del nome o della firma. In genere questo problema è causato da:
- Per la ricerca con metodo lazy, impossibile dichiarare le funzioni C++ con
extern "C"
e la visibilità appropriata (JNIEXPORT
). Tieni presente che prima di Ice Cream Sandwich la macro JNIEXPORT non era corretta, pertanto l'utilizzo di un nuovo GCC con una versione precedente dijni.h
non funzionerà. Puoi usarearm-eabi-nm
per vedere i simboli come appaiono nella libreria; se appaiono 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. - Per la registrazione esplicita, errori minori durante l'inserimento della firma del metodo. Assicurati che i dati che passi alla chiamata di registrazione corrispondano alla firma nel file di log.
Ricorda che "B" corrisponde a
byte
e "Z" corrisponde aboolean
. I componenti dei nomi delle classi nelle firme iniziano con "L", terminano con ";", usa "/" per separare i nomi di pacchetti/classi e usa "$" per separare i nomi delle classi interne (ad esempioLjava/util/Map$Entry;
).
- Per la ricerca con metodo lazy, impossibile dichiarare le funzioni C++ con
L'utilizzo di javah
per generare automaticamente intestazioni JNI può aiutarti a evitare alcuni problemi.
Domande frequenti: perché FindClass
non ha trovato il mio corso?
(La maggior parte di questi consigli si applica anche a errori di individuazione dei metodi
con GetMethodID
o GetStaticMethodID
o ai campi
con GetFieldID
o GetStaticFieldID
.)
Assicurati che il formato della stringa del nome della classe sia corretto. I nomi delle classi JNI
iniziano con il nome del pacchetto e sono separati da barre,
ad esempio java/lang/String
. Se cerchi una classe array, devi iniziare con il numero appropriato di parentesi quadre e includere anche "L" e ";" per la classe; di conseguenza, un array unidimensionale di String
sarà [Ljava/lang/String;
.
Se stai cercando una classe interna, usa "$" invece di ".". In generale,
l'uso di javap
nel file .class è un buon modo per scoprire il
nome interno della tua classe.
Se attivi la riduzione del codice, assicurati di configurare il codice da conservare. La configurazione di regole di conservazione corrette è importante perché lo strumento di riduzione del codice potrebbe altrimenti rimuovere classi, metodi o campi utilizzati solo da JNI.
Se il nome della classe è corretto, è possibile che si sia verificato un problema con il caricatore di classi. FindClass
vuole avviare la ricerca dei corsi nel
caricatore dei corsi associato al tuo codice. Esamina lo stack di chiamate,
che sarà simile a questo:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
Il metodo più in alto è Foo.myfunc
. FindClass
trova l'oggetto ClassLoader
associato alla classe Foo
e lo utilizza.
Di solito fa quello che vuoi. Potresti avere problemi se crei un thread autonomamente (ad esempio chiamando pthread_create
e collegandolo poi con AttachCurrentThread
). Ora la tua applicazione non contiene stack frame.
Se chiami FindClass
da questo thread, JavaVM verrà avviato nel caricatore di classi "system" anziché in quello associato alla tua applicazione, quindi i tentativi di trovare classi specifiche dell'app non riusciranno.
Esistono alcuni modi per risolvere il problema:
- Esegui una ricerca
FindClass
una volta, inJNI_OnLoad
e memorizza nella cache i riferimenti delle classi per utilizzarli in seguito. Qualsiasi chiamata aFindClass
effettuata durante l'esecuzione diJNI_OnLoad
utilizzerà 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 sta caricando la raccolta,FindClass
utilizzerà il caricatore di corsi corretto. - Passa un'istanza della classe nelle funzioni che ne hanno bisogno, dichiarando il tuo metodo nativo per prendere un argomento Class e poi passare
Foo.class
. - Memorizza nella cache un riferimento all'oggetto
ClassLoader
in un punto pratico ed esegui direttamente le chiamateloadClass
. Questa operazione richiede un certo impegno.
Domande frequenti: come faccio a condividere dati non elaborati con codice nativo?
Potresti trovarti in una situazione in cui devi accedere a un grande buffer di dati non elaborati, sia da codice gestito che da codice nativo. Gli esempi più comuni includono la manipolazione di bitmap o campioni audio. Ci sono due approcci di base.
Puoi archiviare i dati in un byte[]
. Ciò consente un accesso
molto rapido dal codice gestito. Sul lato nativo, tuttavia, non hai la garanzia di poter accedere ai dati senza doverli copiare. In alcune implementazioni, GetByteArrayElements
e GetPrimitiveArrayCritical
restituiranno puntatori effettivi ai dati non elaborati nell'heap gestito, mentre in altre allocheranno un buffer nell'heap nativo e copiano i dati.
L'alternativa è memorizzare i dati in un buffer di byte diretto. Possono
essere creati con java.nio.ByteBuffer.allocateDirect
o
con la funzione NewDirectByteBuffer
JNI. A differenza dei normali buffer di byte, lo spazio di archiviazione non è allocato nell'heap gestito e puoi sempre accedervi direttamente dal codice nativo (ricevi l'indirizzo con GetDirectBufferAddress
). A seconda di come viene implementato l'accesso diretto al buffer di byte, l'accesso ai dati dal codice gestito può richiedere molto tempo.
La scelta di quale usare 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 passati a un'API di sistema, in quale formato devono essere? Ad esempio, se i dati vengono passati a una funzione che richiede un byte[], l'elaborazione in un
ByteBuffer
diretto potrebbe non funzionare.
Se il risultato è evidente, utilizza un buffer di byte diretto. Il supporto è integrato direttamente in JNI e le prestazioni dovrebbero migliorare nelle release future.