JNI-Tipps

JNI ist die native Java-Schnittstelle. Sie 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 manchmal umständlich effizient.

Hinweis:Da Android Kotlin auf ähnliche Weise wie die Programmiersprache Java kompiliert, können Sie die Anleitungen auf dieser Seite im Hinblick auf die JNI-Architektur und die damit verbundenen Kosten sowohl auf die Programmiersprachen Kotlin als auch auf Java anwenden. Weitere Informationen finden Sie unter Kotlin und Android.

Wenn Sie damit noch nicht vertraut sind, lesen Sie die Java Native Interface Specification, um ein Gefühl dafür zu bekommen, wie JNI funktioniert und welche Features verfügbar sind. Einige Aspekte der Benutzeroberfläche sind aus den ersten Lesematerialien nicht sofort ersichtlich, sodass die nächsten Abschnitte für Sie von Nutzen sein können.

Mit der Ansicht JNI-Heap im Arbeitsspeicher-Profiler in Android Studio 3.2 und höher können Sie globale JNI-Referenzen durchsuchen und sehen, wo globale JNI-Referenzen erstellt und gelöscht werden.

Allgemeine Tipps

Versuchen Sie, den Fußabdruck Ihrer JNI-Ebene so gering wie möglich zu halten. Hier sind mehrere Dimensionen zu berücksichtigen. Ihre JNI-Lösung sollte nach Möglichkeit diesen Richtlinien entsprechen (nach Wichtigkeit sortiert, beginnend mit der wichtigsten):

  • Marshalling von Ressourcen auf der JNI-Ebene minimieren. Ein Marshalling auf der JNI-Ebene ist mit nicht trivialen Kosten verbunden. Versuchen Sie, eine Schnittstelle zu entwerfen, die die Datenmenge, die Sie per Marshall durchführen müssen, und die Häufigkeit, mit der Daten per Marshalling ausgeführt werden müssen, minimiert.
  • Vermeiden Sie nach Möglichkeit eine asynchrone Kommunikation zwischen Code, der in einer verwalteten Programmiersprache geschrieben ist, und Code, der in C++ geschrieben ist. So bleibt Ihre JNI-Schnittstelle wartungsfreundlicher. Sie können asynchrone UI-Aktualisierungen in der Regel vereinfachen, indem Sie die asynchrone Aktualisierung in derselben Sprache wie die UI belassen. Anstatt beispielsweise eine C++-Funktion aus dem UI-Thread im Java-Code über JNI aufzurufen, ist es besser, einen Callback zwischen zwei Threads in der Java-Programmiersprache durchzuführen. Einer von ihnen führt einen blockierenden C++-Aufruf aus und informiert dann den UI-Thread, wenn der blockierende Aufruf abgeschlossen ist.
  • Minimieren Sie die Anzahl der Threads, die von JNI angegriffen werden müssen. Wenn Sie Thread-Pools in den Sprachen Java und C++ verwenden müssen, versuchen Sie, die JNI-Kommunikation zwischen den Poolinhabern aufrechtzuerhalten, anstatt zwischen einzelnen Worker-Threads.
  • Bewahren Sie den Schnittstellencode an einer geringen Anzahl von leicht zu identifizierenden C++- und Java-Quellspeicherorten auf, um zukünftige Refaktorierungen zu erleichtern. Sie können gegebenenfalls eine JNI-Bibliothek zur automatischen Generierung verwenden.

JavaVM und JNIEnv

JNI definiert zwei wichtige Datenstrukturen, "JavaVM" und "JNIEnv". Beide sind im Wesentlichen Zeiger auf Zeiger auf Funktionstabellen. (In der C++-Version sind dies Klassen mit einem Zeiger auf eine Funktionstabelle und einer Member-Funktion für jede JNI-Funktion, die indirekt durch die Tabelle geleitet wird.) JavaVM bietet die Funktionen der Aufrufschnittstelle, mit denen Sie eine JavaVM erstellen und löschen können. Theoretisch können mehrere JavaVMs pro Prozess verwendet werden, Android lässt jedoch nur eine zu.

JNIEnv bietet die meisten JNI-Funktionen. Alle nativen Funktionen erhalten eine JNIEnv als erstes Argument, mit Ausnahme von @CriticalNative-Methoden. Weitere Informationen finden Sie unter Schnellere native Aufrufe.

JNIEnv wird für den lokalen Thread-Speicher verwendet. Daher können Sie eine JNIEnv-Datei nicht zwischen Threads teilen. Wenn ein Code-Snippet keine andere Möglichkeit hat, seine JNIEnv abzurufen, geben Sie die JavaVM frei und verwenden Sie GetEnv, um die JNIEnv des Threads zu ermitteln. (Angenommen, sie hat einen Wert, siehe AttachCurrentThread unten.)

Die C-Deklarationen von JNIEnv und JavaVM unterscheiden sich von den C++-Deklarationen. Die Include-Datei "jni.h" bietet unterschiedliche typedefs, je nachdem, ob sie in C oder C++ enthalten ist. Aus diesem Grund ist es ratsam, JNIEnv-Argumente in Headerdateien beider Sprachen aufzunehmen. Anders ausgedrückt: Wenn Ihre Header-Datei #ifdef __cplusplus erfordert, sind möglicherweise zusätzliche Arbeit erforderlich, falls sich etwas in diesem Header auf JNIEnv bezieht.

Unterhaltungen

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

In der Regel empfiehlt es sich, Thread.start() zu verwenden, um Threads zu erstellen, die Java-Code aufrufen müssen. Dadurch wird sichergestellt, dass Sie genügend Stackspeicherplatz haben, sich im richtigen ThreadGroup befinden und dasselbe ClassLoader wie für Ihren Java-Code verwenden. Es ist auch einfacher, den Namen des Threads für die Fehlerbehebung in Java festzulegen als in nativem Code (siehe pthread_setname_np(), wenn Sie pthread_t oder thread_t haben, und std::thread::native_handle(), wenn Sie std::thread haben und eine pthread_t benötigen).

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

Android sperrt keine Threads, die nativen Code ausführen. Wenn die automatische Speicherbereinigung durchgeführt wird oder der Debugger eine Anfrage zum Anhalten 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 umständlich 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 von dort DetachCurrentThread() aufrufen. Verwenden Sie diesen Schlüssel mit pthread_setspecific(), um JNIEnv in "thread-local-storage" zu speichern. So wird es als Argument an den Destruktor übergeben.

jclass, jmethodID und jfieldID

Wenn Sie über nativen Code auf ein Feld eines Objekts zugreifen möchten, gehen Sie so vor:

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

Ähnlich verhält es sich beim Aufrufen einer Methode: Sie erhalten zuerst einen Klassenobjektverweis und dann eine Methoden-ID. Die IDs sind oft nur Verweise auf interne Laufzeitdatenstrukturen. Für die Suche nach ihnen sind möglicherweise mehrere Stringvergleiche erforderlich. Sobald sie aber vorliegen, geht der tatsächliche Aufruf zum Abrufen des Felds oder zum Aufrufen der Methode sehr schnell.

Wenn die Leistung wichtig ist, ist es sinnvoll, die Werte einmal nachzuschlagen und die Ergebnisse in Ihrem nativen Code im Cache zu speichern. Da es ein Limit von einer 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 mit einem ClassLoader verknüpften Klassen durch eine automatische Speicherbereinigung bereinigt werden können. Dies ist selten, in Android aber 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 noch einmal im Cache speichern möchten, wenn die Klasse entladen und neu geladen wird, müssen Sie die IDs richtig initialisieren, indem Sie der entsprechenden Klasse ein Code-Snippet hinzufügen, das wie folgt 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-Suche ausführt. Der Code wird einmal ausgeführt, wenn die Klasse initialisiert wird. Wird die Klasse entladen und dann neu geladen, wird sie noch einmal ausgeführt.

Lokale und globale Referenzen

Jedes Argument, das an eine native Methode übergeben wird, und fast jedes von einer JNI-Funktion zurückgegebene Objekt ist eine "lokale Referenz". Das bedeutet, dass sie für die Dauer der aktuellen nativen Methode im aktuellen Thread gültig ist. Der Verweis ist auch dann ungültig, wenn das Objekt selbst weiter existiert, nachdem die native Methode zurückgegeben wurde.

Dies gilt für alle Unterklassen von jobject, einschließlich jclass, jstring und jarray. (Die Laufzeit warnt Sie bei den meisten Missbrauchsverwendungen von Referenzen, 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 hinweg aufbewahren möchten, müssen Sie eine „globale“ Referenz verwenden. Die Funktion NewGlobalRef verwendet den lokalen Verweis als Argument und gibt einen globalen Verweis zurück. Der globale Verweis ist garantiert gültig, bis Sie DeleteGlobalRef aufrufen.

Dieses Muster wird häufig beim Caching einer von FindClass zurückgegebenen jclass verwendet, z.B.:

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

Alle JNI-Methoden akzeptieren sowohl lokale als auch globale Verweise als Argumente. Verweise auf dasselbe Objekt können unterschiedliche Werte haben. Beispielsweise können sich die Rückgabewerte von aufeinanderfolgenden Aufrufen von NewGlobalRef für dasselbe Objekt unterscheiden. Mit der Funktion IsSameObject können Sie feststellen, ob zwei Verweise auf dasselbe Objekt verweisen. Vergleichen Sie niemals Referenzen mit == im nativen Code.

Dies hat zur Folge, dass Sie nicht davon ausgehen dürfen, dass Objektverweise im nativen Code konstant oder eindeutig sind. Der Wert, der ein Objekt darstellt, kann sich von einem Methodenaufruf zum nächsten unterscheiden. Außerdem ist es möglich, dass zwei verschiedene Objekte bei aufeinanderfolgenden Aufrufen denselben Wert haben. Verwenden Sie jobject-Werte nicht als Schlüssel.

Programmierer müssen lokale Referenzen nicht übermäßig zuweisen. In der Praxis bedeutet dies, dass Sie, wenn Sie eine große Anzahl lokaler Verweise erstellen, möglicherweise während der Ausführung eines Arrays von Objekten diese manuell mit DeleteLocalRef freigeben, 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 also mehr benötigen, sollten Sie entweder später löschen oder EnsureLocalCapacity/PushLocalFrame verwenden, um mehr zu reservieren.

jfieldID- und jmethodID-Werte sind intransparente Typen und keine Objektverweise. Sie sollten nicht an NewGlobalRef übergeben werden. Die Rohdatenzeiger, die von Funktionen wie GetStringUTFChars und GetByteArrayElements zurückgegeben werden, sind ebenfalls keine Objekte. Sie können zwischen Threads weitergegeben werden und sind bis zum entsprechenden Release-Aufruf gültig.

Ein ungewöhnlicher Fall verdient eine separate Erwähnung. Wenn Sie einen nativen Thread mit AttachCurrentThread anhängen, gibt der ausgeführte Code so lange lokale Verweise nicht automatisch kostenlos, bis der Thread getrennt ist. Alle von dir erstellten lokalen Referenzen müssen manuell gelöscht werden. Im Allgemeinen muss jeder native Code, der lokale Verweise in einer Schleife erstellt, wahrscheinlich manuell gelöscht werden.

Seien Sie bei der Verwendung globaler Verweise vorsichtig. Globale Verweise sind unvermeidlich, aber sie sind schwer zu debuggen und können zu schwer zu diagnostizierenden Arbeitsspeicherverhalten (Fehlverhalten) führen. Wenn alles andere gleich ist, ist eine Lösung mit weniger globalen Bezügen wahrscheinlich besser.

UTF-8- und UTF-16-Strings

Die Programmiersprache Java verwendet UTF-16. Der Einfachheit halber stellt JNI Methoden bereit, die auch mit geändertem UTF-8 funktionieren. Die geänderte Codierung ist für C-Code nützlich, da sie \u0000 als 0xc0 0x80 statt als 0x00 codiert. Das Tolle daran ist, dass Sie sich darauf verlassen können, dass Sie sich auf nullterminierte C-Strings verlassen können, die für die Verwendung mit standardmäßigen libc-Stringfunktionen geeignet sind. Die Kehrseite ist, dass Sie keine beliebigen UTF-8-Daten an JNI übergeben können und erwarten, dass sie ordnungsgemäß funktionieren.

Für die UTF-16-Darstellung von String verwenden Sie GetStringChars. UTF-16-Strings werden nicht mit Nullen beendet und „\u0000“ ist zulässig. Halten Sie sich also an die Stringlänge und den jchar-Zeiger.

Vergessen Sie nicht, die Strings zu Release, für die Sie Get verwenden. Die Stringfunktionen geben jchar* oder jbyte* zurück. Dabei handelt es sich um C-ähnliche Verweise auf primitive Daten und nicht um lokale Verweise. Sie sind garantiert gültig, bis Release aufgerufen wird. Sie werden also nicht freigegeben, wenn die native Methode zurückgegeben wird.

An NewStringUTF übergebene Daten müssen im geänderten UTF-8-Format vorliegen. Ein häufiger Fehler besteht darin, Zeichendaten aus einer Datei oder einem Netzwerkstream zu lesen und an NewStringUTF zu übergeben, ohne sie zu filtern. Sofern Sie nicht wissen, dass es sich bei den Daten um gültigen MUTF-8 (oder 7-Bit-ASCII-Code, eine kompatible Teilmenge) handelt, müssen Sie ungültige Zeichen entfernen oder in die richtige geänderte UTF-8-Form umwandeln. Andernfalls führt die UTF-16-Konvertierung wahrscheinlich zu unerwarteten Ergebnissen. CheckJNI, das für Emulatoren standardmäßig aktiviert ist, scannt Strings und bricht die VM ab, wenn sie ungültige Eingaben erhält.

Vor Android 8 war es in der Regel schneller, mit UTF-16-Strings zu arbeiten, da für Android keine Kopie in GetStringChars erforderlich war, während für GetStringUTFChars eine Zuordnung und 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 Arbeitsspeicher zu sparen. Außerdem wurde eine bewegende automatische Speicherbereinigung verwendet. Diese Funktionen reduzieren die Anzahl der Fälle, in denen ART einen Verweis auf die String-Daten bereitstellen kann, ohne eine Kopie zu erstellen, auch für GetStringCritical. Wenn jedoch die meisten vom Code verarbeiteten Strings kurz sind, können Sie die Zuweisung und die Aufhebung der Freigabe in den meisten Fällen vermeiden, indem Sie einen Stack-basierten Zwischenspeicher und GetStringRegion oder GetStringUTFRegion verwenden. Beispiele:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr 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);

Primitive Arrays

JNI bietet Funktionen für den Zugriff auf die Inhalte von Array-Objekten. Auf Arrays von Objekten muss jeweils mit einem Eintrag zugegriffen werden. Arrays mit Primitiven können aber direkt so 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, ermöglicht die Aufruffamilie Get<PrimitiveType>ArrayElements der Laufzeit, entweder einen Zeiger auf die tatsächlichen Elemente zurückzugeben oder etwas Arbeitsspeicher zuzuweisen und eine Kopie zu erstellen. In beiden Fällen ist der zurückgegebene Rohzeiger garantiert gültig, bis der entsprechende Release-Aufruf ausgegeben wird. Dies impliziert, dass das Array-Objekt angeheftet wird und im Rahmen der Verdichtung des Heaps nicht verschoben werden kann, wenn die Daten nicht kopiert wurden. Sie müssen für jedes Array, das Sie Get verwenden, Release. Wenn der Get-Aufruf fehlschlägt, dürfen Sie außerdem darauf achten, dass Ihr Code nicht später versucht, einen NULL-Zeiger mit Release zu senden.

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

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

  • 0
    • Tatsächlich: Das Array-Objekt ist nicht angepinnt.
    • Kopieren: Die Daten werden zurückkopiert. Der Zwischenspeicher mit der Kopie wird freigegeben.
  • JNI_COMMIT
    • Tatsächlich: keine Aktion.
    • Kopieren: Die Daten werden zurückkopiert. Der Zwischenspeicher mit der Kopie wird nicht freigegeben.
  • JNI_ABORT
    • Tatsächlich: Das Array-Objekt ist nicht angepinnt. Frühere Schreibvorgänge werden nicht abgebrochen.
    • Copy: Der Zwischenspeicher mit der Kopie wird freigegeben; alle daran vorgenommenen Änderungen gehen verloren.

Ein Grund für die Überprüfung des Flags isCopy besteht darin, zu wissen, ob Sie Release mit JNI_COMMIT aufrufen müssen, nachdem Sie Änderungen an einem Array vorgenommen haben. Wenn Sie zwischen Änderungen und dem Ausführen von Code wechseln, der den Inhalt des Arrays verwendet, können Sie den no-op-Commit möglicherweise überspringen. Ein weiterer möglicher Grund für das Prüfen des Flags ist die effiziente Handhabung von JNI_ABORT. Sie können beispielsweise ein Array abrufen, es an Ort und Stelle ändern, Teile an andere Funktionen übergeben und dann die Änderungen 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 (im Beispielcode wiederholt) wird angenommen, dass der Release-Aufruf übersprungen werden kann, wenn *isCopy „false“ ist. Das ist nicht der Fall. Wenn kein Kopierpuffer zugewiesen wurde, muss der ursprüngliche Arbeitsspeicher festgelegt werden und kann von der automatischen Speicherbereinigung nicht verschoben werden.

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

Regionsaufrufe

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

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

Dadurch wird das Array abgerufen, die ersten len-Byteelemente daraus kopiert und dann das Array freigegeben. Je nach Implementierung wird durch den Get-Aufruf der Arrayinhalt entweder angepinnt oder kopiert. Der Code kopiert die Daten (möglicherweise ein zweites Mal) und ruft dann Release auf. In diesem Fall sorgt JNI_ABORT dafür, dass es nicht möglich ist, eine dritte Kopie zu erstellen.

Das Gleiche lässt sich einfacher erreichen:

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

Das hat mehrere Vorteile:

  • Erfordert einen statt zwei JNI-Aufrufen, wodurch der Aufwand reduziert wird.
  • Anpinnen oder zusätzliche Datenkopien sind nicht erforderlich.
  • Reduziert das Risiko von Programmierfehlern – kein Risiko, dass Sie vergessen, Release nach einem Fehler aufzurufen.

Ähnlich können Sie den Aufruf Set<Type>ArrayRegion verwenden, um Daten in ein Array zu kopieren, und GetStringRegion oder GetStringUTFRegion, um Zeichen aus einer String zu kopieren.

Ausnahmen

Die meisten JNI-Funktionen dürfen nicht aufgerufen werden, solange eine Ausnahme aussteht. Es wird erwartet, dass Ihr Code die Ausnahme bemerkt (über den Rückgabewert der Funktion, ExceptionCheck oder ExceptionOccurred) und zurückgibt oder die Ausnahme löscht und verarbeitet.

Die einzigen JNI-Funktionen, die Sie während einer Ausnahme aufrufen können, 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 zur Fehlerprüfung. Wenn NewString beispielsweise einen Wert ungleich NULL zurückgibt, müssen Sie nicht auf eine Ausnahme prüfen. Wenn Sie jedoch eine Methode mit einer Funktion wie CallObjectMethod aufrufen, müssen Sie immer auf eine Ausnahme prüfen, da der Rückgabewert bei einer Ausnahme nicht gültig ist.

Von verwaltetem Code ausgelöste Ausnahmen wirken sich nicht auf native Stack-Frames aus. (Und C++-Ausnahmen, von denen in der Regel unter Android abgeraten wird, dürfen nicht über die JNI-Übergangsgrenze von C++-Code zu verwaltetem Code verschoben werden.) Die JNI-Anweisungen Throw und ThrowNew haben gerade einen Ausnahmezeiger im aktuellen Thread festgelegt. Nach der Rückkehr zum verwalteten Code aus dem nativen Code wird die Ausnahme notiert und entsprechend behandelt.

Nativer Code kann eine Ausnahme durch Aufrufen von ExceptionCheck oder ExceptionOccurred erfassen und mit ExceptionClear löschen. Wie immer kann das Verwerfen von Ausnahmen ohne deren Verarbeitung zu Problemen führen.

Es gibt keine integrierten Funktionen zur Bearbeitung des Throwable-Objekts selbst. Wenn Sie also beispielsweise den Ausnahmestring abrufen möchten, müssen Sie die Throwable-Klasse suchen, die Methoden-ID für getMessage "()Ljava/lang/String;" abrufen und aufrufen. Wenn das Ergebnis nicht NULL ist, verwenden Sie GetStringUTFChars, um etwas zu erhalten, das Sie an printf(3) oder ein gleichwertiges Objekt übergeben können.

Erweiterte Überprüfung

JNI führt sehr wenig Fehlerprüfungen durch. Fehler führen in der Regel zu einem Absturz. Android bietet außerdem einen Modus namens CheckJNI, in dem die Funktionstabellenzeiger von JavaVM und JNIEnv in Funktionstabellen umgewandelt werden, die vor dem Aufrufen der Standardimplementierung eine umfangreiche Reihe von Prüfungen durchführen.

Zu den zusätzlichen Prüfungen gehören:

  • Arrays: Versuch, ein Array mit negativer Größe zuzuweisen.
  • Fehlerhafte Zeiger: Übergeben eines fehlerhaften jarray/jclass/jobject/jstrings an einen JNI-Aufruf oder einem NULL-Zeiger an einen JNI-Aufruf mit einem Argument, das keine Nullwerte zulässt.
  • Klassennamen: Übergabe von Klassennamen mit Ausnahme des Stils "java/lang/String" an einen JNI-Aufruf.
  • Kritische Aufrufe: JNI-Aufruf zwischen einem „kritischen“ Get und dem entsprechenden Release.
  • Direct ByteBuffers: fehlerhafte Argumente an NewDirectByteBuffer übergeben.
  • Ausnahmen: ein JNI-Aufruf wird ausgeführt, wenn eine Ausnahme aussteht.
  • JNIEnv*s: Verwendung einer JNIEnv* aus dem falschen Thread.
  • jfieldIDs: Verwendung einer jfieldID NULL-Wertes oder Verwendung einer jfieldID zum Festlegen eines Felds auf einen Wert des falschen Typs (z. B. wenn Sie versuchen, einem String-Feld einen StringBuilder zuzuweisen) oder eine jfieldID für ein statisches Feld verwenden, um ein Instanzfeld festzulegen oder umgekehrt, oder eine jfieldID aus einer Klasse mit Instanzen einer anderen Klasse verwenden.
  • jmethodIDs: bei einem Call*Method-JNI-Aufruf wird die falsche Art von jmethodID verwendet: falscher Rückgabetyp, statische/nicht-statische Diskrepanz, falscher Typ für „this“ (für nicht statische Aufrufe) oder falsche Klasse (für statische Aufrufe).
  • Referenzen: Verwendung von DeleteGlobalRef/DeleteLocalRef für die falsche Art von Referenz.
  • Release-Modi: Übergeben eines fehlerhaften Release-Modus an einen Release-Aufruf (etwas anderes als 0, JNI_ABORT oder JNI_COMMIT).
  • Typsicherheit: Rückgabe eines inkompatiblen Typs von Ihrer nativen Methode (Rückgabe eines StringBuilders von einer Methode, die für die Rückgabe eines Strings deklariert ist).
  • UTF-8: Übergeben einer ungültigen Modified UTF-8-Bytesequenz an einen JNI-Aufruf.

(Die Barrierefreiheit von Methoden und Feldern wird immer 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 folgende Befehlsfolge verwenden, um die Laufzeit bei aktiviertem CheckJNI neu zu starten:

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

In beiden Fällen sehen Sie beim Start der Laufzeit in Ihrer Logcat-Ausgabe etwa Folgendes:

D AndroidRuntime: CheckJNI is ON

Wenn Sie ein normales 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 aktiviert. (Ändern Sie die Eigenschaft in einen anderen Wert oder starten Sie CheckJNI einfach erneut.) In diesem Fall sehen Sie in Ihrer Logcat-Ausgabe beim nächsten Start einer Anwendung in etwa Folgendes:

D Late-enabling CheckJNI

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

Native Bibliotheken

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

In der Praxis gab es bei älteren Android-Versionen Fehler im PackageManager, die dazu führten, dass die Installation und Aktualisierung nativer Bibliotheken unzuverlässig war. Das Projekt ReLinker bietet Problemumgehungen für diese und andere Probleme beim Laden nativer Bibliotheken.

Rufen Sie System.loadLibrary (oder ReLinker.loadLibrary) von einem statischen Klasseninitialisierer auf. Das Argument ist der Bibliotheksname "undecoated". Um libfubar.so zu laden, übergeben Sie "fubar".

Wenn Sie nur eine Klasse mit nativen Methoden haben, ist es sinnvoll, dass der Aufruf von System.loadLibrary in einem statischen Initialisierer für diese Klasse erfolgt. Andernfalls können Sie den Aufruf von Application ausführen, damit Sie wissen, dass die Bibliothek immer und früh geladen wird.

Es gibt zwei Möglichkeiten, wie die Laufzeit Ihre nativen Methoden finden kann. Sie können sie entweder explizit bei RegisterNatives registrieren oder sie von der Laufzeit mit dlsym dynamisch abrufen lassen. Die Vorteile von RegisterNatives bestehen darin, dass Sie im Voraus prüfen, ob die Symbole vorhanden sind. Außerdem können Sie kleinere und schnellere gemeinsam genutzte Bibliotheken verwenden, indem Sie nur JNI_OnLoad exportieren. Der Vorteil der Erkennung der Funktionen durch die Laufzeit besteht darin, dass weniger Code geschrieben werden muss.

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

  • Geben Sie eine JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)-Funktion an.
  • Registrieren Sie in Ihrem JNI_OnLoad alle nativen Methoden mit RegisterNatives.
  • Erstellen Sie mit -fvisibility=hidden, damit nur JNI_OnLoad aus Ihrer Bibliothek exportiert wird. Dies erzeugt schnelleren und kleineren Code und vermeidet potenzielle Kollisionen mit anderen Bibliotheken, die in Ihre App geladen werden. Allerdings entstehen dadurch weniger nützliche Stacktraces, wenn Ihre App im nativen Code abstürzt.

Der statische Initialisierer sollte wie folgt 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 „Discovery“ nativer Methoden verwenden möchten, müssen Sie sie auf eine bestimmte Weise benennen. Einzelheiten finden Sie in der JNI-Spezifikation. Wenn eine Methodensignatur falsch ist, erfahren Sie also erst davon, wenn die Methode zum ersten Mal tatsächlich aufgerufen wird.

Alle von JNI_OnLoad ausgeführten FindClass-Aufrufe lösen Klassen im Kontext des Klassenladeprogramms aus, das zum Laden der gemeinsam genutzten Bibliothek verwendet wurde. Wird FindClass aus anderen Kontexten aufgerufen, wird der Klassenladeprogramm verwendet, der der Methode oben im Java-Stack zugeordnet ist. Ist keins vorhanden, weil der Aufruf von einem nativen Thread stammt, der gerade angehängt wurde, wird das Klassenladeprogramm "System" verwendet. Das Systemklassenladeprogramm kennt die Klassen Ihrer Anwendung nicht, sodass Sie mit FindClass in diesem Kontext nicht Ihre eigenen Klassen nachschlagen können. Dies macht JNI_OnLoad zu einem praktischen Ort, um Klassen zu suchen und im Cache zu speichern: Sobald Sie eine gültige globale Referenz für jclass haben, können Sie sie von jedem angehängten Thread aus verwenden.

Schnellere native Anrufe mit @FastNative und @CriticalNative

Native Methoden können mit @FastNative oder @CriticalNative (aber nicht mit beidem) annotiert werden, um den Übergang zwischen verwaltetem und nativem Code zu beschleunigen. Diese Annotationen bringen jedoch gewisse Verhaltensänderungen mit sich, die vor der Verwendung sorgfältig bedacht werden müssen. Diese Änderungen werden im Folgenden kurz erwähnt. Einzelheiten können Sie in der Dokumentation nachlesen.

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

Beim Ausführen einer @FastNative- oder @CriticalNative-Methode kann die automatische Speicherbereinigung den Thread für wichtige Aufgaben nicht anhalten und kann daher blockiert werden. Verwenden Sie diese Annotationen nicht für Methoden mit langer Ausführungszeit, einschließlich normalerweise schneller, aber in der Regel unbegrenzter Methoden. Insbesondere sollte der Code keine nennenswerten E/A-Vorgänge ausführen oder native Sperren erwerben, die über einen längeren Zeitraum beibehalten werden können.

Diese Annotationen wurden seit Android 8 für die Systemnutzung implementiert und wurden unter Android 14 von der CTS getestet. Diese Optimierungen funktionieren wahrscheinlich auch auf Geräten mit Android 8 bis 13, allerdings ohne die starken CTS-Garantien. Die dynamische Suche nativer Methoden wird jedoch nur unter Android 12 und höher unterstützt. Die explizite Registrierung bei JNI RegisterNatives ist für die Ausführung unter Android-Versionen 8 bis 11 jedoch unbedingt erforderlich. Diese Anmerkungen werden unter Android 7 ignoriert. Die ABI-Abweichung für @CriticalNative würde zu einem falschen Argument-Marshalling und wahrscheinlichen Abstürzen führen.

Bei leistungskritischen Methoden, die diese Annotationen benötigen, wird dringend empfohlen, die Methode(n) explizit bei JNI RegisterNatives zu registrieren, anstatt sich auf die Namensbasierte „Erkennung“ nativer Methoden zu verlassen. Für eine optimale Startleistung der Anwendung wird empfohlen, Aufrufer der Methoden @FastNative oder @CriticalNative in das Referenzprofil aufzunehmen. Seit Android 12 ist ein Aufruf einer nativen @CriticalNative-Methode von einer kompilierten verwalteten Methode fast so kostengünstig wie ein Nicht-Inline-Aufruf in C/C++, sofern alle Argumente in Registern passen (z. B. bis zu 8 Integral- und bis zu 8 Gleitkommaargumente bei Verzweigung 64).

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

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

Hinweise zu 64-Bit-Versionen

Verwenden Sie zur Unterstützung von Architekturen, die 64-Bit-Zeiger verwenden, ein long-Feld anstelle von int, wenn Sie einen Zeiger auf eine native Struktur in einem Java-Feld speichern.

Nicht unterstützte Funktionen/Abwärtskompatibilität

Es werden alle JNI 1.6-Funktionen mit der folgenden Ausnahme unterstützt:

  • DefineClass ist nicht implementiert. Android verwendet keine Java-Bytecodes oder -Klassendateien, sodass die Übergabe von Binärklassendaten nicht funktioniert.

Für die Abwärtskompatibilität mit älteren Android-Releases musst du unter Umständen Folgendes beachten:

  • Dynamisches Suchen nativer Funktionen

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

  • Threads trennen

    Bis Android 2.0 (Eclair) war es nicht möglich, eine pthread_key_create-Destruktorfunktion zu verwenden, um die Prüfung „Thread muss vor dem Beenden getrennt werden“ zu vermeiden. (Die Laufzeit verwendet auch eine pthread-Schlüssel-Destruktorfunktion, Sie müssten also erst einmal herausfinden, welche zuerst aufgerufen wird.)

  • Schwache globale Referenzen

    Bis Android 2.2 (Froyo) wurden schwache globale Referenzen nicht implementiert. Ältere Versionen lehnen Versuche, sie zu verwenden, gänzlich ab. Sie können die Versionskonstanten der Android-Plattform verwenden, um den Support zu testen.

    Bis Android 4.0 (Ice Cream Sandwich) konnten schwache globale Verweise nur an NewLocalRef, NewGlobalRef und DeleteWeakGlobalRef übergeben werden. (Die Spezifikation empfiehlt Programmierern dringend, harte Verweise auf schwache globale Variablen zu erstellen, bevor sie etwas damit unternehmen. 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 Verweise eigentlich direkte Verweise. Ice Cream Sandwich hat die erforderliche Indirektion hinzugefügt, um bessere automatische Speicherbereinigungsdienste zu unterstützen. Dies bedeutet jedoch, dass viele JNI-Fehler in älteren Releases nicht erkannt werden können. Weitere Informationen finden Sie unter Änderungen bei lokalen JNI-Referenzen 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 eine unbegrenzte Anzahl lokaler Verweise.

  • Referenztyp mit GetObjectRefType ermitteln

    Bis Android 4.0 (Ice Cream Sandwich) war es aufgrund der Verwendung von direkten Zeigern (siehe oben) nicht möglich, GetObjectRefType korrekt zu implementieren. Stattdessen haben wir eine Heuristik verwendet, die die schwache globals-Tabelle, die Argumente, die lokalen Tabellen und die globals-Tabelle in dieser Reihenfolge durchgegangen ist. Wenn er zum ersten Mal Ihren direkten Cursor gefunden hat, meldet er, dass es sich um eine Referenz handelt, die gerade untersucht wird. Wenn Sie beispielsweise GetObjectRefType für eine globale jclass aufrufen, die mit der jclass identisch ist, die als implizites Argument an Ihre statische native Methode übergeben wurde, erhalten Sie JNILocalRefType anstelle von JNIGlobalRefType.

  • @FastNative und @CriticalNative

    Bis Android 7 wurden diese Optimierungshinweise ignoriert. Die ABI-Abweichung für @CriticalNative würde zu falschem Argument-Marshalling und wahrscheinlichen 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 bekannte Fehler in Android 11. Die Verwendung dieser Optimierungen ohne explizite Registrierung bei JNI RegisterNatives führt unter Android 8 bis 11 wahrscheinlich zu Abstürzen.

FAQ: Warum erhalte ich UnsatisfiedLinkError?

Bei der Arbeit mit nativem Code sind Fehler wie diese nicht ungewöhnlich:

java.lang.UnsatisfiedLinkError: Library foo not found

Manchmal bedeutet dies, was er sagt: Die Bibliothek wurde nicht gefunden. In anderen Fällen ist die Bibliothek vorhanden, konnte aber nicht mit dlopen(3) geöffnet werden. Die Details des Fehlers finden Sie in der Detailnachricht zur Ausnahme.

Häufige Gründe für Ausnahmen vom Typ „Bibliothek nicht gefunden“:

  • Die Bibliothek ist nicht vorhanden oder für die App nicht zugänglich. Verwenden Sie adb shell ls -l <path>, um ihre Präsenz und Berechtigungen zu prüfen.
  • 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 wird Folgendes angezeigt:

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

Dies bedeutet, dass die Laufzeit versucht hat, eine übereinstimmende Methode zu finden, jedoch nicht erfolgreich war. Hier einige häufige Gründe:

  • Die Bibliothek wird nicht geladen. Suchen Sie in der Logcat-Ausgabe nach Meldungen zum Laden der Bibliothek.
  • Die Methode wird aufgrund eines Namens oder einer Signatur nicht gefunden. Dies hat häufig die folgenden Ursachen:
    • Bei einem lazy-Method-Lookup konnten keine C++-Funktionen mit extern "C" und der entsprechenden Sichtbarkeit (JNIEXPORT) deklariert werden. Vor Ice Cream Sandwich war das JNIEXPORT-Makro falsch, sodass die Verwendung einer neuen GCC mit einem alten jni.h nicht funktionierte. Mit arm-eabi-nm können Sie die Symbole so sehen, wie sie in der Bibliothek angezeigt werden. Wenn sie verzerrt aussehen (etwa _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass statt Java_Foo_myfunc) oder wenn der Symboltyp ein kleingeschriebenes „t“ statt ein großes „T“ ist, müssen Sie die Deklaration anpassen.
    • Bei expliziter Registrierung kleinere Fehler beim Eingeben der Methodensignatur. Die Informationen, die du an den Registrierungsaufruf übergeben, müssen mit der Signatur in der Protokolldatei übereinstimmen. Denke daran, dass „B“ byte und „Z“ boolean ist. Komponenten für Klassennamen in Signaturen beginnen mit „L“, enden mit „;“. Verwenden Sie „/“, um Paket-/Klassennamen zu trennen, und „$“, um Namen innerer Klassen zu trennen (z. B. Ljava/util/Map$Entry;).

Durch die Verwendung von javah zum automatischen Generieren von JNI-Headern können einige Probleme vermieden werden.

FAQ: Warum hat FindClass meinen Kurs nicht gefunden?

Die meisten dieser Hinweise gelten gleichermaßen für Fehler, bei denen Methoden mit GetMethodID oder GetStaticMethodID oder Felder mit GetFieldID oder GetStaticFieldID nicht gefunden werden.

Achten Sie darauf, dass der String des Klassennamens das richtige Format hat. JNI-Klassennamen beginnen mit dem Paketnamen und werden durch Schrägstriche getrennt, z. B. java/lang/String. Wenn Sie eine Arrayklasse suchen, müssen Sie mit der entsprechenden Anzahl von eckigen Klammern beginnen und außerdem die Klasse 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 „$“ statt „.“. Im Allgemeinen ist die Verwendung von javap in der .class-Datei eine gute Möglichkeit, den internen Namen Ihrer Klasse zu ermitteln.

Wenn Sie die Codekomprimierung aktivieren, müssen Sie konfigurieren, welcher Code beibehalten werden soll. Die Konfiguration der richtigen Aufbewahrungsregeln ist wichtig, da durch den Codeschrumpf möglicherweise Klassen, Methoden oder Felder entfernt werden, die nur von JNI verwendet werden.

Wenn der Klassenname stimmt, liegt möglicherweise ein Problem mit dem Klassenladeprogramm vor. FindClass möchte die Klassensuche in dem Klassenladeprogramm starten, das mit Ihrem Code verknüpft ist. Sie prüft den Aufrufstack, der in etwa so aussieht:

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

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

Das macht normalerweise das, was Sie wollen. Es kann zu Problemen kommen, wenn Sie selbst einen Thread erstellen (z. B. indem Sie pthread_create aufrufen und dann mit AttachCurrentThread anhängen). Jetzt gibt es keine Stack-Frames von Ihrer Anwendung. Wenn Sie FindClass aus diesem Thread aufrufen, wird die JavaVM im Klassenladeprogramm „System“ statt in dem mit Ihrer Anwendung verknüpften gestartet. Daher schlagen Versuche, anwendungsspezifische Klassen zu finden, fehl.

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

  • Führen Sie die FindClass-Lookups einmal in JNI_OnLoad durch und speichern Sie die Klassenverweise zur späteren Verwendung im Cache. Alle FindClass-Aufrufe, die beim Ausführen von JNI_OnLoad erfolgen, verwenden das Klassenladeprogramm, das der Funktion namens System.loadLibrary zugeordnet ist. Dies ist eine spezielle Regel, die die Initialisierung der Bibliothek erleichtert. Wenn die Bibliothek mit Ihrem App-Code geladen wird, verwendet FindClass den korrekten Klassenladeprogramm.
  • Übergeben Sie eine Instanz der Klasse an die Funktionen, die sie benötigen. Deklarieren Sie dazu Ihre native Methode, die ein Klassenargument verwendet, und übergeben Sie dann Foo.class.
  • Speichern Sie einen Verweis auf das ClassLoader-Objekt an einem leicht zugänglichen Ort und senden Sie loadClass-Aufrufe direkt. Dies ist etwas aufwändig.

FAQ: Wie teile ich Rohdaten mit nativem Code?

Es kann vorkommen, dass Sie aus verwaltetem und nativem Code auf einen großen Puffer an Rohdaten zugreifen müssen. Gängige Beispiele sind die Bearbeitung von Bitmaps oder Tonbeispielen. Es gibt zwei grundlegende Ansätze.

Sie können die Daten in einem byte[] speichern. Dies ermöglicht einen sehr schnellen Zugriff über verwalteten Code. Bei nativen Seiten gibt es jedoch keine Garantie, dass Sie auf die Daten zugreifen können, ohne sie kopieren zu müssen. In einigen Implementierungen geben GetByteArrayElements und GetPrimitiveArrayCritical tatsächliche Verweise auf die Rohdaten im verwalteten Heap zurück. In anderen Fällen wird jedoch ein Zwischenspeicher im nativen Heap zugewiesen und die Daten werden kopiert.

Die Alternative besteht darin, die Daten in einem direkten Byte-Zwischenspeicher zu speichern. Diese können mit java.nio.ByteBuffer.allocateDirect oder der JNI-Funktion NewDirectByteBuffer erstellt werden. Im Gegensatz zu normalen Bytezwischenspeichern wird der Speicher nicht auf dem verwalteten Heap zugewiesen und kann immer direkt aus dem nativen Code aufgerufen werden (Rufen Sie die Adresse mit GetDirectBufferAddress ab). Je nachdem, wie der direkte Byte-Zwischenspeicherzugriff implementiert ist, kann der Zugriff auf die Daten aus verwaltetem Code sehr langsam sein.

Die Wahl des Tools hängt von zwei Faktoren ab:

  1. Erfolgen die meisten Datenzugriffe über Code, der in Java oder C/C++ geschrieben wurde?
  2. In welcher Form müssen die Daten vorliegen, wenn sie an eine System-API übergeben werden? Wenn die Daten beispielsweise an eine Funktion übergeben werden, die ein byte[] annimmt, könnte die Verarbeitung in einer direkten ByteBuffer unklug sein.

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