JNI to natywny interfejs Java. Określa sposób kodowania bajtowego, na podstawie którego Android kompiluje kod zarządzany (napisany w języku Java lub Kotlin) umożliwiający interakcję z kodem natywnym (napisane w języku C/C++). JNI działa niezależnie od dostawcy i obsługuje wczytywanie kodu z dynamicznego udostępniania i chociaż czasem jest to niewygodne, działa rozsądnie.
Uwaga: Android kompiluje Kotlin na kod bajtowy zgodny z ART podobnie jak w języku Java, możesz zastosować wskazówki na tej stronie zarówno języka programowania Kotlin i Java w kontekście architektury JNI i powiązanych z nimi kosztów. Więcej informacji znajdziesz w artykule Kotlin i Android.
Jeśli jeszcze tego nie znasz, przeczytaj Specyfikacja interfejsu natywnego Java aby zorientować się, jak działa JNI i jakie funkcje są dostępne. Niektóre aspekty interfejsu mogą nie być od razu oczywiste, dlatego warto przeczytać kilka kolejnych sekcji.
Aby przeglądać globalne odniesienia JNI i sprawdzić, gdzie tworzone i usuwane są globalne odwołania do JNI, użyj widok sterty JNI w narzędziu Memory Profiler. w Android Studio 3.2 i nowszych wersjach.
Wskazówki ogólne
Postaraj się zminimalizować rozmiar warstwy JNI. Tutaj należy wziąć pod uwagę kilka wymiarów. Twoje rozwiązanie JNI powinno być zgodne z tymi wytycznymi (wymienione poniżej w kolejności według ważności, zaczynając od najważniejszego):
- Zminimalizuj sterowanie zasobami na poziomie JNI. Zbiórka przez warstwa JNI wiąże się z prostymi kosztami. Postaraj się zaprojektować interfejs, który będzie ograniczać do minimum ilość które są niezbędne do gromadzenia danych, oraz częstotliwość, z jaką należy je grupować.
- Unikaj asynchronicznej komunikacji między kodem napisanym w zarządzanym języku programowania a kodem napisanym w C++ (w miarę możliwości). Dzięki temu łatwiej będzie Ci utrzymać interfejs JNI. Zwykle można uprościć proces asynchroniczny Aktualizuje interfejs asynchronicznie w tym samym języku co interfejs użytkownika. Na przykład zamiast wywoływać funkcję C++ z wątku interfejsu użytkownika w kodzie Java za pomocą JNI lepiej jest wykonać wywołanie zwrotne między 2 wątkami w języku programowania Java, z których jeden wywołuje blokujące wywołanie C++, a następnie powiadamia wątek interfejsu użytkownika po zakończeniu wywołania blokującego.
- Zminimalizuj liczbę wątków, które muszą być dotykane lub dotykać JNI. Jeśli musisz korzystać z pul wątków zarówno w języku Java, jak i C++, staraj się, aby komunikacja JNI odbywała się między właścicielami puli, a nie między poszczególnymi wątkami roboczymi.
- Kod interfejsu powinien zawierać jak najmniejszą liczbę łatwo zidentyfikowanych źródeł w językach C++ i Java. lokalizacji umożliwiające refaktoryzacje w przyszłości. Rozważ użycie automatycznego generowania JNI z biblioteki.
JavaVM i JNIEnv
JNI definiuje 2 kluczowe struktury danych: „JavaVM” i „JNIEnv”. Oba te elementy są zasadniczo wskaźniki do tabel funkcji. (W wersji C++ są to klasy z i wskaźnik do tabeli funkcji oraz funkcji składowej każdej funkcji JNI, która pośrednia przez tabeli). JavaVM zapewnia „interfejs wywołania” funkcje, które pozwalają utworzyć i zniszczyć maszynę wirtualną JavaVM. Teoretycznie można mieć wiele maszyn JavaVM na proces, a Android dopuszcza tylko jedną z nich.
JNIEnv zapewnia większość funkcji JNI. Wszystkie funkcje natywne otrzymują JNIEnv jako
pierwszego argumentu, z wyjątkiem metod @CriticalNative
,
zobacz szybsze wywołania natywne.
Protokół JNIEnv jest używany do obsługi pamięci lokalnej w wątku. Z tego powodu nie można udostępniać JNIEnv między wątkami.
Jeśli dla fragmentu kodu nie ma innego sposobu uzyskania JNIEnv, udostępnij
JavaVM i użyć narzędzia GetEnv
, aby wykryć komponent JNIEnv wątku. (Zakładając, że ją ma; patrz AttachCurrentThread
poniżej).
Deklaracje C w JNIEnv i JavaVM różnią się od deklaracji w C++
deklaracje. Plik nagłówka "jni.h"
udostępnia różne typy definicji w zależności od tego, czy jest on uwzględniany w języku C czy C++. Z tego powodu nie zalecamy uwzględniania argumentów JNIEnv w plikach nagłówka uwzględnianych przez oba języki. (Użyj innej metody: jeśli
plik nagłówka wymaga parametru #ifdef __cplusplus
. W razie potrzeby wykonanie dodatkowych czynności może być
ten nagłówek odnosi się do JNIEnv).
Wątki
Wszystkie wątki to wątki Linuksa zaplanowane przez jądro. Zazwyczaj są to
uruchomiono za pomocą kodu zarządzanego (przy użyciu: Thread.start()
),
ale można je też utworzyć w innym miejscu, a potem dołączyć do elementu JavaVM
. Dla:
przykład, wątek zaczynający się od pthread_create()
lub std::thread
można dołączyć za pomocą AttachCurrentThread()
lub
AttachCurrentThreadAsDaemon()
. Do momentu, gdy wątek zostanie
nie ma przypisanego JNIEnv i nie może wykonywać wywołań JNI.
Zwykle najlepiej jest użyć Thread.start()
, aby utworzyć wątek, który musi wywoływać kod Java. Dzięki temu będziesz mieć wystarczająco dużo miejsca na stos, będziesz w prawidłowej ThreadGroup
i będziesz używać tego samego ClassLoader
co w kodzie Java. Łatwiej też jest ustawić nazwę wątku do debugowania w języku Java niż w kodzie natywnym (zobacz pthread_setname_np()
, jeśli masz pthread_t
lub thread_t
, i std::thread::native_handle()
, jeśli masz std::thread
i chcesz pthread_t
).
Dołączenie wątku utworzonego natywnie powoduje utworzenie obiektu java.lang.Thread
i dodanie go do obiektu „main” ThreadGroup
, dzięki czemu jest on widoczny dla debugera. Dzwonię pod AttachCurrentThread()
w już załączonym wątku jest no-op.
Android nie zawiesza wątków wykonujących kod natywny. Jeśli trwa usuwanie czyszczenia lub debuger zawiesił dane Android wstrzyma wątek przy następnym wywołaniu JNI.
Wątki dołączone przez JNI wymagają wywołania
DetachCurrentThread()
przed ich zamknięciem.
Jeśli kodowanie bezpośrednio jest niezręczne, na Androidzie 2.0 (Eclair) i nowszych
może użyć funkcji pthread_key_create()
do zdefiniowania destruatora
funkcję, która zostanie wywołana przed zakończeniem wątku, oraz
zadzwonić pod numer DetachCurrentThread()
. (Użyj tego
klucz z pthread_setspecific()
do przechowywania JNIEnv w
thread-local-storage; w ten sposób zostanie on przekazany do destruktora jako
argumentu).
jclass, jmethodID i jfieldID
Jeśli chcesz uzyskać dostęp do pola obiektu z poziomu kodu natywnego, wykonaj te czynności:
- Pobieranie odwołania do obiektu klasy za pomocą
FindClass
- Uzyskaj identyfikator pola za pomocą funkcji
GetFieldID
- Pobierz odpowiednią zawartość pola, np.
GetIntField
Podobnie, aby wywołać metodę, musisz najpierw uzyskać odniesienie do obiektu klasy, a następnie identyfikator metody. Identyfikatory te są często po prostu i wskaźniki do wewnętrznych struktur danych w czasie działania. Wyszukiwanie ich może wymagać kilku porównań ciągów znaków, ale gdy już je znajdziesz, wywołanie pola lub metody będzie bardzo szybkie.
Jeśli wydajność jest ważna, warto wyszukać wartości raz i zapisać wyniki w pamięci podręcznej w kodzie natywnym. Obowiązuje limit jednej maszyny JavaVM na proces, więc do przechowywania tych danych w statycznej strukturze lokalnej.
Odwołania do klas, identyfikatorów pól i metod pozostają ważne do momentu wyłączenia klasy. Zajęcia
są wyładowywane tylko wtedy, gdy wszystkie klasy powiązane z klasą ClassLoader mogą zostać wyczyszczone,
co zdarza się rzadko, ale na Androidzie nie będzie niemożliwe. Pamiętaj jednak, że
jclass
jest odwołaniem do klasy i musi być chronione za pomocą wywołania
do NewGlobalRef
(patrz następna sekcja).
Jeśli chcesz zapisywać identyfikatory w pamięci podręcznej podczas wczytywania klasy i automatycznie umieszczać je w pamięci podręcznej jeśli klasa zostanie wyładowana i załadowana ponownie, poprawny sposób jej zainicjowania należy dodać do odpowiedniej klasy fragment kodu, który wygląda tak:
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 wyszukuje identyfikatory. Kod
zostanie wykonane jednorazowo po zainicjowaniu klasy. jeśli klasa zostanie kiedykolwiek wyładowana z pamięci i
i załaduje ponownie stronę, zostanie on wykonany jeszcze raz.
Odwołania lokalne i globalne
Każdy argument przekazany metodzie natywnej i prawie każdy obiekt zwracany przez funkcję JNI jest „lokalnym odwołaniem”. Oznacza to, że jest on prawidłowy w tagu czas trwania bieżącej metody natywnej w bieżącym wątku. Nawet jeśli obiekt nadal istnieje po zwróceniu przez metodę natywną, odwołanie jest nieprawidłowe.
Dotyczy to wszystkich podklas klasy jobject
, w tym
jclass
, jstring
i jarray
.
(w przypadku włączonych rozszerzonych kontroli JNI środowisko uruchomieniowe będzie ostrzegać o większości przypadków niewłaściwego użycia odwołania).
Jedynym sposobem na uzyskanie odniesień nielokalnych jest użycie funkcji
NewGlobalRef
i NewWeakGlobalRef
.
Jeśli chcesz przechowywać plik referencyjny przez dłuższy okres, musisz użyć
„globalnie” odwołania. Funkcja NewGlobalRef
przyjmuje
odniesienie lokalne jako argument i zwraca wartość globalną.
Globalny plik referencyjny jest zagwarantowany, że dopóki nie wywołasz
DeleteGlobalRef
Ten wzorzec jest zwykle używany podczas buforowania zwracanego obiektu jclass w pamięci podręcznej
z: 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
w przypadku tego samego obiektu mogą być różne.
Aby sprawdzić, czy 2 odwołania odnoszą się do tego samego obiektu, musisz użyć funkcji IsSameObject
. Nigdy nie porównuj
odwołania z ==
w kodzie natywnym.
Jedną z konsekwencji jest to,
nie mogą zakładać, że odwołania do obiektów są stałe lub unikalne,
w kodzie natywnym. Wartość reprezentująca obiekt może się różnić w zależności od wywołania metody, a możliwe jest, że 2 różne obiekty będą miały tę samą wartość w kolejnych wywołaniach. Nie używać
jobject
wartości jako klucze.
Programiści muszą „nie nadmiernie przydzielać” i lokalne odniesienia. W praktyce oznacza to
W przypadku tworzenia dużej liczby lokalnych odwołań, na przykład wyświetlając
, zwolnij je ręcznie,
DeleteLocalRef
, zamiast pozwolić JNI zrobić to za Ciebie.
wymaga jedynie rezerwacji przedziałów
16 odniesień lokalnych. Jeśli potrzebujesz ich więcej, możesz je usuwać lub użyć
EnsureLocalCapacity
/PushLocalFrame
, by zarezerwować więcej.
Elementy jfieldID
i jmethodID
są nieprzezroczyste
a nie odwołania do obiektów i nie powinny być przekazywane do
NewGlobalRef
Nieprzetworzone dane
wskaźniki zwracane przez funkcje takie jak GetStringUTFChars
i GetByteArrayElements
też nie są obiektami. (mogą być przekazywane między wątkami i są ważne do momentu wywołania metody Release).
Jeden nietypowy przypadek zasługuje na oddzielną wzmiankę. Jeśli dołączysz wątku natywny za pomocą funkcji AttachCurrentThread
, kod, który uruchamiasz, nigdy automatycznie nie zwolni lokalnych odwołań, dopóki wątek nie zostanie odłączony. Dowolny lokalny
musisz usunąć je ręcznie. Zasadniczo każdy kod natywny, który tworzy lokalne odwołania w pętli, wymaga ręcznego usunięcia.
Zachowaj ostrożność podczas korzystania z odwołań globalnych. Odniesienia globalne bywają nie do uniknięcia, ale są trudne do debugowania i może powodować problemy z diagnostyką pamięci. Przy wszystkich pozostałych stałych rozwiązania z mniejszą liczbą odwołań globalnych jest prawdopodobnie lepsze.
ciągi znaków w UTF-8 i UTF-16,
W języku programowania Java zastosowano kodowanie UTF-16. Dla wygody JNI udostępnia metody, które współpracują z zmodyfikowane UTF-8. Zmodyfikowane kodowanie jest przydatne w przypadku kodu C, ponieważ koduje \u0000 jako 0xc0 0x80 zamiast 0x00. Zaletą jest to, że można liczyć na ciągi znaków w stylu C z zerowym zakończeniem. odpowiedni do zastosowania ze standardowymi funkcjami ciągu znaków libc. Wadą jest to, że nie można danych w formacie UTF-8 do JNI i oczekuje, że będą one działać prawidłowo.
Aby wyświetlić ciąg znaków String
w formacie UTF-16, użyj formatu GetStringChars
.
Pamiętaj, że ciągi UTF-16 nie są zakończone od zera, a ciąg znaków \u0000 jest dozwolony.
więc pamiętaj o długości ciągu znaków i wskaźniku jchar.
Nie zapomnij Release
ciągów znaków Get
.
funkcje ciągu znaków zwracają jchar*
lub jbyte*
, które
to wskaźniki w stylu C do danych podstawowych, a nie lokalne odwołania. Ta
są gwarantowane do czasu wywołania funkcji Release
, co oznacza, że nie są
po powrocie metody natywnej.
Dane przekazywane do funkcji NewStringUTF muszą być w formacie zmodyfikowanym UTF-8. Typowym błędem jest odczytywanie danych znakowych z pliku lub strumienia sieciowego i przekazywanie ich do funkcji NewStringUTF
bez filtrowania.
Jeśli nie wiesz, że dane mają format MUTF-8 (lub 7-bitowy kod ASCII, który jest zgodnym podzbiorem),
musisz usunąć nieprawidłowe znaki lub przekonwertować je na prawidłowy zmodyfikowany format UTF-8.
Jeśli tego nie zrobisz, konwersja UTF-16 może przynieść nieoczekiwane wyniki.
CheckJNI, który jest domyślnie włączony 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 działanie ciągów znaków UTF-16 było zwykle szybsze, tak jak w przypadku Androida
nie wymagały kopii w języku GetStringChars
, podczas gdy
Pole GetStringUTFChars
wymagało przydziału i konwersji na UTF-8.
Android 8 zmienił reprezentację String
na 8 bitów na znak
dla ciągów ASCII (aby zaoszczędzić pamięć) i zaczął używać
przeprowadzka
śmieciarek. Te funkcje znacznie ograniczają liczbę przypadków, w których ART
może wyświetlić wskaźnik do danych String
bez tworzenia kopii, nawet
za GetStringCritical
. Jeśli jednak większość ciągów znaków przetwarzanych przez kod jest krótkich, można w większości przypadków uniknąć przydzielenia i zwolnienia zasobów, używając bufora przydzielanego przez stos 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 podstawowe
JNI udostępnia funkcje dostępu do zawartości obiektów tablicy. Dostęp do tablic obiektów wymaga dostępu do jednego wpisu naraz, a tablice elementy podstawowe można odczytywać i zapisywać bezpośrednio tak, jakby były zadeklarowane w C.
Aby maksymalnie zwiększyć wydajność interfejsu bez ograniczania
wdrożenie maszyny wirtualnej, Get<PrimitiveType>ArrayElements
rodzina wywołań pozwala środowisku wykonawczym zwrócić wskaźnik do rzeczywistych elementów lub
przydzielić część pamięci i utworzyć kopię. Tak czy inaczej, został zwrócony nieprzetworzony wskaźnik
jest ważna do momentu odpowiedniego wywołania funkcji Release
(co oznacza, że jeśli dane nie zostały skopiowane, obiekt tablicy
zostaną przypięte i nie można ich przenieść w ramach kompresowania sterty).
Każda tablica, którą Get
, musi być Release
. Ponadto, jeśli Get
nie udało się wywołać, musisz upewnić się, że Twój kod nie próbuje Release
wartości NULL
później.
Możesz sprawdzić, czy dane zostały skopiowane, przekazując w pole
wskaźnik (nie NULL) dla argumentu isCopy
. Jest to rzadko przydatne.
Wywołanie Release
przyjmuje argument mode
, który może
mają jedną z trzech wartości. Działania wykonywane przez środowisko wykonawcze zależą od
czy zwrócił wskaźnik do rzeczywistych danych czy ich kopii:
0
- Rzeczywiste: obiekt tablicy jest odpięty.
- Kopiuj: dane są kopiowane z powrotem. Bufor z kopią jest zwolniony.
JNI_COMMIT
- Rzeczywista: nic nie robi.
- Kopiuj: dane są kopiowane. Bufor z kopią nie została zwolniona.
JNI_ABORT
- Prawidłowe rozwiązanie: obiekt tablicy nie jest przypięty. Wcześniej zapisy nie są przerywane.
- Copy: bufor z kopią jest zwolniony; wszelkie zmiany w niej zostają utracone.
Jednym z powodów sprawdzania flagi isCopy
jest ustalenie, czy po wprowadzeniu zmian w tablicy należy wywołać funkcję Release
z argumentem JNI_COMMIT
. Jeśli naprzemiennie wprowadzasz zmiany i wykonujesz kod, który korzysta z treści tablicy, możesz pominąć zatwierdzanie bez operacji. Innym możliwym powodem sprawdzenia flagi jest wydajne przetwarzanie JNI_ABORT
. Możesz na przykład zaznaczyć,
uzyskać tablicę, zmodyfikować ją na miejscu, przekazać fragmenty do innych funkcji oraz
i odrzuć zmiany. Jeśli wiesz, że JNI tworzy nową kopię pliku
nie ma potrzeby tworzenia kolejnych kopiuj. Jeśli JNI ma stan pozytywny
oryginału, musisz samodzielnie utworzyć jego kopię.
Typowym błędem (powtarzanym w przykładowym kodzie) jest założenie, że możesz pominąć wywołanie funkcji Release
, jeśli *isCopy
ma wartość false. To nieprawda. Jeśli brak było bufora kopiowania
przydzielona, pierwotna pamięć musi być przypięta i nie może zostać przeniesiona
na śmieci.
Pamiętaj też, że flaga JNI_COMMIT
nie zwalnia tablicy,
i trzeba będzie ponownie wywołać funkcję Release
, używając innej flagi
w końcu.
Połączenia z regionem
Istnieją alternatywne metody, takie jak Get<Type>ArrayElements
i GetStringChars
, które mogą być bardzo przydatne, 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); }
Spowoduje to pobranie tablicy i skopiowanie pierwszego len
bajtu
elementów, a potem uwalnia tablicę. W zależności od
implementacja, wywołanie Get
przypnie lub skopiuje tablicę
treści.
Kod kopiuje dane (być może po raz drugi), a potem wywołuje funkcję Release
; w tym przypadku funkcja JNI_ABORT
dba o to, aby nie powstała trzecia kopia.
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 narzut.
- Nie wymaga przypinania ani tworzenia dodatkowych kopii danych.
- Zmniejsza ryzyko błędu programisty – brak ryzyka zapomnienia
aby wywołać
Release
, gdy coś się nie uda.
Podobnie możesz użyć wywołania Set<Type>ArrayRegion
, aby skopiować dane do tablicy, oraz GetStringRegion
lub GetStringUTFRegion
, aby skopiować znaki z elementu String
.
Wyjątki
Nie można wywoływać większości funkcji JNI, gdy oczekuje wyjątek.
Twój kod powinien zauważyć wyjątek (przez zwracaną wartość funkcji
ExceptionCheck
lub ExceptionOccurred
) i zwrot,
lub usuń wyjątek i obsługuj go.
Jedynymi funkcjami JNI, które można wywołać podczas oczekiwania na zatwierdzenie 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ątek, ale często zapewnia prostszy sposób
na ewentualne błędy. Jeśli na przykład NewString
zwraca
nie ma wartości NULL, nie musisz sprawdzać wyjątku. Jeśli jednak wywołasz metodę (za pomocą funkcji takiej jak CallObjectMethod
), musisz zawsze sprawdzić, czy nie wystąpiło żadne wyjątek, ponieważ wartość zwracana nie będzie prawidłowa, jeśli wyjątek został wygenerowany.
Pamiętaj, że wyjątki zgłoszone przez kod zarządzany nie powodują wycofania stosu natywnego
ramki. (Wyjątki C++, które nie są zalecane w przypadku Androida, nie mogą być rzucane przez granicę przejścia JNI z kodu C++ do kodu zarządzanego).
Instrukcje dotyczące JNI Throw
i ThrowNew
ustaw wskaźnik wyjątku w bieżącym wątku. Po powrocie do konta zarządzanego
z kodu natywnego, wyjątek zostanie odnotowany i odpowiednio obsłużony.
Kod natywny może „przechwytywać” wyjątek, wywołając ExceptionCheck
lub
ExceptionOccurred
i usuń ją za pomocą
ExceptionClear
Jak zwykle,
odrzucanie wyjątków bez zajmowania się nimi może prowadzić do problemów.
Nie ma wbudowanych funkcji do manipulowania obiektem Throwable
więc aby uzyskać ciąg wyjątku,
znajdź klasę Throwable
, wyszukaj identyfikator metody dla
getMessage "()Ljava/lang/String;"
, wywołaj go, a jeśli wynik
nie jest wartością NULL, użyj GetStringUTFChars
, aby uzyskać coś,
do printf(3)
lub równoważnego elementu.
Rozszerzone sprawdzanie
JNI wykonuje bardzo niewielką kontrolę błędów. Błędy zwykle powodują zamykanie się aplikacji. Android oferuje również tryb CheckJNI, w którym wskaźniki tabel funkcji JavaVM i JNIEnv są przełączane na tabele funkcji, które przeprowadzają rozszerzoną serię testów przed wywołaniem standardowej implementacji.
Dodatkowe kontrole obejmują:
- Tablice: próba przydzielenia tablicy o ujemkowym rozmiarze.
- Nieprawidłowe wskaźniki: przekazanie do wywołania JNI nieprawidłowego jarray/jclass/jobject/jstring lub przekazanie do wywołania JNI wskaźnika NULL z argumentem, który nie może być NULL.
- Nazwy klas: do wywołania JNI można przekazywać dowolne nazwy klas, z wyjątkiem nazw w stylu „java/lang/String”.
- Wywołania krytyczne: wykonanie wywołania JNI między „krytycznym” pobraniem a odpowiadającą mu wersją.
- Bezpośrednie bufory ByteBuffers: przekazywanie nieprawidłowych argumentów do funkcji
NewDirectByteBuffer
. - Wyjątki: wykonywanie wywołania JNI w czasie oczekiwania na wyjątek.
- JNIEnv*s: użycie JNIEnv* z niewłaściwego wątku.
- jfieldID: używanie NULL jfieldID lub używanie jfieldID do ustawiania pola na wartość o nieprawidłowym typie (np. próba przypisania StringBuilder do pola String), używanie jfieldID dla pola stałego do ustawiania pola instancji lub odwrotnie, używanie jfieldID z jednej klasy z instancjami innej klasy.
- jmethodID: użycie niewłaściwego rodzaju identyfikatora jmethodID przy wywołaniu JNI
Call*Method
: nieprawidłowy typ zwracania, niezgodność statyczna/niestatyczna, nieprawidłowy typ dla argumentu „this” (w przypadku wywołań niestatycznych) lub nieprawidłowa klasa (w przypadku wywołań statycznych). - Pliki referencyjne: użyto
DeleteGlobalRef
/DeleteLocalRef
w niewłaściwym rodzaju pliku referencyjnego. - Tryby wersji: przekazywanie niewłaściwego trybu zwalniania do wywołania zwalniania (innego niż
0
,JNI_ABORT
lubJNI_COMMIT
). - Bezpieczeństwo typów: zwracanie niezgodnego typu z metody natywnej (np. zwracanie StringBuildera z metody zadeklarowanej jako zwracająca Stringa).
- UTF-8: przekazanie do wywołania JNI nieprawidłowej sekwencji bajtowej zmodyfikowanej UTF-8.
(Dostępność metod i pól nadal nie jest zaznaczona – ograniczenia dostępu nie mają zastosowania do kodu natywnego).
Możesz włączyć CheckJNI na kilka sposobów.
Jeśli używasz emulatora, funkcja CheckJNI jest domyślnie włączona.
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 przypadkach po uruchomieniu środowiska uruchomieniowego zobaczysz w wyjściu logcat 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 każda aplikacja uruchomiona od tego momentu będzie miała włączoną funkcję CheckJNI. (Zmień wartość właściwości na dowolną inną wartość lub zrestartowanie urządzenia ponownie spowoduje wyłączenie 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
Możesz wczytać kod natywny z bibliotek udostępnionych za pomocą
System.loadLibrary
W praktyce starsze wersje Androida zawierały błędy w Menedżerze pakietów, które powodowały instalację i aktualizacja bibliotek natywnych. ReLinker projekt oferuje sposoby obejścia tego i innych problemów z wczytywaniem biblioteki natywnej.
Wywołaj System.loadLibrary
(lub ReLinker.loadLibrary
) z statycznego konstruktora klasy. Argument to: „bezdekoracja” nazwa biblioteki,
więc aby wczytać interfejs libfubar.so
, musisz przekazać w "fubar"
.
Jeśli masz tylko jedną klasę z metodami natywnymi, wywołanie funkcji
System.loadLibrary
, aby był statycznym inicjatorem dla tej klasy. W przeciwnym razie
chcesz nawiązać połączenie z numeru Application
, aby wiedzieć, że biblioteka jest zawsze wczytywana,
i zawsze wczytywane z wyprzedzeniem.
Runtime może znaleźć Twoje metody natywnych w 2 sposoby. Możesz wprost
zarejestruj je w usłudze RegisterNatives
lub pozwól na dynamiczne wyszukiwanie w środowisku wykonawczym.
dzięki dlsym
. Zalety RegisterNatives
to możliwość sprawdzenia istnienia symboli oraz mniejsze i szybsze biblioteki współdzielone dzięki braku eksportowania czegokolwiek poza JNI_OnLoad
. Zaletą korzystania z funkcji wykrywania funkcji jest to, że nie trzeba pisać zbyt dużo kodu.
Aby użyć aplikacji RegisterNatives
:
- Podaj funkcję
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - W klasie
JNI_OnLoad
zarejestruj wszystkie metody natywne za pomocą metodyRegisterNatives
. - Utwórz wersję za pomocą skryptu wersji (zalecane) lub użyj opcji
-fvisibility=hidden
, aby z biblioteki wyeksportować tylkoJNI_OnLoad
. Dzięki temu kod jest szybszy i mniejszy oraz pozwala uniknąć potencjalnych problemów kolizje z innymi bibliotekami wczytanymi do aplikacji (ale tworzy to mniej przydatne zrzuty stosu); w przypadku awarii aplikacji w kodzie natywnym).
Statyczny inicjalizator powinien wyglądać tak:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
Funkcja JNI_OnLoad
powinna wyglądać mniej więcej tak, jeśli
napisane w 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; }
Aby zamiast tego korzystać z funkcji „odkrywania” metod natywnych, musisz nazwać je w określony sposób (zobacz specyfikacja JNI ). Oznacza to, że jeśli podpis metody jest nieprawidłowy, nie będziesz wiedzieć o nim do czasu przy pierwszym wywołaniu metody.
Wszystkie wywołania funkcji FindClass
nawiązane z adresu JNI_OnLoad
będą zamykać zajęcia w
w narzędziu do wczytywania zajęć, który został użyty do wczytania biblioteki udostępnionej. W przypadku połączenia z innego urządzenia
w kontekstach, FindClass
używa programu wczytującego klas powiązanego z metodą znajdującą się
stos Java, a jeśli takiego nie ma (wywołanie pochodzi z nowo dołączonego wątku natywnego).
Korzysta on z „systemu” . Systemowy ładownik klas nie zna klas aplikacji, więc w tym kontekście nie będzie można wyszukiwać własnych klas za pomocą FindClass
. Dzięki temu JNI_OnLoad
to wygodne miejsce do wyszukiwania i buforowania klas: raz
masz prawidłowy odniesienie globalne typu jclass
Wykorzystasz go z dowolnego dołączonego wątku.
Szybsze połączenia natywne dzięki funkcjom @FastNative
i @CriticalNative
Metody natywne można opatrzyć adnotacjami @FastNative
lub @CriticalNative
(ale nie obie naraz), aby przyspieszyć przejścia między zarządzanym a natywnym kodem. Jednak te adnotacje
wiążą się z pewnymi zmianami w działaniu, które należy dokładnie przemyśleć przed użyciem. Poniżej krótko omawiamy te zmiany, ale więcej informacji znajdziesz w dokumentacji.
Adnotację @CriticalNative
można stosować tylko do metod natywnych, które nie korzystają z innych metod
używają obiektów zarządzanych (w parametrach lub zwracanych wartościach lub jako niejawnie zdefiniowanej funkcji this
) i to
zmienia interfejs ABI przejścia JNI. Implementacja natywna musi wykluczać
Parametry JNIEnv
i jclass
z podpisu funkcji.
Podczas wykonywania metody @FastNative
lub @CriticalNative
usuwanie elementów z pamięci podręcznej nie może zawiesić wątku na czas wykonywania istotnych operacji i może zostać zablokowane. Nie używaj tych adnotacji w przypadku długotrwałych metod, w tym metod, które zwykle są szybkie, ale nieograniczone.
W szczególności kod nie powinien wykonywać istotnych operacji wejścia-wyjścia ani nakładać blokad natywnych, które
może trwać bardzo długo.
Te adnotacje zostały wdrożone do użytku systemowego od
Androida 8
i stały się publicznymi testowanymi przez CTS
API w Androidzie 14. Te optymalizacje prawdopodobnie działają też na urządzeniach z Androidem 8–13 (choć bez gwarancji CTS), ale dynamiczne wyszukiwanie metod natywnych jest obsługiwane tylko w Androidzie 12 lub nowszym. Aby działać w Androidzie 8–11, aplikacja musi być zarejestrowana za pomocą JNI RegisterNatives
. Te adnotacje są ignorowane na Androidzie 7, niezgodność interfejsu ABI
dla @CriticalNative
spowodowałoby organizowanie niewłaściwego argumentu i prawdopodobnych awarii.
W przypadku metod mających kluczowe znaczenie dla wydajności, które wymagają tych adnotacji, zdecydowanie zalecamy
bezpośrednio zarejestrować metody w JNI RegisterNatives
zamiast polegać na
„odkrywanie” oparte na nazwach metod natywnych. Aby uzyskać optymalną wydajność uruchamiania aplikacji, zalecamy uwzględnienie w profilu bazowym wywołujących metod @FastNative
lub @CriticalNative
. Od Androida 12 wywołanie natywnej metody @CriticalNative
z skompilowanej metody zarządzanej jest prawie tak tanie jak wywołanie nieliniowe 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 najlepiej jest podzielić metodę natywną na dwie, czyli bardzo szybką, i kolejnej, która zajmuje się powolnymi sprawami. 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);
Uwagi na temat wersji 64-bitowej
Aby obsługiwać architektury korzystające z 64-bitowych wskaźników, użyj pola long
zamiast
int
podczas przechowywania wskaźnika do natywnej struktury w polu Java.
Nieobsługiwane funkcje/zgodność wsteczna
Obsługiwane są wszystkie funkcje JNI 1.6 z tymi wyjątkami:
- Interfejs
DefineClass
nie został zaimplementowany. Android nie używa kod bajtowy Java lub pliki klas, więc przekazywanie danych klasy binarnej nie działa.
Aby uzyskać zgodność wsteczną ze starszymi wersjami Androida, może być konieczne pamiętaj o tych kwestiach:
- Dynamiczne wyszukiwanie funkcji natywnych
Do Androida 2.0 (Eclair) znak „$” Znak był nieprawidłowy przekonwertowano na „_00024” podczas wyszukiwania nazw metod. Aby tego uniknąć, musisz użyć jawnej rejestracji lub usunąć metody natywne z klas wewnętrznych.
- Odłączanie wątków
Do Androida 2.0 (Eclair) nie można było używać interfejsu
pthread_key_create
destruktora, aby uniknąć „odłączenia wątku przed wyjdź” sprawdzić. (czas wykonywania używa też funkcji destruktora klucza pthread, więc nie wiadomo, która funkcja zostanie wywołana jako pierwsza). - Słabe globalne odwołania
Do wersji 2.2 (Froyo) nie były wdrażane słabe odwołania globalne. Starsze wersje będą odrzucać próby ich użycia. Za pomocą wersji platformy Androida do testowania pod kątem obsługi.
Do Androida 4.0 (Ice Cream Sandwich) słabe globalne odniesienia mogły być być przekazywane do
NewLocalRef
,NewGlobalRef
iDeleteWeakGlobalRef
(Specyfikacja zdecydowanie zachęca programistów do tworzenia stałych odwołań do słabych zmiennych globalnych przed ich użyciem, więc nie powinno to w żaden sposób ograniczać możliwości).Od Androida 4.0 (Ice Cream Sandwich) można używać słabych odwołań globalnych jak innych odwołań JNI.
- Lokalne materiały referencyjne
Do Androida 4.0 (Ice Cream Sandwich) lokalne odniesienia były a nawet bezpośrednich wskaźników. Ice Cream Sandwich dodał pośrednictwo niezbędne do obsługi lepszych zbieraczy pamięci, ale oznacza to, że wiele błędów JNI jest niewykrywalne w starszych wersjach. Więcej informacji znajdziesz w artykule Zmiany dotyczące odwołań lokalnych JNI w pliku ICS.
Na urządzeniach z Androidem w wersjach starszych niż 8.0 liczba lokalnych odwołań jest ograniczona do limitu określonego dla wersji. Począwszy od Androida 8.0, Android obsługuje nieograniczoną liczbę lokalnych odwołań.
- Określanie typu pliku referencyjnego za pomocą parametru
GetObjectRefType
Do Androida 4.0 (Ice Cream Sandwich) w wyniku używania za pomocą bezpośrednich wskaźników (patrz wyżej), nie dało się wdrożyć
GetObjectRefType
. Zamiast tego zastosowaliśmy metodę heurystyczną, przeanalizował tabelę słabych globalnych światów, argumenty, miejscowych tabeli globalnych i tabeli globalnych w tej kolejności. po pierwszym odnalezieniu Twojego bezpośredni wskaźnik, wskaże, że Twój plik referencyjny jest typu być może. Oznaczało to na przykład, że jeśli wywołasz metodęGetObjectRefType
w globalnej klasie jclass, która jest taka sama jak klasa jclass przekazana jako domyślny argument do metody natywnej statycznej, otrzymasz wartośćJNILocalRefType
zamiastJNIGlobalRefType
. @FastNative
i@CriticalNative
Do Androida 7 te adnotacje optymalizacyjne były ignorowane. Niezgodność ABI dla
@CriticalNative
spowoduje nieprawidłowe sterowanie argumentami i prawdopodobnie doprowadzi do awarii.Dynamiczne wyszukiwanie funkcji natywnych w interfejsach
@FastNative
oraz W Androidzie 8–10 nie zaimplementowano@CriticalNative
metod i zawiera znane błędy w Androidzie 11. Korzystanie z tych optymalizacji bez wyraźnej rejestracji w JNIRegisterNatives
prawdopodobnie spowoduje awarie na Androidzie 8–11.FindClass
rzucaClassNotFoundException
Aby zapewnić zgodność wsteczną, Android zgłasza
ClassNotFoundException
zamiastNoClassDefFoundError
, gdy klasa nie zostanie znaleziona przezFindClass
To zachowanie jest zgodne z interfejsem Javy Referion API.Class.forName(name)
Najczęstsze pytania: dlaczego mam UnsatisfiedLinkError
?
Podczas pracy nad kodem natywnym często można napotkać taki błąd:
java.lang.UnsatisfiedLinkError: Library foo not found
W niektórych przypadkach oznacza to dokładnie to, co jest napisane – nie udało się znaleźć biblioteki. W innych przypadkach biblioteka istnieje, ale dlopen(3)
nie może jej otworzyć. Szczegóły błędu można znaleźć w wiadomości z wyjątkiem.
Najczęstsze przyczyny błędu „Nie znaleziono biblioteki” wyjątki:
- Biblioteka nie istnieje lub nie jest dostępna dla aplikacji. Użyj opcji
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.
Kolejna klasa błędów (UnsatisfiedLinkError
) wygląda tak:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
W narzędziu Logcat zobaczysz:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
Oznacza to, że środowisko wykonawcze próbowało znaleźć metodę dopasowania, ale niepowodzenie. Oto kilka najczęstszych przyczyn:
- Biblioteka się nie wczytuje. Sprawdź dane wyjściowe logcat pod kątem wiadomości o wczytywaniu biblioteki.
- Metoda nie została znaleziona z powodu niezgodności nazwy lub podpisu. Ten
jest często spowodowana przez:
- brak zadeklarowania funkcji w C++ w przypadku leniwego wyszukiwania metodą C++.
z parametrem
extern "C"
i odpowiednim widoczność (JNIEXPORT
). Pamiętaj, że przed makro JNIEXPORT jest nieprawidłowe, więc użycie nowego GCC z Staryjni.h
nie będzie działać. Możesz użyć usługiarm-eabi-nm
aby zobaczyć, jak wyglądają symbole w bibliotece; jeśli spojrzymy na zniekształcony (np._Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
) zamiastJava_Foo_myfunc
) lub jeśli typ symbolu to małe litery „t” zamiast wielkiego T, musisz i dostosować deklarację. - W przypadku jawnej rejestracji drobne błędy podczas wpisywania sygnatury metody. Upewnij się, że informacje przekazywane do
jest zgodne z podpisem w pliku dziennika.
Pamiętaj, że „B” to
byte
i „Z” jestboolean
. Składniki nazwy klasy w podpisach zaczynają się od „L”, a kończą na „;”, a do rozdzielania nazw pakietów i klas używa się znaku „/”, a do rozdzielania nazw wewnętrznych klas – znaku „$” (np.Ljava/util/Map$Entry;
).
- brak zadeklarowania funkcji w C++ w przypadku leniwego wyszukiwania metodą C++.
z parametrem
Użycie javah
do automatycznego generowania nagłówków JNI może pomóc
aby uniknąć problemów.
Najczęstsze pytania: dlaczego FindClass
nie znalazł(a) moich zajęć?
(Większość tych porad dotyczy równie dobrego przypadku, jeśli nie uda się znaleźć metod
z GetMethodID
lub GetStaticMethodID
albo polami
przy użyciu GetFieldID
lub GetStaticFieldID
).
Sprawdź, czy ciąg nazwy klasy ma prawidłowy format. Zajęcia JNI
zaczynają się od nazwy pakietu i są oddzielone ukośnikami,
na przykład java/lang/String
. Jeśli szukasz klasy tablicy,
musisz zacząć od odpowiedniej liczby nawiasów kwadratowych
musi też opakowywać klasę znakiem „L” i „;”, a więc jednowymiarowa tablica
String
to [Ljava/lang/String;
.
Jeśli szukasz informacji o klasie wewnętrznej, użyj „$” zamiast „.”. Ogólnie rzecz biorąc,
użycie funkcji javap
w pliku .class jest dobrym sposobem na sprawdzenie
wewnętrzną nazwę zajęć.
Jeśli włączysz kompresowanie kodu, skonfiguruj, który kod ma zostać zachowany. Konfiguruję bo w przeciwnym razie narzędzie zmniejszające kod może usunąć klasy, metody lub pola, które są używane tylko z JNI.
Jeśli nazwa klasy wygląda prawidłowo, możesz mieć problem z ładowaczem klas. FindClass
chce rozpocząć wyszukiwanie zajęć w
program ładujący klasy powiązany z kodem. Analizuje on stos wywołań,
który będzie wyglądał mniej więcej tak:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
Metoda znajdująca się na samej górze to Foo.myfunc
. FindClass
znajduje obiekt ClassLoader
powiązany z klasą Foo
i używa go.
Zwykle działa to tak, jak chcesz. Możesz mieć problemy, jeśli sam utworzysz wątek (np. wywołując funkcję pthread_create
, a następnie dołączając ją za pomocą funkcji AttachCurrentThread
). Teraz nie ma już ramek stosu z Twojej aplikacji.
Jeśli wywołasz funkcję FindClass
z tego wątku,
Oprogramowanie JavaVM uruchomi się w „systemie” Loader klasa zamiast powiązanego
z aplikacją, więc próby znalezienia klas dla danej aplikacji zakończą się niepowodzeniem.
Oto kilka sposobów, aby obejść ten problem:
- Przeprowadź wyszukiwanie
FindClass
raz – zaJNI_OnLoad
i zapisz w pamięci podręcznej odwołania do klas na później i ich używanie. Wszystkie wywołaniaFindClass
w ramach wykonywaniaJNI_OnLoad
będzie używać programu wczytującego klasy powiązanego z funkcję o nazwieSystem.loadLibrary
(to jest specjalną regułę, aby ułatwić inicjowanie biblioteki). Jeśli kod aplikacji wczytuje bibliotekę,FindClass
będzie używać odpowiedniego programu wczytującego klasy. - Przekaż instancję klasy do funkcji, które wymagają
przez zadeklarowanie metody natywnej dla argumentu Class,
i przekazanie
Foo.class
do źródła. - Umieść w pamięci podręcznej odwołanie do obiektu
ClassLoader
i wykonywać bezpośrednie połączenia (loadClass
). Wymaga to trochę wysiłku.
Najczęstsze pytania: jak udostępnić nieprzetworzone dane za pomocą kodu natywnego?
Możesz znaleźć się w sytuacji, w której będziesz potrzebować dostępu do dużego bufora danych nieprzetworzonych zarówno z kodu zarządzanego, jak i z kodu natywnego. Typowe przykłady obejmują manipulowanie bitmapami lub próbkami dźwięku. Dostępne są 2 podstawowych metod.
Dane możesz przechowywać w byte[]
. Pozwala to szybko
za pomocą kodu zarządzanego. W przypadku funkcji natywnych nie masz jednak gwarancji, że będziesz mieć dostęp do danych bez konieczności ich kopiowania. W
niektóre implementacje, GetByteArrayElements
i
GetPrimitiveArrayCritical
zwróci rzeczywiste wskaźniki do funkcji
nieprzetworzone dane w zarządzanej stercie, a w innych przydzielają bufor
na stercie natywnej i skopiuj dane.
Alternatywą jest przechowywanie danych w bezpośrednim buforze bajtów. Można je utworzyć za pomocą funkcji java.nio.ByteBuffer.allocateDirect
lub NewDirectByteBuffer
w JNI. W odróżnieniu od zwykłych buforów bajtów miejsce na dane nie jest przydzielane w ramach zarządzanego stosu i można do niego zawsze uzyskać bezpośredni dostęp z kodu natywnego (za pomocą funkcji GetDirectBufferAddress
). W zależności od tego, jak zaimplementowano bezpośredni dostęp do bufora bajtów, dostęp do danych z kodu zarządzanego może być bardzo powolny.
Ich wybór zależy od 2 czynników:
- Czy większość danych jest uzyskiwana za pomocą kodu napisanego w Javie? czy w C/C++?
- Jeśli dane będą w końcu przekazywane do systemowego interfejsu API, jaki formularz
musi znajdować się w środku? (Jeśli na przykład dane zostaną później przekazane do
funkcja, która przyjmuje bajt[], wykonuje przetwarzanie w bezpośrednim
ByteBuffer
może być niezrozumiały).
Jeśli nie ma jednoznacznego zwycięzcy, użyj bezpośredniego bufora bajtów. Pomoc dla nich jest wbudowana w JNI, a jego wydajność powinna poprawić się w kolejnych wersjach.