Suggerimenti per JNI

JNI è l'interfaccia nativa Java. Definisce un modo per il bytecode che Android compila da codice gestito (scritto nei linguaggi di programmazione Java o Kotlin) per interagire con il codice nativo (scritto in C/C++). JNI è indipendente dal fornitore e supporta il caricamento di codice da creatività e, anche se a volte ingombrante, è 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 saperne di più, vedi 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 i riferimenti JNI globali, utilizza la vista heap JNI nel 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 tua soluzione JNI dovrebbe provare a seguire queste linee guida (elencate di seguito in ordine di importanza, iniziando con il più importante):

  • Riduci al minimo il marshalling delle risorse nel livello JNI. Marshalling attraverso lo strato JNI ha costi non banali. Cerca di progettare un'interfaccia che riduca al minimo la quantità i dati di cui eseguire il push e la frequenza con cui 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 l'utilizzo di un modello di generazione automatica JNI libreria, a seconda dei casi.

JavaVM e JNIEnv

JNI definisce due strutture di dati chiave: "JavaVM" e "JNIEnv". Entrambi sono essenzialmente puntatori alle tabelle di funzione. (Nella versione C++, sono classi con un un puntatore a una tabella di funzioni e una funzione membro per ogni funzione JNI che indirettamente nella 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 consente una sola.

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 di inclusione "jni.h" fornisce diversi typedef a seconda che sia inclusa in C o C++. Per questo motivo non è una cattiva idea includere argomenti JNIEnv nei file di intestazione inclusi da entrambe le lingue. (In altre parole, se le tue file di intestazione richiede #ifdef __cplusplus; potresti dover eseguire altre operazioni in l'intestazione si riferisce a JNIEnv.)

Thread

Tutti i thread sono thread Linux e pianificati dal kernel. Di solito avviato dal codice gestito (utilizzando Thread.start()), ma possono anche essere creati altrove e quindi collegati al JavaVM. Per ad esempio, un thread iniziato con pthread_create() o std::thread può essere collegato utilizzando il AttachCurrentThread() Funzioni di AttachCurrentThreadAsDaemon(). Fino a quando un thread non viene collegato, non ha JNIEnv e non può effettuare chiamate JNI.

In genere è meglio usare Thread.start() per creare qualsiasi thread che debba per eseguire la chiamata al codice Java. In questo modo ti assicurerai di avere spazio sufficiente per lo stack, nel ThreadGroup corretto e che stai usando lo stesso ClassLoader come 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).

Se alleghi un thread creato in modo nativo, viene generato un java.lang.Thread oggetto da creare e aggiungere al parametro "main" ThreadGroup, rendendola 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ì. (usa questo con pthread_setspecific() per memorizzare JNIEnv thread-local-storage; in questo modo verrà passato al distruttore l'argomento.

jclass, jmethodID e jfieldID

Per accedere al campo di un oggetto dal codice nativo:

  • Ottieni il riferimento all'oggetto 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, otterresti prima un riferimento a un oggetto di 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 le prestazioni sono importanti, è utile cercare i valori una volta e memorizzare i risultati nella cache nel codice nativo. Poiché esiste un limite di una JavaVM per processo, è ragionevole per archiviare 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 tutti gli oggetti sono stati restituiti di una funzione JNI è un "riferimento locale". Ciò significa che è valido per durata del metodo nativo corrente nel thread corrente. Anche se l'oggetto stesso continua a vivere dopo il metodo nativo restituisce i risultati, 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 usi impropri dei riferimenti quando JNI esteso siano attivate.)

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 per lo stesso oggetto potrebbe essere diverso. Per vedere se due riferimenti si riferiscono 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 una chiamata di un metodo all'altra ed è possibile che oggetti diversi potrebbero avere lo stesso valore in chiamate consecutive. Non utilizzare jobject valori 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. Qualsiasi locale i riferimenti creati dovranno essere eliminati manualmente. In generale, qualsiasi asset nativo che crea riferimenti locali in un loop probabilmente deve eseguire l'eliminazione dei dati.

Fai attenzione a utilizzare riferimenti globali. I riferimenti globali possono essere inevitabili, ma sono difficili il debug e può causare (comportamenti errati) 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 di dati UTF-8 arbitrari a JNI e si aspettano che funzionino correttamente.

Per ottenere la rappresentazione UTF-16 di un String, utilizza GetStringChars. Tieni presente che le stringhe UTF-16 non hanno terminazioni zero e \u0000 è consentito, quindi devi attenerti alla lunghezza della stringa e al puntatore jchar.

Non dimenticare di Release le stringhe Get. La le funzioni stringa restituiscono jchar* o jbyte*, che puntatori a dati primitivi in stile C piuttosto che a riferimenti locali. Loro la validità è garantita fino alla chiamata a Release, il che significa che rilasciato quando viene restituito il metodo nativo.

I dati trasmessi a NewStringUTF devono essere nel formato UTF-8 modificato. R errore comune è la lettura dei dati dei caratteri da un file o da uno stream di rete e passarlo a NewStringUTF senza filtrarlo. A meno che tu non sappia che i dati sono in formato MUTF-8 (o ASCII a 7 bit, che è un sottoinsieme compatibile), devi eliminare 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, di solito era più veloce usare le stringhe UTF-16 come Android non richiedevano una copia in GetStringChars, GetStringUTFChars richiedeva un'assegnazione e la 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 o 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 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 Get non va a buon fine, devi assicurarti che il tuo codice non provi a Release di destinazione più avanti.

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ò hanno uno di 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
    • Effettiva: 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 motivo per cui è possibile verificare la segnalazione è la presenza di una gestione efficiente di 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 viene superato l'originale, dovrai creare la 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 il buffer di copia non è stato allocata, la memoria originale deve essere bloccata e non può essere spostata garbage collection.

Nota inoltre che il flag JNI_COMMIT non rilascia l'array, e dovrai chiamare di nuovo Release con un flag diverso e alla fine.

Chiamate 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 acquisisce 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 possibilità di una terza copia.

È possibile ottenere la stessa cosa 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, senza il rischio di dimenticarsi per chiamare Release in caso di errore.

Analogamente, puoi utilizzare la chiamata Set<Type>ArrayRegion per copiare i 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 è in attesa un'eccezione. Il tuo codice dovrebbe notare l'eccezione (tramite il valore restituito della funzione, ExceptionCheck o ExceptionOccurred) e restituire, o eliminare l'eccezione e gestirla.

Le uniche funzioni JNI che puoi chiamare mentre un'eccezione è in attesa 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. (E le eccezioni per C++, generalmente sconsigliate su Android, non devono essere generato attraverso il confine di transizione JNI dal codice C++ al codice gestito.) Le istruzioni di JNI per 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 esistono funzioni integrate per manipolare l'oggetto Throwable Quindi se vuoi (ad esempio) ottenere la stringa di eccezione, dovrai trova la classe Throwable, cerca l'ID metodo getMessage "()Ljava/lang/String;", richiamarla e se il risultato è non NULL, usa GetStringUTFChars per ottenere qualcosa che puoi a printf(3) o 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 trasformati 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: effettuare una chiamata JNI tra un get "critico" e la release corrispondente.
  • 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 sul tipo di riferimento errato.
  • 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 un risultato simile a questo:

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 su qualsiasi app avviata da quel momento in poi sarà abilitato CheckJNI. Modifica la proprietà impostandola su un altro valore o semplicemente riavviando, disattiverà di nuovo CheckJNI. In questo caso, al successivo avvio di un'app verrà visualizzato un risultato simile a questo nell'output di logcat:

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 codice nativo da librerie condivise con 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 ReLinker offre soluzioni alternative per questo e altri problemi di caricamento della libreria nativa.

Chiama System.loadLibrary (o ReLinker.loadLibrary) da un corso statico come inizializzatore. L'argomento è "non decorato" 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.
  • Crea con -fvisibility=hidden in modo che solo il tuo JNI_OnLoad viene esportato dalla raccolta. In questo modo si produce un codice più rapido e di dimensioni ridotte, evitando potenziali collisioni con altre librerie caricate nella tua app (ma vengono create analisi dello stack meno utili) in caso di arresto anomalo dell'app nel 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 un aspetto simile a questo se scritte 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 un altro contesti, FindClass utilizza il caricatore classi associato al metodo nella parte superiore stack Java o se non è presente, perché la chiamata proviene da un thread nativo appena allegato utilizza il "sistema" caricatore di classi. Il caricatore delle classi di sistema non è a conoscenza , perciò non potrai cercare i tuoi corsi che contengono FindClass contesto. In questo modo JNI_OnLoad è una soluzione comoda per cercare e memorizzare nella cache le classi: hai 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 questi per i metodi a lunga esecuzione, inclusi quelli generalmente veloci, 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.

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 causerebbe il 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 economica come una chiamata non in linea in C/C++ purché tutti gli argomenti rientrino nei registri (ad esempio, fino a 8 integrali e fino a 8 argomenti in virgola mobile su arm64).

A volte può essere preferibile dividere un metodo nativo in due, un metodo molto veloce che e un'altra 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, potrebbe essere necessario: tieni presente quanto segue:

  • Ricerca dinamica di 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.

  • Scollegamento dei thread

    Fino ad Android 2.0 (Eclair), non era possibile utilizzare un pthread_key_create funzione di distruttore per evitare "il thread deve essere scollegato prima esci" controllo. 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 venivano implementati i riferimenti globali deboli. Le versioni precedenti rifiuteranno energicamente i tentativi di utilizzo. Puoi utilizzare 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 come qualsiasi altro riferimento JNI.

  • Riferimenti locali

    Fino ad Android 4.0 (Ice Cream Sandwich), i riferimenti locali erano puntatori diretti. Ice Cream Sandwich ha aggiunto l'azione 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 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 utilizzato invece un modello euristico che ha esaminato la tabella dei dati globali deboli, le argomentazioni, le informazioni e la tabella delle globali in questo 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 venivano ignorate. L'ABI la mancata corrispondenza di @CriticalNative genererà un argomento errato marshalling e possibili 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 JavaReflect 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. Nella in altri casi la libreria esiste, ma dlopen(3) non può essere aperta. 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 del metodo lazy, mancata dichiarazione delle funzioni C++ con extern "C" e appropriati visibilità (JNIEXPORT). Tieni presente che prima di un gelato Sandwich, la macro JNIEXPORT non era corretta, quindi usare un nuovo GCC con una vecchia jni.h non funzionerà. Puoi usare arm-eabi-nm per vedere i simboli così come appaiono nella libreria; se sembrano strappato (qualcosa del tipo _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass anziché Java_Foo_myfunc) o se il tipo di simbolo è una "t" minuscola anziché la "T" maiuscola, dovrai modificare la dichiarazione.
    • In caso di registrazione esplicita, si verificano errori minori durante l'inserimento del la firma del metodo. Assicurati che ciò che trasmetti la chiamata di registrazione corrisponda alla firma nel file di log. Ricorda che la lettera "B" è byte e "Z" è boolean. I componenti del nome della classe nelle firme iniziano con "L", terminano con ";", usa "/" per separare i nomi dei pacchetti/delle classi e usare "$" per separare nomi di classi interne (ad esempio 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, usa "$" anziché ".". In generale, usare javap sul file .class è un buon modo per scoprire il nome interno del corso.

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, è possibile che tu stia visualizzando un caricatore di classi problema. 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 metterti nei guai se crea tu un thread (magari chiamando pthread_create e allegandola poi con AttachCurrentThread). Ora ci sono non sono stack frame 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.

Puoi risolvere il problema in diversi modi:

  • 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. Qualsiasi chiamata FindClass effettuata durante l'esecuzione JNI_OnLoad utilizzerà il caricatore classi associato a la funzione che ha chiamato System.loadLibrary (questo è un regola speciale, fornita per rendere più comoda 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 metodi di base.

Puoi archiviare i dati in un byte[]. Ciò consente una velocità l'accesso dal codice gestito. Dal punto di vista dei nativi, tuttavia, non è garantito di poter accedere ai dati senza doverli copiare. Nella alcune implementazioni, GetByteArrayElements e GetPrimitiveArrayCritical restituirà i puntatori effettivi alla non elaborati nell'heap gestito, ma in altri viene allocato un buffer sull'heap nativo e copia i dati.

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 formato 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 esiste un vincitore definitivo, utilizza un buffer di byte diretto. Supporto è integrato direttamente in JNI e le prestazioni dovrebbero migliorare nelle release future.