JNI-Tipps

JNI ist das native Java-Interface. Sie definiert eine Möglichkeit, wie der Bytecode, den Android aus verwaltetem Code (in den Programmiersprachen Java oder Kotlin geschrieben) kompiliert, mit nativem Code (in C/C++) interagieren kann. JNI ist anbieterneutral, unterstützt das Laden von Code aus dynamischen freigegebenen Bibliotheken und ist zwar manchmal umständlich, aber relativ effizient.

Hinweis:Da Android Kotlin in ART-kompatiblen Bytecode kompiliert, ähnlich wie bei der Programmiersprache Java, können Sie die Anweisungen auf dieser Seite sowohl auf Kotlin- und Java-Programmiersprachen im Hinblick auf die JNI-Architektur und die damit verbundenen Kosten zu testen. Weitere Informationen finden Sie unter Kotlin und Android

Wenn Sie noch nicht damit vertraut sind, lesen Sie die Spezifikation für native Java-Schnittstellen wie JNI funktioniert und welche Funktionen verfügbar sind. Einige auf der Benutzeroberfläche nicht sofort damit Sie die nächsten Abschnitte praktisch finden können.

Um globale JNI-Referenzen zu durchsuchen und zu sehen, wo globale JNI-Referenzen erstellt und gelöscht werden, verwenden Sie Ansicht JNI-Heap im Memory Profiler in Android Studio 3.2 und höher.

Allgemeine Tipps

Versuchen Sie, den Platzbedarf Ihrer JNI-Schicht zu minimieren. Hier sind mehrere Dimensionen zu berücksichtigen. Ihre JNI-Lösung sollte diese Richtlinien befolgen (unten nach Wichtigkeit sortiert, beginnend mit dem wichtigsten):

  • Marshalling von Ressourcen auf der JNI-Ebene minimieren. Marshalling über die JNI-Schicht mit relativ geringen Kosten verbunden ist. Versuchen Sie, eine Schnittstelle zu entwerfen, Daten, die Sie marshallen müssen, und die Häufigkeit, mit der Sie Daten marshallen müssen.
  • Die asynchrone Kommunikation zwischen Code, der in einer verwalteten Programmierung geschrieben ist, wird vermieden. Sprache und Code in C++ schreiben, wenn möglich. So lässt sich Ihre JNI-Schnittstelle leichter verwalten. In der Regel können Sie asynchrone Aktualisierungen der Benutzeroberfläche, indem das asynchrone Update in derselben Sprache wie die UI belassen wird Anstatt beispielsweise eine C++-Funktion aus dem UI-Thread im Java-Code über JNI aufzurufen, ist es besser, einen Rückruf zwischen zwei Threads in der Java-Programmiersprache auszuführen. Dabei führt einer der Threads 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ühren oder berührt werden müssen. Wenn Sie Threadpools sowohl in Java als auch in C++ verwenden müssen, sollten Sie die JNI-Kommunikation zwischen den Pooleigentümern und nicht zwischen einzelnen Worker-Threads aufrechterhalten.
  • Behalten Sie den Interface-Code an wenigen, leicht zu identifizierenden C++- und Java-Quellcode-Standorten, um zukünftige Refaktorisierungen zu erleichtern. Verwenden Sie gegebenenfalls eine JNI-Bibliothek zur automatischen Generierung.

JavaVM und JNIEnv

JNI definiert zwei wichtige Datenstrukturen, „JavaVM“ und "JNIEnv". Beide sind im Wesentlichen auf Zeiger auf Funktionstabellen. (In der C++-Version sind es Klassen mit einer Zeiger auf eine Funktionstabelle und eine Member-Funktion für jede JNI-Funktion, die einen indirekten in der Tabelle.) Die JavaVM stellt die Aufrufschnittstelle bereit. Funktionen, 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.

JNIEnv bietet die meisten JNI-Funktionen. Ihre nativen Funktionen erhalten alle eine JNIEnv als das erste Argument, mit Ausnahme der @CriticalNative-Methoden, Siehe schnellere native Aufrufe.

JNIEnv wird für den lokalen Thread-Speicher verwendet. Aus diesem Grund können Sie eine JNIEnv nicht zwischen Threads teilen. Wenn ein Code-Snippet keine andere Möglichkeit hat, seine JNIEnv abzurufen, sollten Sie die JavaVM und verwenden GetEnv, um die JNIEnv des Threads zu ermitteln. (Vorausgesetzt, es gibt eine; siehe AttachCurrentThread unten.)

Die C-Deklarationen von JNIEnv und JavaVM unterscheiden sich von den C++-Deklarationen. Die "jni.h"-Include-Datei enthält unterschiedliche typedefs, je nachdem, ob sie in C oder C++ eingebunden ist. Aus diesem Grund ist es keine gute Idee, JNIEnv-Argumente in Headerdateien aufzunehmen, die von beiden Sprachen eingebunden werden. Anders ausgedrückt: Wenn Ihre Headerdatei erfordert #ifdef __cplusplus. Möglicherweise müssen Sie zusätzliche Schritte ausführen, verweist dieser Header auf JNIEnv.)

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 die JavaVM angehängt werden. Für Beispiel: Eine Unterhaltung, die mit pthread_create() oder std::thread gestartet wurde kann mit AttachCurrentThread() oder AttachCurrentThreadAsDaemon()-Funktionen. Solange ein Thread nicht angehängt ist, hat er keine JNIEnv und kann keine JNI-Aufrufe ausführen.

In der Regel ist es am besten, Thread.start() zu verwenden, um Threads zu erstellen, die in Java-Code ein. Auf diese Weise stellen Sie sicher, dass Sie über genügend Stack-Speicherplatz verfügen, in der richtigen ThreadGroup und Sie verwenden denselben ClassLoader wie Ihren Java-Code. Außerdem ist es einfacher, den Namen des Threads für das Debuggen in Java als in nativem Code festzulegen (siehe pthread_setname_np(), wenn Sie pthread_t oder thread_t haben, und std::thread::native_handle(), wenn Sie std::thread und pthread_t haben möchten).

Wenn Sie einen nativ erstellten Thread anhängen, wird ein java.lang.Thread-Objekt erstellt und dem ThreadGroup-Objekt „main“ hinzugefügt, wodurch es für den Debugger sichtbar wird. AttachCurrentThread() wird angerufen für einen bereits angehängten Thread ist ein No-Op.

Android sperrt keine Threads, die nativen Code ausführen. Wenn Automatische Speicherbereinigung läuft oder der Debugger hat eine Sperrung ausgelöst -Anforderung verwendet, pausiert Android den Thread beim nächsten JNI-Aufruf.

Über JNI angehängte Threads müssen DetachCurrentThread() vor dem Beenden. Wenn die direkte Programmierung umständlich ist, kann mit pthread_key_create() einen Destruktor definieren -Funktion, die aufgerufen wird, bevor der Thread beendet wird, und rufe dort DetachCurrentThread() an. (Verwenden Sie dieses Schlüssel mit pthread_setspecific(), um die JNIEnv-Datei in Thread-local-storage; auf diese Weise in Ihren Zerstörer übergeben werden, das Argument.)

jclass, jmethodID und jfieldID

Wenn Sie über nativen Code auf das 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 Inhalt ab, z. B. GetIntField

Wenn Sie eine Methode aufrufen möchten, müssen Sie zuerst eine Referenz auf das Klassenobjekt und dann eine Methoden-ID abrufen. Die IDs sind oft nur Verweise auf interne Laufzeitdatenstrukturen. Für die Suche sind möglicherweise mehrere Zeichenfolgen erforderlich Vergleiche. Sobald sie jedoch vorliegen, wird der eigentliche Aufruf zum Abrufen des Felds oder zum Aufrufen der Methode sehr schnell.

Wenn Leistung wichtig ist, sollten die Werte einmal abgerufen und die Ergebnisse im Cache gespeichert werden. in Ihrem nativen Code. 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 dann entladen, wenn alle mit einem ClassLoader verknüpften Klassen dem Garbage Collector zugewiesen werden können. Das ist zwar selten, aber unter Android nicht unmöglich. Beachten Sie jedoch, dass jclass ist eine Klassenreferenz und muss mit einem Aufruf geschützt werden. an NewGlobalRef (siehe nächster Abschnitt).

Wenn Sie die IDs beim Laden einer Klasse und automatisch noch einmal im Cache speichern möchten Wenn die Klasse jemals entladen und neu geladen wird, ist die richtige Methode zum Initialisieren ID besteht darin, der entsprechenden Klasse ein Code-Snippet hinzuzufü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, mit der die ID-Suchen durchgeführt werden. Der Code wird einmal ausgeführt, wenn die Klasse initialisiert wird. Wenn die Klasse entladen und dann wieder 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 zurückgegeben von einer JNI-Funktion ist eine „lokale Referenz“. Das bedeutet, dass er für den Dauer der aktuellen nativen Methode im aktuellen Thread. Selbst wenn das Objekt nach der Rückgabe der nativen Methode weiter existiert, ist die Referenz ungültig.

Dies gilt für alle abgeleiteten Klassen von jobject, einschließlich jclass, jstring und jarray. Wenn erweiterte JNI-Prüfungen aktiviert sind, warnt die Laufzeit Sie vor den meisten Fehlnutzungen von Referenzen.

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

Wenn Sie eine Referenz für einen längeren Zeitraum beibehalten möchten, müssen Sie eine „globale“ Referenz verwenden. Mit der Funktion NewGlobalRef werden die Werte lokalen Verweis als Argument und gibt ein globales Argument zurück. Die globale Referenz ist garantiert gültig, bis du sie aufrufst. DeleteGlobalRef

Dieses Muster wird häufig verwendet, wenn eine zurückgegebene jclass-Klasse im Cache gespeichert wird. aus FindClass, 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. Verweise auf dasselbe Objekt können unterschiedliche Werte haben. Die Rückgabewerte von aufeinanderfolgenden Aufrufen an NewGlobalRef für dasselbe Objekt kann unterschiedlich sein. Um zu prüfen, ob zwei Verweise auf dasselbe Objekt verweisen, müssen Sie die Funktion IsSameObject verwenden. Nie vergleichen Referenzen mit == in nativem Code

Eine Konsequenz daraus ist, dass Sie darf nicht davon ausgehen, dass Objektverweise konstant oder eindeutig sind im nativen Code. Der Wert, der ein Objekt darstellt, kann sich von Aufruf zu Aufruf einer Methode 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 müssen „nicht übermäßig viel“ lokale Referenzen. In der Praxis bedeutet das, Wenn Sie eine große Anzahl lokaler Referenzen erstellen, z. B. während Sie eine Reihe von -Objekten enthält, sollten Sie diese manuell mit DeleteLocalRef statt es JNI für Sie erledigen zu lassen. Die Implementierung ist nur erforderlich, um Slots für 16 lokale Verweise zu reservieren. Wenn Sie mehr benötigen, sollten Sie entweder nach und nach löschen oder EnsureLocalCapacity/PushLocalFrame verwenden, um weitere zu reservieren.

Beachten Sie, dass jfieldID- und jmethodID-Werte opak sind. -Typen und keine Objektverweise und sollten nicht an NewGlobalRef Die Rohdaten Cursor, die von Funktionen wie GetStringUTFChars zurückgegeben werden und GetByteArrayElements sind ebenfalls keine Objekte. (Sie werden möglicherweise zwischen Threads 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 Verweise durch den ausgeführten Code nie automatisch freigegeben, bis der Thread getrennt wird. Beliebige lokale Zielgruppe Referenzen, die Sie erstellen, müssen manuell gelöscht werden. Im Allgemeinen zum Erstellen lokaler Verweise in einer Schleife zu löschen.

Seien Sie bei der Verwendung globaler Referenzen vorsichtig. Globale Referenzen sind manchmal unvermeidlich, aber schwer zu beheben und können zu schwer zu diagnostizierenden Arbeitsspeicherproblemen führen. Wenn alle anderen Bedingungen gleich sind, mit weniger globalen Verweisen ist wahrscheinlich besser.

UTF-8- und UTF-16-Strings

Die Programmiersprache Java verwendet UTF-16. Der Einfachheit halber bietet JNI Methoden, die mit UTF-8 geändert. Die modifizierte Codierung ist für C-Code nützlich, da \u0000 als 0xc0 0x80 anstelle von 0x00 codiert wird. Das Schöne daran ist, dass Sie sich darauf verlassen können, dass Sie nullterminierte Strings im C-Format haben, die sich für die Verwendung mit den Standard-libc-Stringfunktionen eignen. Die Kehrseite ist, dass man nicht beliebige UTF-8-Daten an JNI senden und erwarten, dass diese korrekt funktionieren.

Zum Abrufen der UTF-16-Darstellung einer String verwenden Sie GetStringChars. Beachte, dass UTF-16-Strings nicht nullterminiert sind und „\u0000“ zulässig ist, Daher müssen Sie sich sowohl an die Zeichenfolgenlänge als auch an den jchar-Pointer halten.

Vergiss nicht, Release die Zeichenfolgen zu Get. Die Stringfunktionen geben jchar* oder jbyte* zurück, also C-orientierte Zeiger 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.

Die an NewStringUTF übergebenen Daten müssen im Format „Modifiziertes UTF-8“ vorliegen. A Häufiger Fehler ist das Lesen von Zeichendaten aus einer Datei oder einem Netzwerkstream und an NewStringUTF übergeben, ohne sie zu filtern. Sofern Sie nicht wissen, dass die Daten gültiger MUTF-8-Code (oder 7-Bit-ASCII, einer kompatiblen Teilmenge) sind, müssen ungültige Zeichen entfernt oder in das richtige geänderte UTF-8-Format konvertiert werden. 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 empfängt.

Vor Android 8 war es in der Regel schneller, als Android mit UTF-16-Strings zu arbeiten. erforderte keine Kopie in GetStringChars, während Für GetStringUTFChars waren eine Zuordnung und eine Konvertierung in UTF-8 erforderlich. Unter Android 8 wurde die String-Darstellung geändert, sodass jetzt 8 Bit pro Zeichen verwendet werden. für ASCII-Zeichenfolgen (um Arbeitsspeicher zu sparen) und begann, eine Umzug automatische Speicherbereinigung. Durch diese Funktionen sinkt die Zahl der Fälle, in denen ART kann einen Verweis auf die String-Daten bereitstellen, ohne eine Kopie zu erstellen, auch für GetStringCritical. Wenn die meisten vom Code verarbeiteten Strings jedoch kurz sind, kann die Zuweisung und Deaktivierung in den meisten Fällen durch Verwendung eines stackbasierten Buffers und GetStringRegion oder GetStringUTFRegion vermieden 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);

Primitive Arrays

JNI bietet Funktionen für den Zugriff auf die Inhalte von Array-Objekten. Auf Arrays von Objekten muss ein Eintrag nach dem anderen zugegriffen werden. Arrays von Primitive können direkt gelesen und geschrieben werden, als wären sie in C deklariert.

Um die Benutzeroberfläche so effizient wie möglich zu gestalten, ohne die VM-Implementierung einzuschränken, kann die Laufzeit mit der Get<PrimitiveType>ArrayElements-Aufruffamilie entweder einen Verweis auf die tatsächlichen Elemente zurückgeben oder Speicherplatz zuweisen und eine Kopie erstellen. In beiden Fällen hat der Rohzeiger zurückgegeben, ist garantiert bis zum entsprechenden Release-Aufruf gültig. ausgegeben wird (was impliziert, dass das Array-Objekt, falls die Daten nicht kopiert wurden, wird angepinnt und kann bei der Verdichtung des Heaps nicht verschoben werden). Sie müssen jedes Array, das Sie Get, Release. Wenn der Get-Aufruf fehlschlägt, muss Ihr Code später nicht versuchen, einen NULL-Zeiger zu Release.

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

Der Release-Aufruf nimmt ein mode-Argument an, das haben einen von drei Werten. Welche Aktionen von der Laufzeit ausgeführt werden, hängt davon ab, ob ein Zeiger auf die tatsächlichen Daten oder eine Kopie davon zurückgegeben wurde:

  • 0
    • Tatsächlich: Das Array-Objekt ist losgelöst.
    • Kopieren: Die Daten werden zurückkopiert. Der Zwischenspeicher mit der Kopie wird freigegeben.
  • JNI_COMMIT
    • Tatsächlich: nichts.
    • Kopieren: Die Daten werden zurückkopiert. Zwischenspeicher mit der Kopie wird nicht freigegeben.
  • JNI_ABORT
    • Tatsächlich: Das Arrayobjekt ist nicht angepinnt. Früher Schreibvorgänge werden nicht abgebrochen.
    • Copy: Der Zwischenspeicher mit der Kopie wird freigegeben. gehen alle Änderungen verloren.

Ein Grund für die Überprüfung des Flags isCopy besteht darin, zu wissen, ob Du musst Release mit JNI_COMMIT anrufen nachdem Sie Änderungen an einem Array vorgenommen haben – wenn Sie zwischen und Code ausführen, der den Inhalt des Arrays verwendet, können den No-Op-Commit überspringen. Ein weiterer möglicher Grund für die Überprüfung der Kennzeichnung ist effiziente Handhabung von JNI_ABORT. Vielleicht möchten Sie zum Beispiel um ein Array zu erhalten, es an Ort und Stelle zu ändern, Teile an andere Funktionen zu übergeben Verwerfen Sie die Änderungen. Wenn du weißt, dass JNI eine neue Kopie für dich erstellt, musst du keine weitere „bearbeitbare“ Kopie erstellen. Wenn JNI Ihnen das Original übergibt, müssen Sie eine eigene Kopie erstellen.

Es ist ein häufiger Fehler (im Beispielcode wiederholt), anzunehmen, dass Sie den Aufruf von Release überspringen können, wenn *isCopy falsch ist. Das ist nicht der Fall. Wenn kein Kopierpuffer vorhanden war wird der ursprüngliche Arbeitsspeicher angepinnt und kann nicht durch für die automatische Speicherbereinigung.

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

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

Dadurch wird das Array abgerufen und das erste Byte (len) wird kopiert. Elemente und gibt dann das Array frei. Je nach implementiert, wird das Array durch den Get-Aufruf entweder angepinnt oder kopiert. Inhalte. Der Code kopiert die Daten (etwa ein zweites Mal) und ruft dann Release auf. in diesem Fall Mit JNI_ABORT wird verhindert, dass eine dritte Kopie erstellt wird.

Das Gleiche kann auch einfacher erreicht werden:

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

Dies hat mehrere Vorteile:

  • Erfordert einen JNI-Aufruf statt zwei, wodurch der Aufwand reduziert wird.
  • Es sind keine Anpinnen oder zusätzlichen Datenkopien erforderlich.
  • Reduziert das Risiko von Programmierfehlern – kein Risiko, dass sie vergessen werden um Release aufzurufen, wenn ein Fehler auftritt.

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, während eine Ausnahme aussteht. Von Ihrem Code wird erwartet, dass er die Ausnahme bemerkt (über den Rückgabewert der Funktion, ExceptionCheck oder ExceptionOccurred) und zurück, oder die Ausnahme löschen und verarbeiten.

Die einzigen JNI-Funktionen, die Sie während einer Ausnahme aufrufen dürfen, ausstehend:

  • 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 z. B. NewString den Wert Nicht-NULL-Wert ist, müssen Sie nicht nach einer Ausnahme suchen. Wenn jedoch Sie eine Methode aufrufen (mit einer Funktion wie CallObjectMethod), müssen Sie immer nach einer Ausnahme suchen, da der Rückgabewert ist gültig, wenn eine Ausnahme ausgelöst wird.

Beachten Sie, dass Ausnahmen, die von verwaltetem Code ausgegeben werden, den nativen Stack nicht rückgängig machen. Frames. (Und C++-Ausnahmen, von denen unter Android in der Regel abgeraten wird, dürfen nicht über die JNI-Übergangsgrenze vom C++ Code zum verwalteten Code geworfen werden.) Die JNI-Anweisungen Throw und ThrowNew setzen lediglich einen Ausnahmezeiger im aktuellen Thread. Nach Rückkehr zum verwalteten Modus aus nativem Code entfernt wird, wird die Ausnahme vermerkt und entsprechend gehandhabt.

Nativer Code kann „einfangen“, Ausnahmeregelung durch Aufrufen von ExceptionCheck oder ExceptionOccurred und löschen Sie sie mit ExceptionClear Wie immer wenn Ausnahmen verworfen werden, ohne sie zu verarbeiten, kann dies zu Problemen führen.

Es gibt keine integrierten Funktionen zum Bearbeiten des Throwable-Objekts sich selbst. Wenn Sie also (z. B.) die Ausnahmezeichenfolge abrufen möchten, müssen Sie die Throwable-Klasse finden, die Methoden-ID für getMessage "()Ljava/lang/String;", ruft es auf und wenn das Ergebnis nicht NULL ist, verwenden Sie GetStringUTFChars, um etwas zu erhalten, an printf(3) oder eine gleichwertige Stelle senden.

Erweiterte Prü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, bei dem die JavaVM- und JNIEnv-Funktionstabellen-Pointer zu Tabellen von Funktionen wechseln, die eine erweiterte Reihe von Prüfungen durchführen, bevor die Standardimplementierung aufgerufen wird.

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

  • Arrays: Versuch, ein Array mit negativer Größe zuzuweisen
  • Ungültige Zeiger: Übergabe eines fehlerhaften jarray/jclass/jobject/jstring an einen JNI-Aufruf oder Übergabe eines NULL-Zeigers an einen JNI-Aufruf mit einem Argument, das keine Nullwerte zulässt.
  • Klassennamen: Beliebige Werte außer dem Stil „java/lang/String“ des Klassennamens an einen JNI-Aufruf übergeben.
  • Kritische Aufrufe: Ein JNI-Aufruf zwischen einem „kritischen“ Get und der entsprechenden Freigabe.
  • Direkte ByteBuffers: Es werden fehlerhafte Argumente an NewDirectByteBuffer übergeben.
  • Ausnahmen: Ausführen eines JNI-Aufrufs bei ausstehender Ausnahme.
  • JNIEnv*s: JNIEnv* aus dem falschen Thread verwenden.
  • jfieldIDs: Verwendung einer NULL-jfieldID oder Verwendung einer jfieldID, um ein Feld auf einen Wert des falschen Typs festzulegen (z. B. Versuch, einem String-Feld einen StringBuilder zuzuweisen), Verwendung einer jfieldID für ein statisches Feld, um ein Instanzfeld festzulegen, oder umgekehrt, Verwendung einer jfieldID aus einer Klasse mit Instanzen einer anderen Klasse.
  • jmethodIDs: Verwendung der falschen Art von jmethodID bei einem Call*Method-JNI-Aufruf: falscher Rückgabetyp, nicht statisch/nicht statischer Abgleich, falscher Typ für „this“ (bei nicht statischen Aufrufen) oder falsche Klasse (bei statischen Aufrufen).
  • 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 (z. B. Rückgabe eines StringBuilders aus einer deklarierten Methode, um einen String zurückzugeben).
  • UTF-8: Eine ungültige Geänderte UTF-8-Byte-Sequenz wird an einen JNI-Aufruf übergeben.

Die Zugänglichkeit von Methoden und Feldern wird weiterhin 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 mit aktiviertem CheckJNI neu zu 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 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, aber für alle Apps, die ab diesem Zeitpunkt gestartet werden, ist CheckJNI aktiviert. (Ändern Sie die Eigenschaft in einen anderen Wert oder starten Sie einfach einen Neustart, um CheckJNI wieder zu deaktivieren.) In diesem Fall sehen Sie beim nächsten Start einer Anwendung in Ihrer Logcat-Ausgabe etwa Folgendes:

D Late-enabling CheckJNI

Sie können auch das Attribut android:debuggable im Manifest Ihrer App auf und aktiviere CheckJNI nur für deine App. Die Android-Build-Tools erledigen dies automatisch für bestimmte Build-Typen verwenden.

Native Bibliotheken

Sie können nativen Code aus gemeinsam genutzten Bibliotheken mit dem Standardcode System.loadLibrary

In der Praxis traten bei älteren Android-Versionen Fehler im PackageManager auf, die die Installation und native Bibliotheken unzuverlässig zu aktualisieren. Das Projekt ReLinker bietet Lösungen für dieses und andere Probleme beim Laden nativer Bibliotheken.

System.loadLibrary (oder ReLinker.loadLibrary) aus einer statischen Klasse aufrufen Initialisieren. Das Argument ist die Bibliotheksname, Zum Laden von libfubar.so würden Sie also "fubar" übergeben.

Wenn Sie nur eine Klasse mit nativen Methoden haben, ist es sinnvoll, für den Aufruf der System.loadLibrary in einem statischen Initialisierer für diese Klasse sein. Andernfalls können Sie den Aufruf von Application aus starten, damit Sie sicher sind, dass die Bibliothek immer und frühzeitig 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 Laufzeitumgebung kann sie dynamisch mit dlsym abrufen. Die Vorteile von RegisterNatives sind, dass Sie sofort loslegen können. Überprüfen Sie, ob die Symbole vorhanden sind. Außerdem können Sie kleinere und schnellere gemeinsam genutzte Bibliotheken nutzen, indem Sie Export von etwas außer JNI_OnLoad. 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.
  • Build mit einem Versionsskript (bevorzugt) oder mithilfe von -fvisibility=hidden, damit nur JNI_OnLoad wird aus Ihrer Bibliothek exportiert. Dadurch wird schneller und kleinerer Code generiert und potenzielle Kollisionen mit anderen Bibliotheken, die in Ihre App geladen werden, werden vermieden. Allerdings werden weniger nützliche Stack-Traces erstellt, wenn Ihre App im nativen 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 etwa so aussehen, geschrieben in C++:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

Um stattdessen „Discovery“ zu verwenden nativer Methoden verwenden, müssen Sie ihnen eine bestimmte Bezeichnung geben (siehe JNI-Spezifikation . Wenn also eine Methodensignatur falsch ist, werden Sie erst beim ersten Aufruf der Methode darüber informiert.

Alle FindClass-Aufrufe, die von JNI_OnLoad ausgeführt werden, lösen Klassen im Kontext des Klasseladers auf, der zum Laden der freigegebenen Bibliothek verwendet wurde. Bei Anruf von einem anderen Anbieter Kontexte verwendet, verwendet FindClass das Klassenladeprogramm, das mit der Methode oben im Java-Stack oder wenn es keinen gibt (der Aufruf stammt aus einem nativen Thread, der gerade angehängt wurde) wird das „System“- Klassenladeprogramm. Der System-Klassenloader kennt die Klassen Ihrer Anwendung nicht. Daher können Sie Ihre eigenen Klassen in diesem Kontext nicht mit FindClass aufrufen. Dadurch ist JNI_OnLoad ein praktischer Ort, um Klassen zu suchen und im Cache zu speichern: Sie haben eine gültige globale Referenz für jclass können Sie sie in jedem angehängten Thread verwenden.

Schnellere native Anrufe mit @FastNative und @CriticalNative

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

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

Beim Ausführen einer @FastNative- oder @CriticalNative-Methode Sammlung kann den Thread nicht für wichtige Aufgaben sperren und wird möglicherweise blockiert. Verwenden Sie diese Anmerkungen nicht für langlaufende Methoden, einschließlich normalerweise schneller, aber allgemein unbegrenzter Methoden. Insbesondere sollten keine erheblichen E/A-Vorgänge ausgeführt und keine nativen Sperren erworben werden, die für lange Zeit gehalten werden können.

Diese Anmerkungen wurden für das System implementiert, seit Android 8 und wurden von CTS getestet, API unter Android 14. Wahrscheinlich funktionieren diese Optimierungen auch auf Geräten mit Android 8 bis 13, ohne die starken CTS-Garantien), aber die dynamische Suche nach nativen Methoden wird nur auf Android 12 oder höher, die ausdrückliche Registrierung bei JNI RegisterNatives ist zwingend erforderlich für die Android-Version 8 bis 11. 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, für die diese Anmerkungen erforderlich sind, wird dringend empfohlen, die Methoden explizit bei JNI RegisterNatives zu registrieren, anstatt sich auf die namenbasierte „Erkennung“ nativer Methoden zu verlassen. Für eine optimale Leistung beim Starten der App wird empfohlen, 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 kostengünstig wie ein nicht-Inline-Aufruf in C/C++, solange alle Argumente in Register passen (z. B. bis zu 8 ganze und bis zu 8 Gleitkommaargumente auf arm64).

Manchmal ist es besser, eine native Methode in zwei Teile aufzuteilen. Dies ist eine sehr schnelle Methode, und eine weitere, die langsame Fälle abwickelt. 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-Hinweise

Wenn Sie Architekturen unterstützen möchten, die 64‑Bit-Pointer verwenden, verwenden Sie zum Speichern eines Pointers auf eine native Struktur in einem Java-Feld ein long-Feld anstelle eines int-Felds.

Nicht unterstützte Funktionen/Abwärtskompatibilität

Bis auf die folgende Ausnahme werden alle Funktionen von JNI 1.6 unterstützt:

  • DefineClass ist nicht implementiert. Android verwendet nicht Java-Bytecodes oder Klassendateien, sodass binäre Klassendaten übergeben werden. nicht funktioniert.

Für die Abwärtskompatibilität mit älteren Android-Releases musst du möglicherweise beachten Sie Folgendes:

  • Dynamische Suche nach nativen Funktionen

    Bis Android 2.0 (Eclair) wird das "$"-Zeichen Zeichen war nicht richtig konvertiert in "_00024" bei der Suche nach Methodennamen. Wird ausgeführt eine explizite Registrierung oder das Verschieben des aus inneren Klassen herauszufiltern.

  • Threads werden getrennt

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

  • Schwache globale Referenzen

    Bis Android 2.2 (Froyo) wurden schwache globale Referenzen nicht implementiert. Versuche, sie zu verwenden, werden von älteren Versionen strikt abgelehnt. Sie können die Konstanten für die Android-Plattformversion verwenden, um die Unterstützung zu testen.

    Bis Android 4.0 (Ice Cream Sandwich) konnten schwache globale Referenzen an NewLocalRef, NewGlobalRef und DeleteWeakGlobalRef (In der Spezifikation wird ausdrücklich empfohlen, harte Verweise auf schwache globale Daten zu erstellen, nichts mit ihnen zu tun, also sollte dies überhaupt nicht einschränken.)

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

  • Lokale Verweise

    Bis Android 4.0 (Ice Cream Sandwich) waren lokale Verweise tatsächlich direkte Verweise. Ice Cream Sandwich hat die Indirektion hinzugefügt. um bessere automatische Speicherbereinigungen zu unterstützen. der JNI-Fehler sind in älteren Releases nicht erkennbar. Weitere Informationen finden Sie unter Änderungen an lokalen JNI-Referenzen in ICS.

    In früheren Android-Versionen vor Android 8.0 Die Anzahl der lokalen Verweise ist auf ein versionsspezifisches Limit begrenzt. Ab Android 8.0 werden unbegrenzte lokale Verweise unterstützt.

  • Referenztyp mit GetObjectRefType ermitteln

    Bis Android 4.0 (Ice Cream Sandwich), da die Verwendung von direkte Verweise (siehe oben) enthält, war es nicht möglich, GetObjectRefType richtig. Stattdessen haben wir eine Heuristik die Tabelle der schwachen globalen Konzerne, die Argumente, die lokalen und der globalen Tabelle in dieser Reihenfolge. Wenn der direkte Verweis zum ersten Mal gefunden wird, wird gemeldet, dass die Referenz den Typ hat, der gerade geprüft wird. Das bedeutete zum Beispiel, Sie haben GetObjectRefType in einem globalen jclass aufgerufen, muss mit der jclass identisch sein, die als implizites Argument an Ihre statische erhalten Sie JNILocalRefType statt JNIGlobalRefType

  • @FastNative und @CriticalNative

    Bis Android 7 wurden diese Optimierungsanmerkungen ignoriert. Das ABI Eine Abweichung für @CriticalNative würde zu einem falschen Argument führen. und es kommt wahrscheinlich zu Abstürzen.

    Dynamische Suche nach nativen Funktionen für @FastNative und @CriticalNative-Methoden wurden in Android 8-10 nicht implementiert und enthält bekannte Fehler in Android 11. Wenn Sie diese Optimierungen ohne explizite Registrierung bei JNI RegisterNatives wahrscheinlich zu Abstürzen unter Android 8 bis 11 führen.

  • FindClass wirft ClassNotFoundException

    Aus Gründen der Abwärtskompatibilität wirft Android ClassNotFoundException statt NoClassDefFoundError, wenn ein Kurs nicht über FindClass Dieses Verhalten entspricht der Java Reflexion API. Class.forName(name)

FAQ: Why do I get UnsatisfiedLinkError?

Bei der Arbeit an nativem Code treten häufig folgende Fehler auf:

java.lang.UnsatisfiedLinkError: Library foo not found

Manchmal bedeutet das, dass die Bibliothek nicht gefunden wurde. In 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 Detailnachricht zur Ausnahme.

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

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

Eine weitere Art 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 übereinstimmende Methode zu finden, dies aber nicht geschafft hat. Hier einige häufige Gründe dafür:

  • Die Bibliothek wird nicht geladen. Prüfen Sie die Logcat-Ausgabe auf zum Laden der Mediathek.
  • Die Methode wird aufgrund einer Namens- oder Signaturabweichung nicht gefunden. Dieses wird häufig durch folgende Ursachen verursacht:
    • Bei verzögerter Methodensuche werden keine C++-Funktionen deklariert mit extern "C" und angemessen Sichtbarkeit (JNIEXPORT) Hinweis: Vor Ice Cream Sandwich enthält, war das JNIEXPORT-Makro falsch. Die Verwendung eines neuen GCC mit Ein altes jni.h funktioniert nicht. Mit arm-eabi-nm können Sie die Symbole so sehen, wie sie in der Bibliothek angezeigt werden. Wenn sie beschädigt aussehen (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: Kleinere Fehler beim Eingeben der Methodensignatur. Achten Sie darauf, dass die Daten, die Sie an die Registrierungsaufruf mit der Signatur in der Protokolldatei übereinstimmt. Denken Sie daran, dass „B“ ist byte und „Z“ ist boolean. Komponenten von Klassennamen in Signaturen beginnen mit „L“ und enden mit „;“, Verwenden Sie '/' zum Trennen von Paket-/Klassennamen und verwenden Sie '$' zum Trennen einer Namen der inneren Klassen (z. B. Ljava/util/Map$Entry;).

Die Verwendung von javah zur automatischen Generierung von JNI-Headern kann hilfreich sein Probleme zu vermeiden.

FAQ: Warum hat FindClass meinen Kurs nicht gefunden?

(Die meisten dieser Tipps gelten auch für Fehler beim Finden von Methoden. mit GetMethodID oder GetStaticMethodID oder Feldern mit GetFieldID oder GetStaticFieldID.)

Achten Sie darauf, dass der String für den Klassennamen das richtige Format hat. JNI-Klasse beginnen mit dem Paketnamen und werden durch Schrägstriche getrennt. wie java/lang/String. Wenn Sie nach einer Array-Klasse suchen, beginnen Sie mit der entsprechenden Anzahl eckiger Klammern muss die Klasse auch mit „L“ umschließen. und ';', also ein eindimensionales Array von String wäre [Ljava/lang/String;. Wenn du nach einer inneren Klasse suchst, verwende "$". statt ".". Im Allgemeinen javap in der .class-Datei ist eine gute Möglichkeit, internen Namen Ihres Kurses an.

Wenn Sie die Codekomprimierung aktivieren, konfigurieren, welcher Code beibehalten werden soll. Wird konfiguriert ist wichtig, weil der Code-Crumker sonst Klassen, Methoden, oder Felder, die nur von JNI verwendet werden.

Wenn der Klassenname stimmt, wird möglicherweise ein Klassenladeprogramm angezeigt. Problem. FindClass möchte die Klassensuche im mit Ihrem Code verknüpften Class Loader starten. Es prüft den Aufrufstack, Das sieht ungefähr so aus:

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

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

Das tut in der Regel, was Sie möchten. Sie können in Schwierigkeiten geraten, wenn Sie Thread selbst erstellen (z. B. durch Aufrufen von pthread_create) und es dann mit AttachCurrentThread anhängen). Jetzt sind keine Stack-Frames aus Ihrer Anwendung. Wenn Sie FindClass aus diesem Thread aufrufen, wird die JavaVM im „System“-Klassenloader gestartet, nicht im mit Ihrer Anwendung verknüpften Klassenloader. Versuche, anwendungsspezifische Klassen zu finden, schlagen fehl.

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

  • Führen Sie eine FindClass-Suche einmal durch, in JNI_OnLoad und speichern Sie die Klassenreferenzen zur späteren Verwendung im Cache. verwenden. Alle FindClass-Aufrufe, die im Rahmen der Ausführung erfolgen JNI_OnLoad verwendet das Klassenladeprogramm, das verknüpft ist mit die Funktion System.loadLibrary (dies ist ein Sonderregel, die dazu dient, die Initialisierung der Bibliothek zu vereinfachen. Wenn die Bibliothek über deinen App-Code geladen wird, FindClass verwendet das richtige Klassenladeprogramm.
  • Übergeben Sie eine Instanz der Klasse an die Funktionen, die die indem Sie Ihre native Methode so deklarieren, dass ein Klassenargument verwendet wird und und dann Foo.class übergeben.
  • Speichern Sie einen Verweis auf das ClassLoader-Objekt im Cache. und direkt loadClass-Anrufe senden. Dies erfordert etwas Anstrengung.

FAQ: Wie kann ich Rohdaten mit nativem Code teilen?

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 die Bearbeitung von Bitmaps oder Tonbeispielen beinhalten. Es gibt zwei grundlegende Ansätze.

Sie können die Daten in einer byte[] speichern. So können sehr schnelle Zugriff über verwalteten Code. Auf der nativen Seite können Sie jedoch nicht garantiert auf die Daten zugreifen, ohne sie kopieren zu müssen. In einige Implementierungen, GetByteArrayElements und GetPrimitiveArrayCritical gibt tatsächliche Zeiger auf die Rohdaten werden im verwalteten Heap gespeichert, in anderen wird aber ein Zwischenspeicher zugewiesen. auf dem nativen Heap und kopieren die Daten hinüber.

Die Alternative besteht darin, die Daten in einem direkten Byte-Zwischenspeicher zu speichern. Diese können mit java.nio.ByteBuffer.allocateDirect erstellt werden oder die JNI-Funktion NewDirectByteBuffer. Anders als normales Bytepuffer, wird der Speicher nicht auf dem verwalteten Heap zugewiesen und kann kann immer direkt aus dem nativen Code heraus aufgerufen werden (die Adresse mit GetDirectBufferAddress). Je nachdem, wie direkt Es ist ein Byte-Pufferzugriff implementiert, wodurch der Zugriff auf die Daten aus dem verwalteten Code erfolgt. sehr langsam sein.

Welche Sie verwenden, hängt von zwei Faktoren ab:

  1. Erfolgen die meisten Datenzugriffe über Code, der in Java geschrieben ist? oder in C/C++?
  2. In welcher Form werden die Daten schließlich an eine System-API übergeben? muss es drin sein? Wenn die Daten z. B. irgendwann an einen -Funktion, die ein byte[] verwendet und die Verarbeitung in einem direkten ByteBuffer wäre vielleicht unklug.)

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