JNI-Tipps

JNI steht für Java Native Interface. Es definiert eine Möglichkeit für den Bytecode, den Android aus verwaltetem Code (in den Programmiersprachen Java oder Kotlin geschrieben) kompiliert, mit nativem Code (in C/C++ geschrieben) zu interagieren. JNI ist anbieterneutral, unterstützt das Laden von Code aus dynamischen gemeinsam genutzten Bibliotheken und ist, obwohl manchmal umständlich, relativ effizient.

Hinweis:Da Android Kotlin auf ähnliche Weise wie die Programmiersprache Java in ART-freundlichen Bytecode kompiliert, können Sie die Informationen auf dieser Seite sowohl auf die Programmiersprachen Kotlin als auch auf Java anwenden, was die JNI-Architektur und die damit verbundenen Kosten betrifft. Weitere Informationen zu Kotlin und Android

Wenn Sie noch nicht damit vertraut sind, lesen Sie die Java Native Interface Specification, um sich ein Bild davon zu machen, wie JNI funktioniert und welche Funktionen verfügbar sind. Einige Aspekte der Benutzeroberfläche sind beim ersten Lesen nicht sofort ersichtlich. Die nächsten Abschnitte können daher hilfreich sein.

Wenn Sie globale JNI-Verweise aufrufen und sehen möchten, wo globale JNI-Verweise erstellt und gelöscht werden, verwenden Sie die Ansicht JNI-Heap im Memory Profiler in Android Studio 3.2 und höher.

Allgemeine Tipps

Versuchen Sie, den Umfang Ihrer JNI-Ebene zu minimieren. Dabei sind mehrere Dimensionen zu berücksichtigen. Ihre JNI-Lösung sollte versuchen, die folgenden Richtlinien einzuhalten (nach Wichtigkeit sortiert, beginnend mit der wichtigsten):

  • Minimieren Sie das Marshalling von Ressourcen über die JNI-Ebene hinweg. Das Marshalling über die JNI-Ebene hinweg ist mit nicht unerheblichen Kosten verbunden. Versuchen Sie, eine Schnittstelle zu entwerfen, die die Menge der Daten, die Sie marshallen müssen, und die Häufigkeit, mit der Sie Daten marshallen müssen, minimiert.
  • Vermeiden Sie nach Möglichkeit die asynchrone Kommunikation zwischen Code, der in einer verwalteten Programmiersprache geschrieben wurde, und Code, der in C++ geschrieben wurde. So lässt sich Ihre JNI-Schnittstelle leichter verwalten. Sie können asynchrone UI-Aktualisierungen in der Regel vereinfachen, indem Sie die asynchrone Aktualisierung in derselben Sprache wie die UI vornehmen. Anstatt beispielsweise eine C++-Funktion über JNI aus dem UI-Thread im Java-Code aufzurufen, ist es besser, einen Callback zwischen zwei Threads in der Java-Programmiersprache zu verwenden. Einer der Threads führt einen blockierenden C++-Aufruf aus und benachrichtigt dann den UI-Thread, wenn der blockierende Aufruf abgeschlossen ist.
  • Minimieren Sie die Anzahl der Threads, die von JNI berührt werden müssen oder JNI berühren. Wenn Sie Threadpools sowohl in Java als auch in C++ verwenden müssen, sollten Sie die JNI-Kommunikation zwischen den Pool-Eigentümern und nicht zwischen einzelnen Worker-Threads aufrechterhalten.
  • Bewahren Sie Ihren Schnittstellencode an einer geringen Anzahl von leicht identifizierbaren C++- und Java-Quellspeicherorten auf, um zukünftige Refactorings zu erleichtern. Verwenden Sie gegebenenfalls eine Bibliothek zur automatischen JNI-Generierung.

JavaVM und JNIEnv

JNI definiert zwei wichtige Datenstrukturen: „JavaVM“ und „JNIEnv“. Beide sind im Grunde Zeiger auf Zeiger auf Funktionstabellen. In der C++-Version sind sie Klassen mit einem Zeiger auf eine Funktionstabelle und einer Member-Funktion für jede JNI-Funktion, die über die Tabelle indirekt aufgerufen wird. Die JavaVM bietet die Funktionen der „Aufrufschnittstelle“, mit denen Sie eine JavaVM erstellen und zerstören können. Theoretisch können Sie mehrere JavaVMs pro Prozess haben, aber Android lässt nur eine zu.

Die meisten JNI-Funktionen werden über die JNIEnv bereitgestellt. Alle Ihre nativen Funktionen erhalten ein JNIEnv als erstes Argument, mit Ausnahme von @CriticalNative-Methoden. Weitere Informationen finden Sie unter Schnellere native Aufrufe.

Die JNIEnv wird für den threadlokalen Speicher verwendet. Aus diesem Grund können Sie JNIEnv nicht zwischen Threads teilen. Wenn ein Code-Abschnitt keine andere Möglichkeit hat, sein JNIEnv zu erhalten, sollten Sie die JavaVM freigeben und GetEnv verwenden, um das JNIEnv des Threads zu ermitteln. (Sofern vorhanden, siehe AttachCurrentThread unten.)

Die C-Deklarationen von JNIEnv und JavaVM unterscheiden sich von den C++-Deklarationen. Die Include-Datei "jni.h" enthält verschiedene typedefs, je nachdem, ob sie in C oder C++ enthalten ist. Aus diesem Grund ist es keine gute Idee, JNIEnv-Argumente in Header-Dateien aufzunehmen, die von beiden Sprachen eingebunden werden. Anders ausgedrückt: Wenn für Ihre Headerdatei #ifdef __cplusplus erforderlich ist, müssen Sie möglicherweise zusätzlichen Aufwand betreiben, wenn sich in diesem Header etwas auf JNIEnv bezieht.

Threads

Alle Threads sind Linux-Threads, die vom Kernel geplant werden. Sie werden in der Regel über verwalteten Code (mit Thread.start()) gestartet, können aber auch an anderer Stelle erstellt und dann an das JavaVM angehängt werden. Ein mit pthread_create() oder std::thread gestarteter Thread kann beispielsweise mit den Funktionen AttachCurrentThread() oder AttachCurrentThreadAsDaemon() angehängt werden. Bis ein Thread angehängt wird, hat er keine JNIEnv und kann keine JNI-Aufrufe ausführen.

Normalerweise ist es am besten, Thread.start() zu verwenden, um Threads zu erstellen, die Java-Code aufrufen müssen. So wird sichergestellt, dass Sie genügend Stapelspeicher haben, sich im richtigen ThreadGroup befinden und dieselbe ClassLoader wie Ihr Java-Code verwenden. Außerdem ist es einfacher, den Namen des Threads für das Debugging in Java festzulegen als über nativen Code (siehe pthread_setname_np(), wenn Sie ein pthread_t oder thread_t haben, und std::thread::native_handle(), wenn Sie ein std::thread haben und ein pthread_t möchten).

Wenn Sie einen nativ erstellten Thread anhängen, wird ein java.lang.Thread-Objekt erstellt und dem ThreadGroup-Objekt „main“ hinzugefügt, sodass es im Debugger sichtbar ist. Das Aufrufen von AttachCurrentThread() in einem bereits angehängten Thread hat keine Auswirkungen.

Unter Android werden Threads, die nativen Code ausführen, nicht angehalten. Wenn die Garbage Collection läuft oder der Debugger eine Suspend-Anfrage gesendet hat, pausiert Android den Thread beim nächsten JNI-Aufruf.

Threads, die über JNI angehängt werden, müssen DetachCurrentThread() aufrufen, bevor sie beendet werden. Wenn die direkte Programmierung schwierig ist, können Sie in Android 2.0 (Eclair) und höher mit pthread_key_create() eine Destruktorfunktion definieren, die vor dem Beenden des Threads aufgerufen wird, und DetachCurrentThread() von dort aus aufrufen. Verwenden Sie diesen Schlüssel mit pthread_setspecific(), um die JNIEnv im Thread-Local-Storage zu speichern. So wird sie als Argument an Ihren Destruktor übergeben.

jclass, jmethodID und jfieldID

Wenn Sie von nativem Code aus auf das Feld eines Objekts zugreifen möchten, gehen Sie so vor:

  • Rufen Sie die Klassenobjektreferenz für die Klasse mit FindClass ab.
  • Rufen Sie die Feld-ID für das Feld mit GetFieldID ab.
  • Rufen Sie den Inhalt des Felds mit einem geeigneten Befehl ab, z. B. GetIntField.

Wenn Sie eine Methode aufrufen möchten, müssen Sie zuerst eine Klassenobjektreferenz und dann eine Methoden-ID abrufen. Die IDs sind oft nur Verweise auf interne Laufzeitdatenstrukturen. Das Nachschlagen kann mehrere Stringvergleiche erfordern, aber sobald Sie sie haben, ist der eigentliche Aufruf zum Abrufen des Felds oder zum Aufrufen der Methode sehr schnell.

Wenn die Leistung wichtig ist, sollten Sie die Werte einmal nachschlagen und die Ergebnisse in Ihrem nativen Code im Cache speichern. Da es nur eine JavaVM pro Prozess gibt, ist es sinnvoll, diese Daten in einer statischen lokalen Struktur zu speichern.

Die Klassenreferenzen, Feld-IDs und Methoden-IDs sind garantiert gültig, bis die Klasse entladen wird. Klassen werden nur entladen, wenn alle Klassen, die mit einem ClassLoader verknüpft sind, per Garbage Collection entfernt werden können. Das ist selten, aber in Android nicht unmöglich. Beachten Sie jedoch, dass jclass eine Klassenreferenz ist und mit einem Aufruf von NewGlobalRef geschützt werden muss (siehe nächster Abschnitt).

Wenn Sie die IDs beim Laden einer Klasse im Cache speichern und automatisch neu im Cache speichern möchten, falls die Klasse entladen und neu geladen wird, müssen Sie die IDs so initialisieren: Fügen Sie der entsprechenden Klasse einen Code hinzu, der so aussieht:

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();
    }

Erstellen Sie in Ihrem C/C++-Code eine nativeClassInit-Methode, die die ID-Suchvorgänge ausführt. Der Code wird einmal ausgeführt, wenn die Klasse initialisiert wird. Wenn die Klasse entladen und dann neu geladen wird, wird sie noch einmal ausgeführt.

Lokale und globale Referenzen

Jedes Argument, das an eine native Methode übergeben wird, und fast jedes Objekt, das von einer JNI-Funktion zurückgegeben wird, ist eine „lokale Referenz“. Das bedeutet, dass sie für die Dauer der aktuellen nativen Methode im aktuellen Thread gültig ist. Auch wenn das Objekt selbst nach der Rückgabe der nativen Methode weiterhin vorhanden ist, ist die Referenz nicht gültig.

Dies gilt für alle Unterklassen von jobject, einschließlich jclass, jstring und jarray. Die Laufzeit warnt Sie vor den meisten Referenzfehlern, wenn erweiterte JNI-Prüfungen aktiviert sind.

Nicht lokale Verweise können nur über die Funktionen NewGlobalRef und NewWeakGlobalRef abgerufen werden.

Wenn Sie eine Referenz über einen längeren Zeitraum beibehalten möchten, müssen Sie eine „globale“ Referenz verwenden. Die Funktion NewGlobalRef verwendet die lokale Referenz als Argument und gibt eine globale Referenz zurück. Die globale Referenz ist garantiert gültig, bis Sie DeleteGlobalRef aufrufen.

Dieses Muster wird häufig verwendet, wenn eine von FindClass zurückgegebene jclass gecacht wird, z.B.:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

Alle JNI-Methoden akzeptieren sowohl lokale als auch globale Referenzen als Argumente. Es ist möglich, dass Verweise auf dasselbe Objekt unterschiedliche Werte haben. Die Rückgabewerte aufeinanderfolgender Aufrufe von NewGlobalRef für dasselbe Objekt können beispielsweise unterschiedlich sein. Wenn Sie prüfen möchten, ob sich zwei Verweise auf dasselbe Objekt beziehen, müssen Sie die Funktion IsSameObject verwenden. Vergleichen Sie Referenzen im nativen Code niemals mit ==.

Eine Folge davon ist, dass Sie nicht davon ausgehen dürfen, dass Objektreferenzen im nativen Code konstant oder eindeutig sind. Der Wert, der ein Objekt darstellt, kann sich von einem Aufruf einer Methode zum nächsten unterscheiden. Es ist auch möglich, dass zwei verschiedene Objekte bei aufeinanderfolgenden Aufrufen denselben Wert haben. Verwenden Sie keine jobject-Werte als Schlüssel.

Programmierer dürfen lokale Referenzen nicht übermäßig zuweisen. In der Praxis bedeutet das, dass Sie bei der Erstellung einer großen Anzahl lokaler Referenzen, z. B. beim Durchlaufen eines Arrays von Objekten, diese manuell mit DeleteLocalRef freigeben sollten, anstatt JNI dies für Sie erledigen zu lassen. Die Implementierung ist nur erforderlich, um Slots für 16 lokale Referenzen zu reservieren. Wenn Sie mehr benötigen, sollten Sie entweder nach Bedarf löschen oder EnsureLocalCapacity/PushLocalFrame verwenden, um mehr zu reservieren.

jfieldIDs und jmethodIDs sind undurchsichtige Typen, keine Objektreferenzen, und sollten nicht an NewGlobalRef übergeben werden. Die Rohdaten-Pointer, die von Funktionen wie GetStringUTFChars und GetByteArrayElements zurückgegeben werden, sind ebenfalls keine Objekte. Sie können zwischen Threads übergeben werden und sind bis zum entsprechenden Release-Aufruf gültig.

Ein ungewöhnlicher Fall verdient eine gesonderte Erwähnung. Wenn Sie einen nativen Thread mit AttachCurrentThread anhängen, werden lokale Referenzen im ausgeführten Code erst automatisch freigegeben, wenn der Thread getrennt wird. Alle von Ihnen erstellten lokalen Verweise müssen manuell gelöscht werden. Im Allgemeinen muss jeder native Code, der lokale Referenzen in einer Schleife erstellt, manuell gelöscht werden.

Seien Sie vorsichtig bei der Verwendung globaler Referenzen. Globale Referenzen lassen sich manchmal nicht vermeiden, sind aber schwer zu debuggen und können schwer zu diagnostizierende Speicherfehler verursachen. Wenn alles andere gleich ist, ist eine Lösung mit weniger globalen Referenzen wahrscheinlich besser.

UTF-8- und UTF-16-Strings

Die Programmiersprache Java verwendet UTF-16. JNI bietet Methoden, die auch mit Modified UTF-8 funktionieren. Die geänderte Codierung ist für C-Code nützlich, da \u0000 als 0xc0 0x80 anstelle von 0x00 codiert wird. Der Vorteil ist, dass Sie mit C-ähnlichen, nullterminierten Strings rechnen können, die sich für die Verwendung mit Standard-libc-Stringfunktionen eignen. Der Nachteil ist, dass Sie keine beliebigen UTF-8-Daten an JNI übergeben können und erwarten, dass sie korrekt funktionieren.

Um die UTF-16-Darstellung eines String abzurufen, verwenden Sie GetStringChars. UTF-16-Strings sind nicht nullterminiert und \u0000 ist zulässig. Sie müssen also sowohl die Stringlänge als auch den jchar-Zeiger beibehalten.

Vergiss nicht, die Strings zu Release, die du Get. Die Stringfunktionen geben jchar* oder jbyte* zurück. Das sind C-Style-Pointer auf primitive Daten und keine lokalen Referenzen. Sie sind garantiert gültig, bis Release aufgerufen wird. Das bedeutet, dass sie nicht freigegeben werden, wenn die native Methode zurückkehrt.

An NewStringUTF übergebene Daten müssen im modifizierten UTF-8-Format vorliegen. Ein häufiger Fehler besteht darin, Zeichendaten aus einer Datei oder einem Netzwerkstream zu lesen und sie ohne Filterung an NewStringUTF zu übergeben. Sofern Sie nicht wissen, dass die Daten gültiges MUTF-8 (oder 7-Bit-ASCII, das eine kompatible Teilmenge ist) enthalten, müssen Sie ungültige Zeichen entfernen oder in das richtige Modified UTF-8-Format konvertieren. Andernfalls liefert die UTF‑16-Konvertierung wahrscheinlich unerwartete Ergebnisse. CheckJNI, das standardmäßig für Emulatoren aktiviert ist, scannt Strings und bricht die VM ab, wenn ungültige Eingaben empfangen werden.

Vor Android 8 war es in der Regel schneller, mit UTF-16-Strings zu arbeiten, da Android keine Kopie in GetStringChars benötigte, während für GetStringUTFChars eine Zuweisung und eine Konvertierung in UTF-8 erforderlich war. In Android 8 wurde die String-Darstellung so geändert, dass für ASCII-Strings 8 Bit pro Zeichen verwendet werden (um Speicherplatz zu sparen). Außerdem wurde eine automatische Speicherbereinigung eingeführt. Diese Funktionen reduzieren die Anzahl der Fälle, in denen ART einen Zeiger auf die String-Daten bereitstellen kann, ohne eine Kopie zu erstellen, auch für GetStringCritical. Wenn die meisten vom Code verarbeiteten Strings jedoch kurz sind, kann die Zuordnung und Freigabe in den meisten Fällen vermieden werden, indem ein auf dem Stapel zugewiesener Puffer und GetStringRegion oder GetStringUTFRegion verwendet werden. Beispiel:

    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);

Einfache Arrays

JNI bietet Funktionen für den Zugriff auf den Inhalt von Array-Objekten. Während auf Arrays von Objekten jeweils nur ein Eintrag gleichzeitig zugegriffen werden kann, können Arrays von Primitiven direkt gelesen und geschrieben werden, als wären sie in C deklariert.

Um die Schnittstelle so effizient wie möglich zu gestalten, ohne die VM-Implementierung einzuschränken, kann die Laufzeit mit der Get<PrimitiveType>ArrayElements-Familie von Aufrufen entweder einen Zeiger auf die tatsächlichen Elemente zurückgeben oder etwas Speicher zuweisen und eine Kopie erstellen. In beiden Fällen ist der zurückgegebene Rohzeiger garantiert gültig, bis der entsprechende Release-Aufruf erfolgt. Wenn die Daten nicht kopiert wurden, wird das Array-Objekt fixiert und kann nicht im Rahmen der Heap-Kompaktierung verschoben werden. Sie müssen jedes Array, das Sie Get, Release. Wenn der Get-Aufruf fehlschlägt, muss Ihr Code außerdem verhindern, dass später versucht wird, einen NULL-Zeiger zu Release.

Sie können ermitteln, ob die Daten kopiert wurden, indem Sie einen nicht leeren Zeiger für das isCopy-Argument übergeben. Das ist selten hilfreich.

Der Aufruf Release verwendet ein mode-Argument, das einen von drei Werten haben kann. Die von der Laufzeit ausgeführten Aktionen hängen davon ab, ob sie einen Zeiger auf die tatsächlichen Daten oder eine Kopie davon zurückgegeben hat:

  • 0
    • Tatsächlich: Das Array-Objekt ist nicht angepinnt.
    • Kopieren: Daten werden zurückkopiert. Der Puffer mit der Kopie wird freigegeben.
  • JNI_COMMIT
    • Tatsächlich: Es passiert nichts.
    • Kopieren: Daten werden zurückkopiert. Der Puffer mit der Kopie wird nicht freigegeben.
  • JNI_ABORT
    • Tatsächlich: Das Array-Objekt ist nicht angepinnt. Frühere Schreibvorgänge werden nicht abgebrochen.
    • Kopieren: Der Puffer mit der Kopie wird freigegeben. Alle Änderungen daran gehen verloren.

Ein Grund für die Prüfung des Flags isCopy ist, um zu wissen, ob Sie Release mit JNI_COMMIT aufrufen müssen, nachdem Sie Änderungen an einem Array vorgenommen haben. Wenn Sie abwechselnd Änderungen vornehmen und Code ausführen, der den Inhalt des Arrays verwendet, können Sie den No-Op-Commit möglicherweise überspringen. Ein weiterer möglicher Grund für die Überprüfung des Flags ist die effiziente Verarbeitung von JNI_ABORT. Sie möchten beispielsweise ein Array abrufen, es direkt ändern, Teile davon an andere Funktionen übergeben und die Änderungen dann verwerfen. Wenn Sie wissen, dass JNI eine neue Kopie für Sie erstellt, müssen Sie keine weitere „bearbeitbare“ Kopie erstellen. Wenn JNI Ihnen das Original übergibt, müssen Sie eine eigene Kopie erstellen.

Ein häufiger Fehler (der auch im Beispielcode wiederholt wird) ist die Annahme, dass Sie den Release-Aufruf überspringen können, wenn *isCopy „false“ ist. Das ist nicht der Fall. Wenn kein Kopierpuffer zugewiesen wurde, muss der ursprüngliche Speicherbereich fixiert werden und kann nicht vom Garbage Collector verschoben werden.

Beachten Sie außerdem, dass das Flag JNI_COMMIT das Array nicht freigibt. Sie müssen Release also irgendwann noch einmal mit einem anderen Flag aufrufen.

Regionsaufrufe

Es gibt eine Alternative zu Aufrufen wie Get<Type>ArrayElements und GetStringChars, die sehr hilfreich sein kann, wenn Sie nur Daten kopieren möchten. Hier einige Tipps:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

Damit wird das Array abgerufen, die ersten len Byte-Elemente daraus kopiert und das Array dann freigegeben. Je nach Implementierung werden die Arrayinhalte durch den Get-Aufruf entweder angepinnt oder kopiert. Der Code kopiert die Daten (möglicherweise zum zweiten Mal) und ruft dann Release auf. In diesem Fall sorgt JNI_ABORT dafür, dass keine dritte Kopie erstellt wird.

Das geht auch einfacher:

    env->GetByteArrayRegion(array, 0, len, buffer);

Das bietet mehrere Vorteile:

  • Es ist nur ein JNI-Aufruf anstelle von zwei erforderlich, wodurch der Overhead reduziert wird.
  • Es sind keine zusätzlichen Datenkopien oder das Anpinnen von Daten erforderlich.
  • Reduziert das Risiko von Programmierfehlern – es besteht kein Risiko, dass Release nach einem Fehler nicht aufgerufen wird.

Ebenso können Sie mit dem Aufruf Set<Type>ArrayRegion Daten in ein Array kopieren und mit GetStringRegion oder GetStringUTFRegion Zeichen aus einem String kopieren.

Ausnahmen

Die meisten JNI-Funktionen dürfen nicht aufgerufen werden, wenn eine Ausnahme aussteht. Ihr Code sollte die Ausnahme über den Rückgabewert der Funktion (ExceptionCheck oder ExceptionOccurred) erkennen und zurückgeben oder die Ausnahme löschen und verarbeiten.

Die einzigen JNI-Funktionen, die Sie aufrufen dürfen, wenn eine Ausnahme aussteht, sind:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

Viele JNI-Aufrufe können eine Ausnahme auslösen, bieten aber oft eine einfachere Möglichkeit, auf Fehler zu prüfen. Wenn NewString beispielsweise einen Wert ungleich NULL zurückgibt, müssen Sie nicht nach einer Ausnahme suchen. Wenn Sie jedoch eine Methode aufrufen (mit einer Funktion wie CallObjectMethod), müssen Sie immer auf eine Ausnahme prüfen, da der Rückgabewert nicht gültig ist, wenn eine Ausnahme ausgelöst wurde.

Beachten Sie, dass von verwaltetem Code ausgelöste Ausnahmen keine nativen Stapelframes abwickeln. C++-Ausnahmen, die auf Android generell nicht empfohlen werden, dürfen nicht über die JNI-Übergangsgrenze vom C++-Code zum verwalteten Code ausgelöst werden. Die JNI-Anweisungen Throw und ThrowNew legen lediglich einen Ausnahmezeiger im aktuellen Thread fest. Wenn Sie vom nativen Code zum verwalteten Code zurückkehren, wird die Ausnahme vermerkt und entsprechend behandelt.

Nativer Code kann eine Ausnahme mit ExceptionCheck oder ExceptionOccurred „abfangen“ und mit ExceptionClear löschen. Wie üblich kann das Verwerfen von Ausnahmen ohne Behandlung zu Problemen führen.

Es gibt keine integrierten Funktionen zum Bearbeiten des Throwable-Objekts selbst. Wenn Sie beispielsweise den Ausnahmestring abrufen möchten, müssen Sie die Throwable-Klasse suchen, die Methoden-ID für getMessage "()Ljava/lang/String;" nachschlagen, sie aufrufen und, falls das Ergebnis nicht NULL ist, GetStringUTFChars verwenden, um etwas zu erhalten, das Sie an printf(3) oder eine ähnliche Funktion übergeben können.

Erweiterte Überprüfung

JNI führt nur sehr wenige Fehlerprüfungen durch. Fehler führen in der Regel zu einem Absturz. Android bietet auch einen Modus namens CheckJNI, in dem die JavaVM- und JNIEnv-Funktionstabellenzeiger auf Tabellen mit Funktionen umgestellt werden, die eine erweiterte Reihe von Prüfungen durchführen, bevor die Standardimplementierung aufgerufen wird.

Die zusätzlichen Prüfungen umfassen:

  • Arrays: Es wird versucht, ein Array mit negativer Größe zuzuweisen.
  • Ungültige Zeiger: Übergabe eines ungültigen jarray/jclass/jobject/jstring an einen JNI-Aufruf oder Übergabe eines NULL-Zeigers an einen JNI-Aufruf mit einem Argument, das nicht NULL sein darf.
  • Klassennamen: Wenn Sie bei einem JNI-Aufruf etwas anderes als den Klassennamen im Stil „java/lang/String“ übergeben.
  • Kritische Aufrufe: JNI-Aufruf zwischen einem „kritischen“ Get und der entsprechenden Freigabe.
  • Direct ByteBuffers: Übergabe ungültiger Argumente an NewDirectByteBuffer.
  • Ausnahmen: JNI-Aufruf, während eine Ausnahme aussteht.
  • JNIEnv*: Verwendung eines JNIEnv* aus dem falschen Thread.
  • jfieldIDs: Verwendung einer NULL-jfieldID, Verwendung einer jfieldID zum Festlegen eines Felds auf einen Wert des falschen Typs (z. B. Versuch, einem String-Feld einen StringBuilder zuzuweisen), Verwendung einer jfieldID für ein statisches Feld zum Festlegen eines Instanzfelds oder umgekehrt oder Verwendung einer jfieldID aus einer Klasse mit Instanzen einer anderen Klasse.
  • jmethodIDs: Bei einem Call*Method-JNI-Aufruf wird die falsche Art von jmethodID verwendet: falscher Rückgabetyp, statische/nicht statische Abweichung, falscher Typ für „this“ (bei nicht statischen Aufrufen) oder falsche Klasse (bei statischen Aufrufen).
  • Verweise: DeleteGlobalRef/DeleteLocalRef wird für den falschen Verweistyp verwendet.
  • Release-Modi: Wenn Sie einen ungültigen Release-Modus an einen Release-Aufruf übergeben (etwas anderes als 0, JNI_ABORT oder JNI_COMMIT).
  • Typsicherheit: Wenn Sie einen inkompatiblen Typ aus Ihrer nativen Methode zurückgeben (z. B. einen StringBuilder aus einer Methode, die einen String zurückgeben soll).
  • UTF-8: Übergabe einer ungültigen Modified UTF-8-Bytefolge an einen JNI-Aufruf.

(Die Barrierefreiheit von Methoden und Feldern wird noch nicht geprüft: Zugriffsbeschränkungen gelten nicht für nativen Code.)

Es gibt mehrere Möglichkeiten, CheckJNI zu aktivieren.

Wenn Sie den Emulator verwenden, ist CheckJNI standardmäßig aktiviert.

Wenn Sie ein gerootetes Gerät haben, können Sie die Laufzeit mit aktiviertem CheckJNI mit der folgenden Befehlsfolge neu starten:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

In beiden Fällen wird in der Logcat-Ausgabe beim Start der Laufzeit etwa Folgendes angezeigt:

D AndroidRuntime: CheckJNI is ON

Wenn Sie ein reguläres Gerät haben, können Sie den folgenden Befehl verwenden:

adb shell setprop debug.checkjni 1

Dies hat keine Auswirkungen auf bereits ausgeführte Apps. Für alle Apps, die ab diesem Zeitpunkt gestartet werden, ist CheckJNI jedoch aktiviert. Wenn Sie das Attribut in einen anderen Wert ändern oder das Gerät einfach neu starten, wird CheckJNI wieder deaktiviert. In diesem Fall sehen Sie beim nächsten Start einer App in der Logcat-Ausgabe etwa Folgendes:

D Late-enabling CheckJNI

Sie können das Attribut android:debuggable auch im Manifest Ihrer Anwendung festlegen, um CheckJNI nur für Ihre App zu aktivieren. Die Android-Build-Tools tun dies automatisch für bestimmte Build-Typen.

Native Bibliotheken

Sie können nativen Code aus gemeinsam genutzten Bibliotheken mit dem Standard-System.loadLibrary laden.

In der Praxis gab es in älteren Android-Versionen Fehler in PackageManager, die dazu führten, dass die Installation und Aktualisierung nativer Bibliotheken nicht zuverlässig war. Das ReLinker-Projekt bietet Workarounds für dieses und andere Probleme beim Laden nativer Bibliotheken.

Rufen Sie System.loadLibrary (oder ReLinker.loadLibrary) aus einem statischen Klasseninitialisierer auf. Das Argument ist der „undekorierte“ Bibliotheksname. Wenn Sie also libfubar.so laden möchten, müssen Sie "fubar" übergeben.

Wenn Sie nur eine Klasse mit nativen Methoden haben, ist es sinnvoll, den Aufruf von System.loadLibrary in einem statischen Initialisierer für diese Klasse zu platzieren. Andernfalls sollten Sie den Aufruf möglicherweise über Application ausführen, damit die Bibliothek immer und immer früh geladen wird.

Es gibt zwei Möglichkeiten, wie die Laufzeit Ihre nativen Methoden finden kann. Sie können sie entweder explizit mit RegisterNatives registrieren oder die Laufzeit kann sie dynamisch mit dlsym nachschlagen. Die Vorteile von RegisterNatives sind, dass Sie vorab prüfen können, ob die Symbole vorhanden sind, und dass Sie kleinere und schnellere gemeinsam genutzte Bibliotheken erstellen können, da nur JNI_OnLoad exportiert wird. Der Vorteil, wenn die Laufzeit Ihre Funktionen erkennt, besteht darin, dass Sie etwas weniger Code schreiben müssen.

Gehe so vor, wenn du RegisterNatives verwenden möchtest:

  • Stellen Sie eine JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)-Funktion bereit.
  • Registrieren Sie in Ihrem JNI_OnLoad alle nativen Methoden mit RegisterNatives.
  • Erstellen Sie mit einem Versionsskript (bevorzugt) oder verwenden Sie -fvisibility=hidden, damit nur Ihre JNI_OnLoad aus Ihrer Bibliothek exportiert wird. Dadurch wird schnellerer und kleinerer Code erzeugt und potenzielle Konflikte mit anderen in Ihre App geladenen Bibliotheken werden vermieden. Allerdings sind die erstellten Stacktraces weniger nützlich, wenn Ihre App in nativem Code abstürzt.

Der statische Initialisierer sollte so aussehen:

Kotlin

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Java

static {
    System.loadLibrary("fubar");
}

Die Funktion JNI_OnLoad sollte in C++ in etwa so aussehen:

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;
}

Wenn Sie stattdessen die „Erkennung“ von nativen Methoden verwenden möchten, müssen Sie sie auf eine bestimmte Weise benennen (siehe JNI-Spezifikation für weitere Informationen). Wenn eine Methodensignatur falsch ist, erfahren Sie das erst, wenn die Methode zum ersten Mal aufgerufen wird.

Alle FindClass-Aufrufe von JNI_OnLoad lösen Klassen im Kontext des Klassenladeprogramms auf, das zum Laden der gemeinsam genutzten Bibliothek verwendet wurde. Wenn FindClass aus anderen Kontexten aufgerufen wird, wird der Klassenlader verwendet, der der Methode oben im Java-Stack zugeordnet ist. Wenn kein Klassenlader vorhanden ist (weil der Aufruf von einem nativen Thread stammt, der gerade angehängt wurde), wird der „System“-Klassenlader verwendet. Der Systemklassen-Loader kennt die Klassen Ihrer Anwendung nicht. Sie können Ihre eigenen Klassen also nicht mit FindClass in diesem Kontext suchen. JNI_OnLoad ist daher ein praktischer Ort, um Klassen nachzuschlagen und im Cache zu speichern: Sobald Sie eine gültige jclass globale Referenz haben, können Sie sie von jedem angehängten Thread aus verwenden.

Schnellere native Aufrufe mit @FastNative und @CriticalNative

Native Methoden können mit @FastNative oder @CriticalNative (aber nicht beides) annotiert werden, um Übergänge zwischen verwaltetem und nativem Code zu beschleunigen. Diese Anmerkungen sind jedoch mit bestimmten Verhaltensänderungen verbunden, die vor der Verwendung sorgfältig berücksichtigt werden müssen. Wir gehen unten kurz auf diese Änderungen ein. Die Details finden Sie in der Dokumentation.

Die Annotation @CriticalNative kann nur auf native Methoden angewendet werden, die keine verwalteten Objekte verwenden (in Parametern oder Rückgabewerten oder als implizites this). Diese Annotation ändert die JNI-Übergangs-ABI. Die native Implementierung darf die Parameter JNIEnv und jclass nicht in ihrer Funktionssignatur enthalten.

Bei der Ausführung einer @FastNative- oder @CriticalNative-Methode kann die Garbage Collection den Thread nicht für wichtige Aufgaben unterbrechen und er kann blockiert werden. Verwenden Sie diese Anmerkungen nicht für Methoden, die lange ausgeführt werden, einschließlich Methoden, die normalerweise schnell, aber im Allgemeinen unbegrenzt sind. Insbesondere sollte der Code keine umfangreichen E/A-Vorgänge ausführen oder native Sperren abrufen, die lange gehalten werden können.

Diese Anmerkungen wurden für die Systemnutzung seit Android 8 implementiert und sind in Android 14 zu einer CTS-getesteten öffentlichen API geworden. Diese Optimierungen funktionieren wahrscheinlich auch auf Geräten mit Android 8 bis 13 (wenn auch ohne die starken CTS-Garantien), aber die dynamische Suche nach nativen Methoden wird nur auf Android 12 und höher unterstützt. Die explizite Registrierung mit JNI RegisterNatives ist für die Ausführung auf Android-Versionen 8 bis 11 unbedingt erforderlich. Diese Anmerkungen werden unter Android 7 ignoriert. Die ABI-Inkompatibilität für @CriticalNative würde zu einer falschen Argument-Marshalling und wahrscheinlich zu Abstürzen führen.

Für leistungskritische Methoden, die diese Anmerkungen benötigen, wird dringend empfohlen, die Methode(n) explizit bei JNI RegisterNatives zu registrieren, anstatt sich auf die namensbasierte „Erkennung“ nativer Methoden zu verlassen. Um eine optimale Leistung beim App-Start zu erzielen, empfiehlt es sich, Aufrufer von @FastNative- oder @CriticalNative-Methoden in das Baseline-Profil aufzunehmen. Seit Android 12 ist ein Aufruf einer nativen @CriticalNative-Methode aus einer kompilierten verwalteten Methode fast so günstig wie ein nicht inline ausgeführter Aufruf in C/C++, sofern alle Argumente in Register passen (z. B. bis zu 8 Ganzzahl- und bis zu 8 Gleitkommaargumente auf arm64).

Manchmal ist es besser, eine native Methode in zwei aufzuteilen: eine sehr schnelle Methode, die fehlschlagen kann, und eine andere, die die langsamen Fälle abdeckt. Beispiel:

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);

64-Bit-Überlegungen

Wenn Sie Architekturen mit 64-Bit-Zeigern unterstützen möchten, verwenden Sie das Feld long anstelle von int, wenn Sie einen Zeiger auf eine native Struktur in einem Java-Feld speichern.

Nicht unterstützte Funktionen/Abwärtskompatibilität

Alle JNI 1.6-Funktionen werden unterstützt, mit der folgenden Ausnahme:

  • DefineClass ist nicht implementiert. Unter Android werden keine Java-Bytecodes oder ‑Klassendateien verwendet. Daher funktioniert die Übergabe von binären Klassendaten nicht.

Für die Abwärtskompatibilität mit älteren Android-Versionen müssen Sie möglicherweise Folgendes beachten:

  • Dynamische Suche nach nativen Funktionen

    Bis Android 2.0 (Eclair) wurde das Zeichen „$“ bei der Suche nach Methodennamen nicht richtig in „_00024“ konvertiert. Um dieses Problem zu umgehen, müssen Sie die explizite Registrierung verwenden oder die nativen Methoden aus den inneren Klassen verschieben.

  • Konversationen trennen

    Bis Android 2.0 (Eclair) war es nicht möglich, eine pthread_key_create-Destruktorfunktion zu verwenden, um die Prüfung „thread must be detached before exit“ zu umgehen. Die Laufzeit verwendet auch eine Pthread-Schlüssel-Destruktorfunktion. Es ist also ein Wettlauf, welche zuerst aufgerufen wird.

  • Schwache globale Referenzen

    Bis Android 2.2 (Froyo) wurden keine schwachen globalen Referenzen implementiert. Ältere Versionen lehnen Versuche, sie zu verwenden, vehement ab. Sie können die Android-Plattformversionskonstanten verwenden, um die Unterstützung zu testen.

    Bis Android 4.0 (Ice Cream Sandwich) konnten schwache globale Referenzen nur an NewLocalRef, NewGlobalRef und DeleteWeakGlobalRef übergeben werden. (Die Spezifikation empfiehlt Programmierern dringend, vor der Verwendung von schwachen globalen Variablen starke Referenzen zu erstellen. Dies sollte also keine Einschränkung darstellen.)

    Ab Android 4.0 (Ice Cream Sandwich) können schwache globale Referenzen wie alle anderen JNI-Referenzen verwendet werden.

  • Lokale Referenzen

    Bis Android 4.0 (Ice Cream Sandwich) waren lokale Referenzen tatsächlich direkte Zeiger. Mit Ice Cream Sandwich wurde die Indirektion eingeführt, die für bessere Garbage Collectors erforderlich ist. Das bedeutet jedoch, dass viele JNI-Bugs in älteren Versionen nicht erkannt werden können. Weitere Informationen finden Sie unter JNI Local Reference Changes in ICS.

    In Android-Versionen vor Android 8.0 ist die Anzahl der lokalen Referenzen auf ein versionsspezifisches Limit beschränkt. Ab Android 8.0 unterstützt Android unbegrenzte lokale Referenzen.

  • Referenztyp mit GetObjectRefType bestimmen

    Bis Android 4.0 (Ice Cream Sandwich) war es aufgrund der Verwendung direkter Zeiger (siehe oben) nicht möglich, GetObjectRefType korrekt zu implementieren. Stattdessen haben wir eine Heuristik verwendet, die in dieser Reihenfolge die Tabelle mit schwachen globalen Variablen, die Argumente, die Tabelle mit lokalen Variablen und die Tabelle mit globalen Variablen durchsucht hat. Beim ersten Auffinden des direkten Zeigers würde gemeldet, dass die Referenz vom Typ ist, der gerade untersucht wird. Das bedeutete beispielsweise, dass Sie bei einem Aufruf von GetObjectRefType für eine globale jclass, die zufällig mit der jclass übereinstimmte, die als implizites Argument an Ihre statische native Methode übergeben wurde, JNILocalRefType anstelle von JNIGlobalRefType erhielten.

  • @FastNative und @CriticalNative

    Bis Android 7 wurden diese Optimierungsanmerkungen ignoriert. Die ABI-Abweichung für @CriticalNative würde zu einer falschen Argument-Marshalling und wahrscheinlich zu Abstürzen führen.

    Die dynamische Suche nach nativen Funktionen für die Methoden @FastNative und @CriticalNative wurde in Android 8 bis 10 nicht implementiert und enthält in Android 11 bekannte Fehler. Wenn Sie diese Optimierungen ohne explizite Registrierung mit JNI RegisterNatives verwenden, kommt es auf Android 8 bis 11 wahrscheinlich zu Abstürzen.

  • FindClass wirft ClassNotFoundException

    Aus Gründen der Abwärtskompatibilität löst Android ClassNotFoundException anstelle von NoClassDefFoundError aus, wenn eine Klasse von FindClass nicht gefunden wird. Dieses Verhalten entspricht der Java Reflection API Class.forName(name).

FAQs: Warum erhalte ich UnsatisfiedLinkError?

Bei der Arbeit mit nativem Code ist es nicht ungewöhnlich, dass ein Fehler wie dieser auftritt:

java.lang.UnsatisfiedLinkError: Library foo not found

In einigen Fällen bedeutet das, was es sagt: Die Bibliothek wurde nicht gefunden. In anderen Fällen ist die Bibliothek vorhanden, konnte aber nicht von dlopen(3) geöffnet werden. Die Details des Fehlers finden Sie in der Detailmeldung der Ausnahme.

Häufige Gründe für „library not found“-Ausnahmen:

  • Die Bibliothek ist nicht vorhanden oder die App kann nicht darauf zugreifen. Verwenden Sie adb shell ls -l <path>, um zu prüfen, ob sie vorhanden ist und welche Berechtigungen sie hat.
  • Die Bibliothek wurde nicht mit dem NDK erstellt. Dies kann zu Abhängigkeiten von Funktionen oder Bibliotheken führen, die auf dem Gerät nicht vorhanden sind.

Eine weitere Klasse von UnsatisfiedLinkError-Fehlern sieht so aus:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

In Logcat sehen Sie Folgendes:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

Das bedeutet, dass die Laufzeit versucht hat, eine passende Methode zu finden, aber nicht erfolgreich war. Hier einige häufige Gründe dafür:

  • Die Bibliothek wird nicht geladen. Prüfen Sie die logcat-Ausgabe auf Meldungen zum Laden der Bibliothek.
  • Die Methode wird aufgrund einer Namens- oder Signaturabweichung nicht gefunden. Mögliche Ursachen:
    • Bei der verzögerten Methodensuche müssen C++-Funktionen mit extern "C" und der entsprechenden Sichtbarkeit (JNIEXPORT) deklariert werden. Vor Ice Cream Sandwich war das JNIEXPORT-Makro falsch. Die Verwendung eines neuen GCC mit einem alten jni.h funktioniert daher nicht. Mit arm-eabi-nm können Sie die Symbole so sehen, wie sie in der Bibliothek angezeigt werden. Wenn sie falsch dargestellt werden (z. B. _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass anstelle von Java_Foo_myfunc) oder der Symboltyp ein Kleinbuchstabe „t“ anstelle eines Großbuchstabens „T“ ist, müssen Sie die Deklaration anpassen.
    • Bei der expliziten Registrierung können kleinere Fehler bei der Eingabe der Methodensignatur auftreten. Achten Sie darauf, dass die Parameter, die Sie an den Registrierungsaufruf übergeben, mit der Signatur in der Logdatei übereinstimmen. Denken Sie daran, dass „B“ byte und „Z“ boolean ist. Klassennamenkomponenten in Signaturen beginnen mit „L“, enden mit „;“, verwenden „/“ zum Trennen von Paket-/Klassennamen und verwenden „$“ zum Trennen von Namen innerer Klassen (z. B. Ljava/util/Map$Entry;).

Wenn Sie javah verwenden, um JNI-Header automatisch zu generieren, können Sie einige Probleme vermeiden.

Häufig gestellte Fragen: Warum hat FindClass meinen Kurs nicht gefunden?

Die meisten dieser Empfehlungen gelten gleichermaßen für Fehler beim Suchen nach Methoden mit GetMethodID oder GetStaticMethodID oder nach Feldern mit GetFieldID oder GetStaticFieldID.

Prüfen Sie, ob der Klassenname-String das richtige Format hat. JNI-Klassennamen beginnen mit dem Paketnamen und werden durch Schrägstriche getrennt, z. B. java/lang/String. Wenn Sie eine Array-Klasse suchen, müssen Sie mit der entsprechenden Anzahl von eckigen Klammern beginnen und die Klasse auch mit „L“ und „;“ umschließen. Ein eindimensionales Array von String wäre also [Ljava/lang/String;. Wenn Sie nach einer inneren Klasse suchen, verwenden Sie „$“ anstelle von „.“ . Im Allgemeinen ist es eine gute Methode, mit javap in der .class-Datei den internen Namen Ihrer Klasse herauszufinden.

Wenn Sie die Code-Verkleinerung aktivieren, müssen Sie konfigurieren, welcher Code beibehalten werden soll. Die richtige Konfiguration von Keep-Regeln ist wichtig, da der Code-Shrinker sonst Klassen, Methoden oder Felder entfernen könnte, die nur über JNI verwendet werden.

Wenn der Klassenname richtig aussieht, liegt möglicherweise ein Problem mit dem Klassenlader vor. FindClass möchte die Klassensuche im mit Ihrem Code verknüpften Klassenloader starten. Dabei wird der Aufrufstapel untersucht, der in etwa so aussieht:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

Die oberste Methode ist Foo.myfunc. FindClass findet das ClassLoader-Objekt, das mit der Foo-Klasse verknüpft ist, und verwendet es.

Das ist in der Regel das, was Sie möchten. Sie können in Schwierigkeiten geraten, wenn Sie selbst einen Thread erstellen, z. B. durch Aufrufen von pthread_create und anschließendes Anhängen mit AttachCurrentThread. In diesem Fall sind keine Stackframes aus Ihrer Anwendung vorhanden. Wenn Sie FindClass über diesen Thread aufrufen, wird die JavaVM im Klassenlader „system“ anstelle des Klassenladers gestartet, der mit Ihrer Anwendung verknüpft ist. Versuche, anwendungsspezifische Klassen zu finden, schlagen daher fehl.

Es gibt mehrere Möglichkeiten, dieses Problem zu umgehen:

  • Führen Sie Ihre FindClass-Lookups einmal in JNI_OnLoad aus und speichern Sie die Klassenreferenzen für die spätere Verwendung im Cache. Alle FindClass-Aufrufe, die im Rahmen der Ausführung von JNI_OnLoad erfolgen, verwenden den Klassenlader, der der Funktion zugeordnet ist, die System.loadLibrary aufgerufen hat. Dies ist eine spezielle Regel, die die Initialisierung von Bibliotheken vereinfachen soll. Wenn Ihr App-Code die Bibliothek lädt, wird für FindClass der richtige Klassenlader verwendet.
  • Übergeben Sie eine Instanz der Klasse an die Funktionen, die sie benötigen, indem Sie Ihre native Methode so deklarieren, dass sie ein Class-Argument akzeptiert, und dann Foo.class übergeben.
  • Speichern Sie einen Verweis auf das ClassLoader-Objekt an einem geeigneten Ort und führen Sie loadClass-Aufrufe direkt aus. Das erfordert etwas Aufwand.

Häufig gestellte Fragen: Wie gebe ich Rohdaten für nativen Code frei?

Es kann vorkommen, dass Sie sowohl über verwalteten als auch über nativen Code auf einen großen Puffer mit Rohdaten zugreifen müssen. Gängige Beispiele sind die Bearbeitung von Bitmaps oder Sound-Samples. Es gibt zwei grundlegende Ansätze.

Sie können die Daten in einem byte[] speichern. Dies ermöglicht sehr schnellen Zugriff aus verwaltetem Code. Auf der nativen Seite ist jedoch nicht garantiert, dass Sie auf die Daten zugreifen können, ohne sie kopieren zu müssen. In einigen Implementierungen geben GetByteArrayElements und GetPrimitiveArrayCritical tatsächliche Zeiger auf die Rohdaten im verwalteten Heap zurück. In anderen wird ein Puffer im nativen Heap zugewiesen und die Daten werden kopiert.

Alternativ können Sie die Daten in einem direkten Bytepuffer speichern. Sie können mit java.nio.ByteBuffer.allocateDirect oder der JNI-Funktion NewDirectByteBuffer erstellt werden. Im Gegensatz zu regulären Byte-Puffern wird der Speicher nicht im verwalteten Heap zugewiesen und kann immer direkt über nativen Code aufgerufen werden (die Adresse wird mit GetDirectBufferAddress abgerufen). Je nachdem, wie der direkte Byte-Pufferzugriff implementiert ist, kann der Zugriff auf die Daten über verwalteten Code sehr langsam sein.

Die Entscheidung, welches verwendet werden soll, hängt von zwei Faktoren ab:

  1. Erfolgen die meisten Datenzugriffe über Code, der in Java oder C/C++ geschrieben wurde?
  2. Wenn die Daten letztendlich an eine System-API übergeben werden, in welcher Form müssen sie vorliegen? Wenn die Daten beispielsweise an eine Funktion übergeben werden, die ein Byte[] akzeptiert, ist die Verarbeitung in einem direkten ByteBuffer möglicherweise nicht sinnvoll.

Wenn es keinen eindeutigen Gewinner gibt, verwenden Sie einen direkten Byte-Puffer. Die Unterstützung für sie ist direkt in JNI integriert und die Leistung sollte sich in zukünftigen Releases verbessern.