Suggerimenti per JNI

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 o JNI_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 utilizzando RegisterNatives.
  • Esegui la compilazione con uno script di versione (opzione preferita) o utilizza -fvisibility=hidden in modo che solo JNI_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 e DeleteWeakGlobalRef. (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 chiamato GetObjectRefType in una jclass globale che si è verificata in modo che sia uguale alla classe jclass passata come argomento implicito all'elemento metodo nativo, riceveresti JNILocalRefType 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 JNI RegisterNatives potrebbero causare arresti anomali su Android 8-11.

  • FindClass lancia ClassNotFoundException

    Per la compatibilità con le versioni precedenti, Android genera ClassNotFoundException anziché NoClassDefFoundError quando non trova un corso FindClass. Questo comportamento è coerente con l'API di riflessione Java Class.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 vecchio jni.h non funzionerà. Puoi utilizzare arm-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;).

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, nel JNI_OnLoad e memorizza nella cache i riferimenti alla classe per un secondo momento per gli utilizzi odierni. Eventuali chiamate a FindClass effettuate nell'ambito dell'esecuzione di JNI_OnLoad utilizzeranno il caricatore di classi associato alla funzione che ha chiamato System.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 chiamate loadClass. 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:

  1. La maggior parte degli accessi ai dati avverrà da codice scritto in Java o in C/C++?
  2. 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.