Wskazówki dotyczące JNI

JNI to Java Native Interface. Określa sposób, w jaki kod bajtowy, który Android kompiluje z kodu zarządzanego (napisanego w językach programowania Java lub Kotlin), wchodzi w interakcję z kodem natywnym (napisanym w językach C/C++). JNI jest niezależny od dostawcy, obsługuje wczytywanie kodu z dynamicznych bibliotek współdzielonych i chociaż czasami jest uciążliwy, to dość wydajny.

Uwaga: ponieważ Android kompiluje kod w języku Kotlin do kodu bajtowego zgodnego z ART w podobny sposób jak w przypadku języka Java, możesz stosować wskazówki podane na tej stronie zarówno w przypadku języka Kotlin, jak i Java w zakresie architektury JNI i związanych z nią kosztów. Więcej informacji znajdziesz w artykule Kotlin i Android.

Jeśli nie znasz tego interfejsu, przeczytaj specyfikację Java Native Interface, aby dowiedzieć się, jak działa JNI i jakie funkcje są dostępne. Niektóre aspekty interfejsu nie są od razu oczywiste, dlatego kolejne sekcje mogą być przydatne.

Aby przeglądać globalne odwołania JNI i sprawdzać, gdzie są tworzone i usuwane, użyj widoku sterty JNIprofilerze pamięci w Android Studio 3.2 lub nowszym.

Wskazówki ogólne

Staraj się minimalizować ślad warstwy JNI. Warto tu wziąć pod uwagę kilka aspektów. Rozwiązanie JNI powinno być zgodne z tymi wytycznymi (wymienionymi poniżej w kolejności ważności, od najważniejszych):

  • Zminimalizuj przekazywanie zasobów przez warstwę JNI. Przekazywanie danych przez warstwę JNI wiąże się z niemałymi kosztami. Staraj się projektować interfejs, który minimalizuje ilość danych, które musisz serializować, oraz częstotliwość, z jaką musisz to robić.
  • W miarę możliwości unikaj komunikacji asynchronicznej między kodem napisanym w zarządzanym języku programowania a kodem napisanym w C++. Ułatwi to utrzymanie interfejsu JNI. Asynchroniczne aktualizacje interfejsu użytkownika można zwykle uprościć, zachowując ten sam język aktualizacji asynchronicznej i interfejsu. Na przykład zamiast wywoływać funkcję C++ z wątku interfejsu w kodzie Java za pomocą JNI, lepiej jest wykonać wywołanie zwrotne między dwoma wątkami w języku Java, z których jeden wykonuje blokujące wywołanie C++, a następnie powiadamia wątek interfejsu o zakończeniu tego wywołania.
  • Zminimalizuj liczbę wątków, które muszą wchodzić w interakcję z JNI lub być przez nie obsługiwane. Jeśli musisz używać puli wątków w językach Java i C++, staraj się utrzymywać komunikację JNI między właścicielami puli, a nie między poszczególnymi wątkami roboczymi.
  • Kod interfejsu powinien znajdować się w niewielkiej liczbie łatwych do zidentyfikowania lokalizacji w kodzie źródłowym C++ i Java, aby ułatwić przyszłe refaktoryzacje. W razie potrzeby rozważ użycie biblioteki automatycznego generowania JNI.

JavaVM i JNIEnv

JNI definiuje 2 kluczowe struktury danych: „JavaVM” i „JNIEnv”. Oba te elementy są wskaźnikami wskaźników do tabel funkcji. (W wersji C++ są to klasy ze wskaźnikiem do tabeli funkcji i funkcją składową dla każdej funkcji JNI, która pośredniczy w tabeli). JavaVM udostępnia funkcje „interfejsu wywoływania”, które umożliwiają tworzenie i usuwanie maszyny JavaVM. Teoretycznie w ramach jednego procesu można mieć wiele maszyn JavaVM, ale Android zezwala tylko na jedną.

Interfejs JNIEnv udostępnia większość funkcji JNI. Wszystkie funkcje natywne otrzymują JNIEnv jako pierwszy argument, z wyjątkiem metod @CriticalNative. Więcej informacji znajdziesz w artykule Szybsze wywołania natywne.

JNIEnv jest używany do przechowywania danych lokalnych wątku. Z tego powodu nie można udostępniać interfejsu JNIEnv między wątkami. Jeśli fragment kodu nie ma innego sposobu na uzyskanie JNIEnv, udostępnij JavaVM i użyj GetEnv, aby wykryć JNIEnv wątku. (Zakładając, że ma on taką funkcję; patrz AttachCurrentThread poniżej).

Deklaracje JNIEnv i JavaVM w języku C różnią się od deklaracji w C++. Plik "jni.h" include zawiera różne definicje typów w zależności od tego, czy jest dołączony do kodu w języku C czy C++. Z tego powodu nie należy umieszczać argumentów JNIEnv w plikach nagłówkowych dołączanych przez oba języki. (Inaczej mówiąc: jeśli plik nagłówkowy wymaga #ifdef __cplusplus, może być konieczne wykonanie dodatkowych czynności, jeśli cokolwiek w tym pliku nagłówkowym odwołuje się do JNIEnv).

Wątki

Wszystkie wątki to wątki Linuksa, które są planowane przez jądro. Zwykle są one uruchamiane z kodu zarządzanego (za pomocą Thread.start()), ale można je też tworzyć w innych miejscach, a następnie dołączać do JavaVM. Na przykład wątek rozpoczęty od pthread_create() lub std::thread można dołączyć za pomocą funkcji AttachCurrentThread() lub AttachCurrentThreadAsDaemon(). Dopóki wątek nie zostanie dołączony, nie ma JNIEnv i nie może wykonywać wywołań JNI.

Zwykle najlepiej jest używać Thread.start() do tworzenia wątków, które muszą wywoływać kod w języku Java. Dzięki temu będziesz mieć wystarczającą ilość miejsca na stosie, znajdziesz się w odpowiednim ThreadGroup i będziesz używać tego samego ClassLoader co w kodzie Java. W przypadku debugowania w języku Java łatwiej jest też ustawić nazwę wątku niż w przypadku kodu natywnego (jeśli masz pthread_t lub thread_t, zobacz pthread_setname_np(), a jeśli masz std::thread i chcesz pthread_t, zobacz std::thread::native_handle()).

Dołączenie wątku utworzonego natywnie powoduje utworzenie obiektu i dodanie go do wątku „main”, dzięki czemu jest on widoczny dla debugera.java.lang.ThreadThreadGroup Wywołanie AttachCurrentThread() w już dołączonym wątku nie powoduje żadnych działań.

Android nie wstrzymuje wątków wykonujących kod natywny. Jeśli trwa odśmiecanie lub debuger wysłał żądanie zawieszenia, Android wstrzyma wątek przy następnym wywołaniu JNI.

Wątki dołączone za pomocą JNI muszą przed zakończeniem działania wywołać funkcję DetachCurrentThread(). Jeśli bezpośrednie kodowanie jest niewygodne, w Androidzie 2.0 (Eclair) i nowszych możesz użyć pthread_key_create(), aby zdefiniować funkcję destruktora, która będzie wywoływana przed zakończeniem wątku, i wywołać z niej DetachCurrentThread(). (Użyj tego klucza z pthread_setspecific(), aby zapisać JNIEnv w pamięci lokalnej wątku. W ten sposób zostanie on przekazany do destruktora jako argument).

jclass, jmethodID i jfieldID

Jeśli chcesz uzyskać dostęp do pola obiektu z kodu natywnego, wykonaj te czynności:

  • Pobierz odwołanie do obiektu klasy dla klasy z FindClass.
  • Uzyskiwanie identyfikatora pola z wartością GetFieldID
  • Pobierz zawartość pola za pomocą odpowiedniego kodu, np.GetIntField

Podobnie, aby wywołać metodę, najpierw uzyskaj odniesienie do obiektu klasy, a potem identyfikator metody. Identyfikatory są często tylko wskaźnikami wewnętrznych struktur danych środowiska wykonawczego. Wyszukiwanie może wymagać kilku porównań ciągów znaków, ale gdy już je znajdziesz, wywołanie pola lub metody jest bardzo szybkie.

Jeśli wydajność jest ważna, warto wyszukać wartości raz i zapisać wyniki w pamięci podręcznej w kodzie natywnym. Ponieważ na proces przypada tylko jedna maszyna JavaVM, warto przechowywać te dane w statycznej strukturze lokalnej.

Odwołania do klas, identyfikatory pól i identyfikatory metod są prawidłowe do momentu zwolnienia klasy. Klasy są zwalniane tylko wtedy, gdy wszystkie klasy powiązane z elementem ClassLoader mogą zostać usunięte przez odśmiecanie pamięci. Jest to rzadkie, ale nie niemożliwe w Androidzie. Pamiętaj jednak, że jclass jest odwołaniem do klasy i musi być chronione wywołaniem funkcji NewGlobalRef (patrz następna sekcja).

Jeśli chcesz zapisywać identyfikatory w pamięci podręcznej podczas wczytywania klasy i automatycznie zapisywać je ponownie, gdy klasa zostanie zwolniona i ponownie wczytana, prawidłowym sposobem inicjowania identyfikatorów jest dodanie do odpowiedniej klasy fragmentu kodu podobnego do tego:

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

Utwórz w kodzie C/C++ metodę nativeClassInit, która będzie wyszukiwać identyfikatory. Kod zostanie wykonany raz, podczas inicjowania klasy. Jeśli klasa zostanie kiedykolwiek zwolniona, a następnie ponownie załadowana, zostanie ponownie wykonana.

Odwołania lokalne i globalne

Każdy argument przekazywany do metody natywnej i niemal każdy obiekt zwracany przez funkcję JNI jest „lokalnym odwołaniem”. Oznacza to, że jest on ważny przez czas trwania bieżącej metody natywnej w bieżącym wątku. Nawet jeśli obiekt nadal istnieje po zwróceniu wartości przez metodę natywną, odwołanie jest nieprawidłowe.

Dotyczy to wszystkich podklas jobject, w tym jclass, jstringjarray. (Środowisko wykonawcze ostrzeże Cię o większości przypadków nieprawidłowego użycia odwołań, gdy włączone są rozszerzone kontrole JNI).

Jedynym sposobem na uzyskanie odwołań do innych arkuszy jest użycie funkcji NewGlobalRefNewWeakGlobalRef.

Jeśli chcesz zachować odwołanie na dłużej, musisz użyć odwołania „globalnego”. Funkcja NewGlobalRef przyjmuje lokalne odwołanie jako argument i zwraca globalne. Gwarantujemy, że globalny numer referencyjny będzie ważny do momentu wywołania funkcji DeleteGlobalRef.

Ten wzorzec jest często używany podczas buforowania obiektu jclass zwróconego przez FindClass, np.:

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

Wszystkie metody JNI akceptują jako argumenty zarówno odwołania lokalne, jak i globalne. Odwołania do tego samego obiektu mogą mieć różne wartości. Na przykład wartości zwracane przez kolejne wywołania funkcji NewGlobalRef na tym samym obiekcie mogą się różnić. Aby sprawdzić, czy 2 odwołania odnoszą się do tego samego obiektu, musisz użyć funkcji IsSameObject. Nigdy nie porównuj odwołań z == w kodzie natywnym.

Jedną z konsekwencji tego jest to, że w kodzie natywnym nie można zakładać, że odwołania do obiektów są stałe lub unikalne. Wartość reprezentująca obiekt może się różnić w zależności od wywołania metody, a dwa różne obiekty mogą mieć tę samą wartość w kolejnych wywołaniach. Nie używaj wartości jobject jako kluczy.

Programiści muszą „nie przydzielać nadmiernie” odwołań lokalnych. W praktyce oznacza to, że jeśli tworzysz dużą liczbę lokalnych odwołań, np. podczas przetwarzania tablicy obiektów, musisz je zwalniać ręcznie za pomocą funkcji DeleteLocalRef, zamiast pozwalać na to JNI. Implementacja musi tylko zarezerwować miejsca na 16 lokalnych odwołań, więc jeśli potrzebujesz więcej, usuwaj je na bieżąco lub użyj EnsureLocalCapacity/PushLocalFrame, aby zarezerwować więcej.

Pamiętaj, że jfieldIDjmethodID to typy nieprzezroczyste, a nie odwołania do obiektów, i nie należy ich przekazywać do NewGlobalRef. Wskaźniki surowych danych zwracane przez funkcje takie jak GetStringUTFCharsGetByteArrayElements również nie są obiektami. (Mogą być przekazywane między wątkami i są ważne do momentu wywołania pasującej funkcji Release).

Jeden nietypowy przypadek zasługuje na osobną wzmiankę. Jeśli dołączysz natywny wątek z AttachCurrentThread, uruchomiony kod nigdy nie zwolni automatycznie lokalnych odwołań, dopóki wątek nie zostanie odłączony. Wszystkie utworzone przez Ciebie lokalne odwołania musisz usunąć ręcznie. Ogólnie rzecz biorąc, każdy kod natywny, który tworzy lokalne odwołania w pętli, prawdopodobnie wymaga ręcznego usuwania.

Zachowaj ostrożność podczas korzystania z odwołań globalnych. Odwołań globalnych nie da się uniknąć, ale trudno je debugować i mogą one powodować trudne do zdiagnozowania problemy z pamięcią. Przy założeniu, że wszystkie inne czynniki są równe, lepsze jest rozwiązanie z mniejszą liczbą odwołań globalnych.

Ciągi znaków UTF-8 i UTF-16

Język programowania Java używa kodowania UTF-16. Dla wygody JNI udostępnia też metody, które działają z kodowaniem zmodyfikowanym UTF-8. Zmodyfikowane kodowanie jest przydatne w przypadku kodu C, ponieważ koduje znak \u0000 jako 0xc0 0x80 zamiast 0x00. Zaletą tego rozwiązania jest to, że możesz liczyć na ciągi znaków zakończone zerem w stylu C, które nadają się do użycia ze standardowymi funkcjami ciągów znaków biblioteki libc. Minusem jest to, że nie możesz przekazywać do JNI dowolnych danych w formacie UTF-8 i oczekiwać, że będą działać prawidłowo.

Aby uzyskać reprezentację UTF-16 znaku String, użyj GetStringChars. Pamiętaj, że ciągi znaków UTF-16 nie są zakończone zerem, a znak \u0000 jest dozwolony, więc musisz zachować długość ciągu znaków oraz wskaźnik jchar.

Nie zapomnij Release ciągów znaków, które Get. Funkcje ciągów znaków zwracają jchar* lub jbyte*, które są wskaźnikami w stylu C do danych pierwotnych, a nie odwołaniami lokalnymi. Są one ważne do momentu wywołania funkcji Release, co oznacza, że nie są zwalniane, gdy metoda natywna zwraca wartość.

Dane przekazywane do funkcji NewStringUTF muszą być w formacie zmodyfikowanym UTF-8. Częstym błędem jest odczytywanie danych znakowych z pliku lub strumienia sieciowego i przekazywanie ich do funkcji NewStringUTF bez filtrowania. Jeśli nie masz pewności, że dane są prawidłowe w formacie MUTF-8 (lub 7-bitowym ASCII, który jest zgodnym podzbiorem), musisz usunąć nieprawidłowe znaki lub przekonwertować je na prawidłową postać zmodyfikowanego formatu UTF-8. W przeciwnym razie konwersja na UTF-16 może dać nieoczekiwane wyniki. CheckJNI, które jest domyślnie włączone w przypadku emulatorów, skanuje ciągi znaków i przerywa działanie maszyny wirtualnej, jeśli otrzyma nieprawidłowe dane wejściowe.

Przed Androidem 8 operowanie na ciągach UTF-16 było zwykle szybsze, ponieważ Android nie wymagał kopii w GetStringChars, podczas gdy GetStringUTFChars wymagał przydzielenia pamięci i konwersji na UTF-8. W Androidzie 8 zmieniono reprezentację String, aby w przypadku ciągów ASCII używać 8 bitów na znak (w celu oszczędzania pamięci), i wprowadzono przenośny moduł odśmiecania pamięci. Te funkcje znacznie zmniejszają liczbę przypadków, w których ART może przekazać wskaźnik do danych String bez tworzenia kopii, nawet w przypadku GetStringCritical. Jeśli jednak większość ciągów przetwarzanych przez kod jest krótka, w większości przypadków można uniknąć przydzielania i zwalniania pamięci, używając bufora przydzielonego na stosie i funkcji GetStringRegion lub GetStringUTFRegion. Na przykład:

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

Tablice typów prostych

JNI udostępnia funkcje umożliwiające dostęp do zawartości obiektów tablicy. Do tablic obiektów trzeba uzyskiwać dostęp po jednym wpisie, natomiast tablice typów prostych można odczytywać i zapisywać bezpośrednio, tak jakby były zadeklarowane w języku C.

Aby interfejs był jak najbardziej wydajny bez ograniczania implementacji maszyny wirtualnej, Get<PrimitiveType>ArrayElements rodzina wywołań umożliwia środowisku wykonawczemu zwrócenie wskaźnika do rzeczywistych elementów lub przydzielenie pamięci i utworzenie kopii. W każdym przypadku zwrócony surowy wskaźnik jest prawidłowy do momentu wydania odpowiedniego wywołania Release (co oznacza, że jeśli dane nie zostały skopiowane, obiekt tablicy zostanie przypięty i nie będzie można go przenieść w ramach kompresowania sterty). Musisz Release każdą tablicę, którą Get. Jeśli wywołanie się nie powiedzie, musisz też zadbać o to, aby Twój kod nie próbował później Release wskaźnika NULL.Get

Możesz sprawdzić, czy dane zostały skopiowane, przekazując wskaźnik niezerowy dla argumentu isCopy. Rzadko się to przydaje.

Wywołanie Release przyjmuje argument mode, który może mieć jedną z 3 wartości. Działania wykonywane przez środowisko wykonawcze zależą od tego, czy zwróciło ono wskaźnik do rzeczywistych danych, czy ich kopię:

  • 0
    • Rzeczywistość: obiekt tablicy jest odpięty.
    • Kopiowanie: dane są kopiowane z powrotem. Bufor z kopią zostanie zwolniony.
  • JNI_COMMIT
    • Działanie: nic nie robi.
    • Kopiowanie: dane są kopiowane z powrotem. Bufor z kopią nie jest zwalniany.
  • JNI_ABORT
    • Rzeczywistość: obiekt tablicy jest odpięty. Wcześniejsze zapisy nie są przerywane.
    • Kopiowanie: bufor z kopią jest zwalniany, a wszelkie zmiany w nim zostają utracone.

Sprawdzanie flagi isCopy jest przydatne, aby wiedzieć, czy po wprowadzeniu zmian w tablicy trzeba wywołać funkcję Release z argumentem JNI_COMMIT. Jeśli na przemian wprowadzasz zmiany i wykonujesz kod, który korzysta z zawartości tablicy, możesz pominąć operację commit bez efektu. Innym możliwym powodem sprawdzania flagi jest efektywne zarządzanie JNI_ABORT. Możesz na przykład pobrać tablicę, zmodyfikować ją w miejscu, przekazać jej części do innych funkcji, a następnie odrzucić zmiany. Jeśli wiesz, że JNI tworzy dla Ciebie nową kopię, nie musisz tworzyć kolejnej kopii „z możliwością edycji”. Jeśli JNI przekazuje Ci oryginał, musisz utworzyć własną kopię.

Częstym błędem (powtarzanym w przykładach kodu) jest założenie, że można pominąć wywołanie Release, jeśli *isCopy ma wartość false. Tak nie jest. Jeśli nie przydzielono bufora kopii, oryginalna pamięć musi być przypięta i nie może być przenoszona przez moduł odśmiecania.

Pamiętaj też, że flaga JNI_COMMIT nie zwalnia tablicy. W końcu musisz ponownie wywołać funkcję Release z inną flagą.

Połączenia z regionu

Istnieje alternatywa dla wywołań takich jak Get<Type>ArrayElementsGetStringChars, która może być bardzo przydatna, gdy chcesz tylko skopiować dane. Weź pod uwagę następujące kwestie:

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

Pobiera tablicę, kopiuje z niej pierwsze len bajtów, a potem zwalnia tablicę. W zależności od implementacji wywołanie Get przypnie lub skopiuje zawartość tablicy. Kod kopiuje dane (być może po raz drugi), a następnie wywołuje funkcję Release. W tym przypadku JNI_ABORT zapewnia, że nie ma możliwości utworzenia trzeciej kopii.

Można to zrobić w prostszy sposób:

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

Ma to kilka zalet:

  • Wymaga 1 wywołania JNI zamiast 2, co zmniejsza obciążenie.
  • Nie wymaga przypinania ani dodatkowych kopii danych.
  • Zmniejsza ryzyko błędu programisty – nie ma ryzyka zapomnienia wywołania funkcji Release po wystąpieniu błędu.

Podobnie możesz użyć wywołania Set<Type>ArrayRegion, aby skopiować dane do tablicy, oraz GetStringRegion lub GetStringUTFRegion, aby skopiować znaki z String.

Wyjątki

Nie możesz wywoływać większości funkcji JNI, gdy oczekuje wyjątek. Kod powinien wykryć wyjątek (za pomocą wartości zwracanej przez funkcję,ExceptionCheck lub ExceptionOccurred) i zwrócić lub wyczyścić wyjątek i go obsłużyć.

Jedynymi funkcjami JNI, które możesz wywołać, gdy oczekuje się wyjątku, są:

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

Wiele wywołań JNI może zgłaszać wyjątki, ale często udostępniają prostszy sposób sprawdzania błędów. Jeśli na przykład funkcja NewString zwraca wartość inną niż NULL, nie musisz sprawdzać, czy wystąpił wyjątek. Jeśli jednak wywołasz metodę (za pomocą funkcji takiej jak CallObjectMethod), zawsze musisz sprawdzić, czy nie wystąpił wyjątek, ponieważ wartość zwracana nie będzie prawidłowa, jeśli wyjątek został zgłoszony.

Pamiętaj, że wyjątki zgłaszane przez kod zarządzany nie powodują wycofania natywnych ramek stosu. (Wyjątki C++, które są na Androidzie ogólnie odradzane, nie mogą być zgłaszane w ramach przejścia przez granicę JNI z kodu C++ do kodu zarządzanego). Instrukcje JNI ThrowThrowNew tylko ustawiają wskaźnik wyjątku w bieżącym wątku. Po powrocie do kodu zarządzanego z kodu natywnego wyjątek zostanie odnotowany i odpowiednio obsłużony.

Kod natywny może „przechwycić” wyjątek, wywołując funkcję ExceptionCheck lub ExceptionOccurred, i usunąć go za pomocą funkcji ExceptionClear. Jak zwykle odrzucanie wyjątków bez ich obsługi może prowadzić do problemów.

Nie ma wbudowanych funkcji do manipulowania samym obiektem Throwable, więc jeśli chcesz np. uzyskać ciąg znaków wyjątku, musisz znaleźć klasę Throwable, wyszukać identyfikator metody dla getMessage "()Ljava/lang/String;", wywołać ją, a jeśli wynik nie jest wartością NULL, użyć GetStringUTFChars, aby uzyskać coś, co możesz przekazać do printf(3) lub podobnej funkcji.

Rozszerzone sprawdzanie

JNI wykonuje bardzo mało sprawdzania błędów. Błędy zwykle powodują awarię. Android oferuje też tryb o nazwie CheckJNI, w którym wskaźniki tabeli funkcji JavaVM i JNIEnv są przełączane na tabele funkcji, które przed wywołaniem standardowej implementacji wykonują rozszerzoną serię sprawdzeń.

Dodatkowe kontrole obejmują:

  • Tablice: próba przydzielenia tablicy o ujemnym rozmiarze.
  • Nieprawidłowe wskaźniki: przekazywanie nieprawidłowego argumentu jarray/jclass/jobject/jstring do wywołania JNI lub przekazywanie wskaźnika NULL do wywołania JNI z argumentem, który nie może mieć wartości null.
  • Nazwy klas: przekazywanie do wywołania JNI czegoś innego niż nazwa klasy w stylu „java/lang/String”.
  • Krytyczne wywołania: wywołanie JNI między „krytycznym” pobraniem a odpowiednim zwolnieniem.
  • Direct ByteBuffers: przekazywanie nieprawidłowych argumentów do funkcji NewDirectByteBuffer.
  • Wyjątki: wykonywanie wywołania JNI, gdy oczekuje wyjątek.
  • JNIEnv*: używanie JNIEnv* z niewłaściwego wątku.
  • jfieldIDs: użycie wartości NULL jfieldID, użycie jfieldID do ustawienia pola na wartość nieprawidłowego typu (np. próba przypisania obiektu StringBuilder do pola String), użycie jfieldID dla pola statycznego do ustawienia pola instancji lub odwrotnie albo użycie jfieldID z jednej klasy z instancjami innej klasy.
  • jmethodIDs: użycie niewłaściwego rodzaju jmethodID podczas wykonywania wywołania JNI: nieprawidłowy typ zwracany, niezgodność statyczności, nieprawidłowy typ argumentu „this” (w przypadku wywołań niestatycznych) lub nieprawidłowa klasa (w przypadku wywołań statycznych).Call*Method
  • Odwołania: użycie DeleteGlobalRef/DeleteLocalRef w nieodpowiednim rodzaju odwołania.
  • Tryby zwalniania: przekazywanie nieprawidłowego trybu zwalniania do wywołania zwalniania (innego niż 0, JNI_ABORT lub JNI_COMMIT).
  • Bezpieczeństwo typów: zwracanie niezgodnego typu z metody natywnej (np. zwracanie obiektu StringBuilder z metody zadeklarowanej jako zwracającej ciąg znaków).
  • UTF-8: przekazywanie nieprawidłowej sekwencji bajtów zmodyfikowanego UTF-8 do wywołania JNI.

(Dostępność metod i pól nie jest jeszcze sprawdzana: ograniczenia dostępu nie mają zastosowania do kodu natywnego).

CheckJNI można włączyć na kilka sposobów.

Jeśli używasz emulatora, CheckJNI jest domyślnie włączony.

Jeśli masz urządzenie z dostępem do roota, możesz użyć tej sekwencji poleceń, aby ponownie uruchomić środowisko wykonawcze z włączoną funkcją CheckJNI:

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

W obu tych przypadkach po uruchomieniu środowiska wykonawczego w danych wyjściowych Logcat zobaczysz coś takiego:

D AndroidRuntime: CheckJNI is ON

Jeśli masz zwykłe urządzenie, możesz użyć tego polecenia:

adb shell setprop debug.checkjni 1

Nie wpłynie to na już uruchomione aplikacje, ale w przypadku każdej aplikacji uruchomionej od tego momentu funkcja CheckJNI będzie włączona. (Zmiana właściwości na dowolną inną wartość lub po prostu ponowne uruchomienie wyłączy ponownie CheckJNI). W takim przypadku przy następnym uruchomieniu aplikacji w danych wyjściowych logcat zobaczysz coś takiego:

D Late-enabling CheckJNI

Możesz też ustawić atrybut android:debuggable w pliku manifestu aplikacji, aby włączyć CheckJNI tylko w przypadku tej aplikacji. Pamiętaj, że narzędzia do kompilacji Androida zrobią to automatycznie w przypadku niektórych typów kompilacji.

Biblioteki natywne

Kod natywny z bibliotek udostępnionych możesz wczytywać za pomocą standardowego polecenia System.loadLibrary.

W praktyce starsze wersje Androida miały błędy w usłudze PackageManager, które powodowały, że instalowanie i aktualizowanie bibliotek natywnych było zawodne. Projekt ReLinker oferuje obejścia tego i innych problemów z wczytywaniem bibliotek natywnych.

Wywołaj funkcję System.loadLibrary (lub ReLinker.loadLibrary) z inicjatora klasy statycznej. Argumentem jest „nieozdobiona” nazwa biblioteki, więc aby wczytać libfubar.so, musisz przekazać "fubar".

Jeśli masz tylko jedną klasę z metodami natywnymi, wywołanie System.loadLibrary powinno znajdować się w inicjatorze statycznym tej klasy. W przeciwnym razie warto wywołać funkcję z Application, aby mieć pewność, że biblioteka jest zawsze wczytywana i zawsze wczytywana z wyprzedzeniem.

Środowisko wykonawcze może znaleźć metody natywne na 2 sposoby. Możesz je jawnie zarejestrować w RegisterNatives lub pozwolić środowisku wykonawczemu na dynamiczne wyszukiwanie ich za pomocą dlsym. Zaletą RegisterNatives jest to, że z góry sprawdzasz, czy symbole istnieją, a także możesz mieć mniejsze i szybsze biblioteki współdzielone, ponieważ eksportujesz tylko JNI_OnLoad. Zaletą umożliwienia środowisku wykonawczemu wykrywania funkcji jest to, że trzeba napisać nieco mniej kodu.

Aby użyć aplikacji RegisterNatives:

  • Podaj funkcję JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved).
  • W pliku JNI_OnLoad zarejestruj wszystkie metody natywne za pomocą RegisterNatives.
  • Utwórz skrypt wersji (zalecane) lub użyj -fvisibility=hidden, aby z biblioteki eksportować tylko JNI_OnLoad. Dzięki temu kod jest szybszy i mniejszy, a także unikasz potencjalnych kolizji z innymi bibliotekami załadowanymi do aplikacji (ale w przypadku awarii aplikacji w kodzie natywnym ślady stosu są mniej przydatne).

Statyczny inicjator powinien wyglądać tak:

Kotlin

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

Java

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

Funkcja JNI_OnLoad napisana w C++ powinna wyglądać mniej więcej tak:

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

Jeśli chcesz zamiast tego użyć „odkrywania” metod natywnych, musisz je nazwać w określony sposób (szczegóły znajdziesz w specyfikacji JNI). Oznacza to, że jeśli sygnatura metody jest nieprawidłowa, dowiesz się o tym dopiero przy pierwszym wywołaniu tej metody.

Wszelkie wywołania FindClassJNI_OnLoad będą rozwiązywać klasy w kontekście narzędzia do wczytywania klas, które zostało użyte do wczytania biblioteki współdzielonej. Gdy wywoływana jest z innych kontekstów, funkcja FindClass używa narzędzia do wczytywania klas powiązanego z metodą na górze stosu Java lub, jeśli takiego narzędzia nie ma (ponieważ wywołanie pochodzi z wątku natywnego, który został właśnie dołączony), używa narzędzia do wczytywania klas „system”. Systemowy program ładujący klasy nie zna klas aplikacji, więc nie możesz wyszukiwać własnych klas za pomocą FindClass w tym kontekście. Dzięki temu JNI_OnLoad jest wygodnym miejscem do wyszukiwania i buforowania klas: gdy masz prawidłowe jclass odwołanie globalne, możesz go używać z dowolnego dołączonego wątku.

Szybsze połączenia natywne dzięki @FastNative i @CriticalNative

Metody natywne można oznaczać adnotacjami @FastNative lub @CriticalNative (ale nie obiema jednocześnie), aby przyspieszyć przejścia między kodem zarządzanym a natywnym. Te adnotacje wiążą się jednak z pewnymi zmianami w działaniu, które należy dokładnie rozważyć przed użyciem. Poniżej krótko opisujemy te zmiany, ale szczegółowe informacje znajdziesz w dokumentacji.

Adnotację @CriticalNative można stosować tylko do metod natywnych, które nie używają obiektów zarządzanych (w parametrach, wartościach zwracanych ani jako niejawny parametr this), a ta adnotacja zmienia interfejs ABI przejścia JNI. Implementacja natywna musi wykluczać parametry JNIEnvjclass z sygnatury funkcji.

Podczas wykonywania metody @FastNative lub @CriticalNative odśmiecanie nie może zawiesić wątku w celu wykonania niezbędnej pracy i może zostać zablokowane. Nie używaj tych adnotacji w przypadku długotrwałych metod, w tym metod zwykle szybkich, ale ogólnie nieograniczonych. W szczególności kod nie powinien wykonywać znaczących operacji wejścia-wyjścia ani uzyskiwać natywnych blokad, które mogą być utrzymywane przez długi czas.

Te adnotacje zostały wdrożone do użytku systemowego od Androida 8 i stały się publicznym interfejsem API testowanym w ramach CTS w Androidzie 14. Te optymalizacje prawdopodobnie będą działać również na urządzeniach z Androidem 8–13 (chociaż bez gwarancji CTS), ale dynamiczne wyszukiwanie metod natywnych jest obsługiwane tylko na Androidzie 12 lub nowszym, a jawna rejestracja w JNI RegisterNatives jest bezwzględnie wymagana do działania na Androidzie w wersji 8–11. Na Androidzie 7 i starszych te adnotacje są ignorowane, a niezgodność ABI w przypadku @CriticalNative prowadzi do nieprawidłowego przekazywania argumentów i prawdopodobnie do awarii.

W przypadku metod o krytycznym znaczeniu dla wydajności, które wymagają tych adnotacji, zdecydowanie zalecamy wyraźne zarejestrowanie metod w JNI RegisterNatives zamiast polegać na „odkrywaniu” metod natywnych na podstawie nazwy. Aby uzyskać optymalną wydajność uruchamiania aplikacji, zalecamy uwzględnienie wywołań metod @FastNative lub @CriticalNativeprofilu podstawowym. Od Androida 12 wywołanie @CriticalNative metody natywnej z skompilowanej metody zarządzanej jest prawie tak tanie jak wywołanie nieinline’owe w C/C++, o ile wszystkie argumenty mieszczą się w rejestrach (np. do 8 argumentów całkowitych i do 8 argumentów zmiennoprzecinkowych na arm64).

Czasami lepiej jest podzielić metodę natywną na 2 metody: bardzo szybką, która może się nie powieść, i inną, która obsługuje wolniejsze przypadki. Na przykład:

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

Wskazówki dotyczące wersji 64-bitowej

Aby obsługiwać architektury, które używają 64-bitowych wskaźników, podczas przechowywania wskaźnika do struktury natywnej w polu Java używaj pola long zamiast pola int.

Nieobsługiwane funkcje i zgodność wsteczna

Obsługiwane są wszystkie funkcje JNI 1.6 z wyjątkiem:

  • DefineClass nie jest zaimplementowana. Android nie używa plików bytecode Javy ani plików klas, więc przekazywanie binarnych danych klasy nie działa.

Aby zapewnić zgodność wsteczną ze starszymi wersjami Androida, musisz pamiętać o tych kwestiach:

  • Dynamiczne wyszukiwanie funkcji natywnych

    Do Androida 2.0 (Eclair) znak „$” nie był prawidłowo konwertowany na „_00024” podczas wyszukiwania nazw metod. Aby obejść ten problem, musisz użyć jawnej rejestracji lub przenieść metody natywne poza klasy wewnętrzne.

  • Odłączanie wątków

    Do Androida 2.0 (Eclair) nie można było używać funkcji pthread_key_create destruktora, aby uniknąć sprawdzania „wątek musi zostać odłączony przed zakończeniem”. (Środowisko wykonawcze używa też funkcji destruktora klucza pthread, więc wyścig o to, która z nich zostanie wywołana jako pierwsza, byłby nierozstrzygnięty).

  • Słabe globalne pliki referencyjne

    Do wersji Androida 2.2 (Froyo) słabe odniesienia globalne nie były zaimplementowane. Starsze wersje będą zdecydowanie odrzucać próby ich użycia. Aby sprawdzić, czy funkcja jest obsługiwana, możesz użyć stałych wersji platformy Android.

    Do Androida 4.0 (Ice Cream Sandwich) słabe odwołania globalne można było przekazywać tylko do funkcji NewLocalRef, NewGlobalRefDeleteWeakGlobalRef. (Specyfikacja zdecydowanie zaleca programistom tworzenie twardych odwołań do słabych zmiennych globalnych przed wykonaniem jakichkolwiek operacji na nich, więc nie powinno to w żaden sposób ograniczać możliwości).

    Od Androida 4.0 (Ice Cream Sandwich) słabe odwołania globalne mogą być używane jak inne odwołania JNI.

  • Lokalne pliki referencyjne

    Do wersji Androida 4.0 (Ice Cream Sandwich) odwołania lokalne były w rzeczywistości bezpośrednimi wskaźnikami. W wersji Ice Cream Sandwich dodano pośrednie odwołania, które są niezbędne do obsługi lepszych mechanizmów odśmiecania pamięci, ale oznacza to, że wiele błędów JNI jest niewykrywalnych w starszych wersjach. Więcej informacji znajdziesz w artykule JNI Local Reference Changes in ICS.

    W wersjach Androida starszych niż Android 8.0 liczba lokalnych odwołań jest ograniczona do limitu specyficznego dla danej wersji. Od Androida 8.0 Android obsługuje nieograniczoną liczbę lokalnych odwołań.

  • Określanie typu odwołania za pomocą symbolu GetObjectRefType

    Do wersji Androida 4.0 (Ice Cream Sandwich) nie można było prawidłowo zaimplementować GetObjectRefType ze względu na używanie bezpośrednich wskaźników (patrz wyżej). Zamiast tego użyliśmy heurystyki, która w tej kolejności przeszukiwała tabelę słabych zmiennych globalnych, argumenty, tabelę zmiennych lokalnych i tabelę zmiennych globalnych. Gdy po raz pierwszy znajdzie wskaźnik bezpośredni, zgłosi, że odwołanie jest typu, który akurat sprawdza. Oznaczało to na przykład, że jeśli wywołasz GetObjectRefType w globalnej klasie jclass, która jest taka sama jak klasa jclass przekazana jako argument domyślny do statycznej metody natywnej, otrzymasz JNILocalRefType zamiast JNIGlobalRefType.

  • @FastNative i @CriticalNative

    W Androidzie 7 i starszych wersjach te adnotacje optymalizacyjne były ignorowane. Niezgodność interfejsu ABI w przypadku @CriticalNative spowoduje nieprawidłowe przekazywanie argumentów i prawdopodobnie awarie.

    Dynamiczne wyszukiwanie funkcji natywnych w przypadku metod @FastNative@CriticalNative nie zostało zaimplementowane w Androidzie 8–10 i zawiera znane błędy w Androidzie 11. Korzystanie z tych optymalizacji bez wyraźnej rejestracji w JNIRegisterNatives może prowadzić do awarii na urządzeniach z Androidem w wersji 8–11.

  • FindClass rzuca ClassNotFoundException

    Ze względu na zgodność wsteczną Android zgłasza wyjątek ClassNotFoundException zamiast NoClassDefFoundError, gdy klasa nie zostanie znaleziona przez FindClass. To zachowanie jest zgodne z interfejsem Java Reflection APIClass.forName(name).

Najczęstsze pytania: dlaczego widzę symbol UnsatisfiedLinkError?

Podczas pracy nad kodem natywnym często pojawia się taki błąd:

java.lang.UnsatisfiedLinkError: Library foo not found

W niektórych przypadkach oznacza to, że biblioteki nie znaleziono. W innych przypadkach biblioteka istnieje, ale nie można jej otworzyć za pomocą dlopen(3). Szczegóły błędu można znaleźć w komunikacie o wyjątku.

Najczęstsze przyczyny występowania wyjątków „library not found”:

  • Biblioteka nie istnieje lub jest niedostępna dla aplikacji. Użyj funkcji adb shell ls -l <path>, aby sprawdzić jej obecność i uprawnienia.
  • Biblioteka nie została utworzona za pomocą NDK. Może to spowodować zależności od funkcji lub bibliotek, które nie istnieją na urządzeniu.

Inna klasa błędów UnsatisfiedLinkError wygląda tak:

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

W logcat zobaczysz:

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

Oznacza to, że środowisko wykonawcze próbowało znaleźć pasującą metodę, ale nie udało mu się to. Oto kilka najczęstszych przyczyn:

  • Biblioteka nie wczytuje się. Sprawdź dane wyjściowe logcat pod kątem komunikatów o wczytywaniu biblioteki.
  • Metoda nie została znaleziona z powodu niezgodności nazwy lub podpisu. Zwykle jest to spowodowane:
    • W przypadku wyszukiwania metod leniwych nie udało się zadeklarować funkcji C++ z użyciem extern "C" i odpowiedniej widoczności (JNIEXPORT). Pamiętaj, że przed Ice Cream Sandwich makro JNIEXPORT było nieprawidłowe, więc użycie nowego kompilatora GCC ze starym jni.h nie będzie działać. Możesz użyć arm-eabi-nm, aby zobaczyć symbole w bibliotece. Jeśli wyglądają na zniekształcone (np. _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass zamiast Java_Foo_myfunc) lub jeśli typ symbolu to mała litera „t” zamiast wielkiej litery „T”, musisz dostosować deklarację.
    • W przypadku jawnej rejestracji drobne błędy podczas wpisywania sygnatury metody. Upewnij się, że to, co przekazujesz do wywołania rejestracji, jest zgodne z sygnaturą w pliku dziennika. Pamiętaj, że „B” to byte, a „Z” to boolean. Składniki nazw klas w sygnaturach zaczynają się od litery „L”, kończą się znakiem „;”, używają znaku „/” do oddzielania nazw pakietów i klas oraz znaku „$” do oddzielania nazw klas wewnętrznych (Ljava/util/Map$Entry;).

Używanie javah do automatycznego generowania nagłówków JNI może pomóc uniknąć niektórych problemów.

Najczęstsze pytania: dlaczego FindClass nie znalazł moich zajęć?

(Większość tych porad dotyczy również sytuacji, w których nie można znaleźć metod z GetMethodID lub GetStaticMethodID albo pól z GetFieldID lub GetStaticFieldID).

Sprawdź, czy ciąg nazwy klasy ma prawidłowy format. Nazwy klas JNI zaczynają się od nazwy pakietu i są oddzielone ukośnikami, np. java/lang/String. Jeśli szukasz klasy tablicy, musisz zacząć od odpowiedniej liczby nawiasów kwadratowych, a także ująć klasę w znaki „L” i „;”, więc jednowymiarowa tablica String będzie wyglądać tak: [Ljava/lang/String;. Jeśli szukasz klasy wewnętrznej, użyj znaku „$” zamiast „.”. Ogólnie rzecz biorąc, użycie polecenia javap w pliku .class to dobry sposób na znalezienie wewnętrznej nazwy klasy.

Jeśli włączysz kompresowanie kodu, skonfiguruj, który kod ma zostać zachowany. Prawidłowe skonfigurowanie reguł zachowywania jest ważne, ponieważ w przeciwnym razie narzędzie do zmniejszania kodu może usunąć klasy, metody lub pola, które są używane tylko w JNI.

Jeśli nazwa klasy jest prawidłowa, problem może dotyczyć narzędzia do wczytywania klas. FindClass chce rozpocząć wyszukiwanie klasy w programie ładującym klasy powiązanym z Twoim kodem. Sprawdza ona stos wywołań, który wygląda mniej więcej tak:

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

Najwyżej na liście znajduje się metoda Foo.myfunc. FindClass znajduje obiekt ClassLoader powiązany z klasą Foo i używa go.

Zwykle robi to, czego oczekujesz. Możesz mieć problemy, jeśli sam utworzysz wątek (np. wywołując pthread_create, a następnie dołączając go za pomocą AttachCurrentThread). W takim przypadku nie ma ramek stosu z Twojej aplikacji. Jeśli wywołasz FindClass z tego wątku, JavaVM uruchomi się w ładowarce klas „system”, a nie w ładowarce powiązanej z Twoją aplikacją, więc próby znalezienia klas specyficznych dla aplikacji zakończą się niepowodzeniem.

Możesz to obejść na kilka sposobów:

  • Wykonaj wyszukiwania FindClass tylko raz w JNI_OnLoad i zapisz w pamięci podręcznej odwołania do klas, aby użyć ich później. Wszystkie wywołania FindClass wykonane w ramach realizacji JNI_OnLoad będą korzystać z ładowarki klas powiązanej z funkcją, która wywołała System.loadLibrary (jest to specjalna reguła, która ma ułatwić inicjowanie biblioteki). Jeśli kod aplikacji wczytuje bibliotekę, FindClassużyje prawidłowego programu wczytującego klasy.
  • Przekaż instancję klasy do funkcji, które jej potrzebują, deklarując metodę natywną, która przyjmuje argument Class, a następnie przekazując Foo.class.
  • Zapisz w pamięci podręcznej odwołanie do obiektu ClassLoader w wygodnym miejscu i bezpośrednio wywołuj funkcje loadClass. Wymaga to pewnego wysiłku.

Najczęstsze pytania: jak udostępniać nieprzetworzone dane za pomocą kodu natywnego?

Może się zdarzyć, że będziesz potrzebować dostępu do dużego bufora surowych danych z kodu zarządzanego i natywnego. Typowe przykłady to manipulowanie bitmapami lub próbkami dźwięku. Istnieją 2 podstawowe podejścia.

Dane możesz przechowywać w byte[]. Umożliwia to bardzo szybki dostęp z kodu zarządzanego. Po stronie natywnej nie masz jednak gwarancji, że będziesz mieć dostęp do danych bez konieczności ich kopiowania. W niektórych implementacjach metody GetByteArrayElementsGetPrimitiveArrayCritical zwracają rzeczywiste wskaźniki do surowych danych w zarządzanym stogu, a w innych przydzielają bufor w stogu natywnym i kopiują do niego dane.

Alternatywą jest przechowywanie danych w buforze bajtów bezpośrednich. Można je tworzyć za pomocą funkcji java.nio.ByteBuffer.allocateDirect lub funkcji JNI NewDirectByteBuffer. W przeciwieństwie do zwykłych buforów bajtów pamięć nie jest przydzielana na zarządzanym stercie i zawsze można uzyskać do niej bezpośredni dostęp z kodu natywnego (adres można uzyskać za pomocą GetDirectBufferAddress). W zależności od sposobu implementacji bezpośredniego dostępu do bufora bajtów dostęp do danych z kodu zarządzanego może być bardzo powolny.

Wybór zależy od 2 czynników:

  1. Czy większość dostępów do danych będzie pochodzić z kodu napisanego w języku Java lub C/C++?
  2. Jeśli dane są ostatecznie przekazywane do interfejsu API systemu, w jakiej formie muszą się znajdować? (Jeśli np. dane są ostatecznie przekazywane do funkcji, która przyjmuje argument byte[], przetwarzanie w bezpośrednim ByteBuffer może być nierozsądne).

Jeśli nie ma wyraźnego zwycięzcy, użyj bezpośredniego bufora bajtów. Ich obsługa jest wbudowana bezpośrednio w JNI, a wydajność powinna się poprawić w przyszłych wersjach.