Wskazówki dotyczące JNI

JNI to natywny interfejs Java. Określa sposób, w jaki kod bajtowy skompilowany przez Androida z kodu zarządzanego (napisanego w językach programowania Java lub Kotlin) wchodzi w interakcję z kodem natywnym (napisanym w języku C/C++). JNI nie wymaga dostawców, obsługuje ładowanie kodu z dynamicznie współużytkowanych bibliotek i chociaż może być kłopotliwe, może być czasochłonne.

Uwaga: Android kompiluje Kotlin do kodu bajtowego przyjaznego dla ART w sposób podobny do języka programowania Java, dlatego wskazówki na tej stronie możesz zastosować zarówno do języków programowania Kotlin, jak i Java pod kątem architektury JNI i związanych z nią kosztów. Więcej informacji znajdziesz na stronie Kotlin i Androida.

Jeśli jeszcze nie znasz tej technologii, przeczytaj specyfikację natywnego interfejsu Java, aby zrozumieć, jak działa interfejs JNI i jakie funkcje są dostępne. Niektóre aspekty interfejsu nie są od razu oczywiste podczas pierwszego odczytu, więc może Ci się przydać kilka następnych sekcji.

Aby przeglądać globalne odwołania do JNI i sprawdzić, gdzie są tworzone i usuwane globalne odwołania do JNI, użyj widoku sterty JNI w programie profilu pamięci w Androidzie Studio 3.2 lub nowszym.

Wskazówki ogólne

Spróbuj zminimalizować ślady pracy warstwy JNI. Należy wziąć pod uwagę kilka aspektów. Rozwiązanie JNI powinno być zgodne z tymi wytycznymi (wymienionymi poniżej według ważności, zaczynając od najważniejszego):

  • Ogranicz organizowanie zasobów w warstwie JNI. Planowanie w warstwie JNI wiąże się z nieistotnymi kosztami. Postaraj się zaprojektować interfejs, który będzie miał jak najmniejszą ilość danych i ogranicza częstotliwość, z jaką musisz zbierać dane.
  • W miarę możliwości unikaj asynchronicznej komunikacji między kodem napisanym w zarządzanym języku programowania a kodem napisanym w C++. Pozwoli to łatwiej zarządzać interfejsem JNI. Zwykle możesz uprościć asynchroniczne aktualizacje interfejsu użytkownika, pozostawiając aktualizacje asynchroniczne w tym samym języku co interfejs. Na przykład zamiast wywoływać za pomocą JNI funkcję C++ z wątku interfejsu w kodzie Java, lepiej wykonać wywołanie zwrotne między dwoma wątkami w języku programowania Java, przy czym jeden z nich wykonuje blokujące wywołanie C++, a następnie powiadamia wątek interfejsu o zakończeniu wywołania blokującego.
  • Zminimalizuj liczbę wątków, które muszą zostać dotknięte lub dotknięte przez JNI. Jeśli musisz używać pul wątków zarówno w języku Java, jak i C++, staraj się utrzymywać komunikację JNI między właścicielami pul, a nie między poszczególnymi wątkami instancji roboczych.
  • Umieść kod interfejsu w niewielkiej liczbie łatwych do zidentyfikowania lokalizacji źródłowych w językach C++ i Java, aby ułatwić dalszą refaktoryzację. Rozważ użycie biblioteki automatycznego generowania JNI.

JavaVM i JNIEnv

JNI definiuje 2 kluczowe struktury danych: „JavaVM” i „JNIEnv”. Zasadniczo oba te elementy są wskaźnikami do tabel funkcji. (W wersji C++ są to klasy ze wskaźnikiem tabeli funkcji i elementami składowymi każdej funkcji JNI, która pośrednio przez tabelę). JavaVM udostępnia funkcje „interfejsu wywołania”, które umożliwiają tworzenie i niszczenie maszyny JavaVM. Teoretycznie można mieć wiele maszyn JavaVM na każdy proces, a Android dopuszcza tylko jedną.

JNIEnv zapewnia większość funkcji JNI. Wszystkie funkcje natywne otrzymują jako pierwszy argument JNIEnv z wyjątkiem metod @CriticalNative. Zobacz szybsze wywołania natywne.

JNIEnv służy do obsługi lokalnej pamięci w wątku. Z tego powodu nie można udostępniać JNIEnv między wątkami. Jeśli za pomocą jakiegoś fragmentu kodu nie ma innego sposobu uzyskania JNIEnv, udostępnij kod JavaVM i użyj GetEnv, aby znaleźć JNIEnv wątku. (Przy założeniu, że jest dostępna – patrz AttachCurrentThread poniżej).

Deklaracje C JNIEnv i JavaVM różnią się od deklaracji C++. Plik dołączany "jni.h" zawiera różne typy plików typedefs w zależności od tego, czy znajduje się w języku C czy C++. Z tego powodu nie warto dołączać argumentów JNIEnv w plikach nagłówka w obu językach. Innymi słowy, jeśli plik nagłówka wymaga #ifdef __cplusplus, być może musisz wykonać dodatkową pracę, jeśli któraś w tym nagłówku odwołuje się do JNIEnv.

Wątki

Wszystkie wątki to wątki systemu Linux zaplanowane przez jądro. Zwykle rozpoczyna się od kodu zarządzanego (za pomocą Thread.start()), ale można je utworzyć w innym miejscu i połączyć z elementem JavaVM. Na przykład wątek, który rozpoczął się od pthread_create() lub std::thread, można dołączyć za pomocą funkcji AttachCurrentThread() lub AttachCurrentThreadAsDaemon(). Dopóki wątek nie zostanie podłączony, nie będzie on miał JNIEnv i nie będzie można wykonywać wywołań JNI.

Zwykle najlepiej jest użyć Thread.start() do utworzenia dowolnego wątku, który musi wywoływać kod Java. Dzięki temu będziesz mieć pewność, że masz wystarczającą ilość miejsca na stosie, pracujesz we właściwym elemencie ThreadGroup i że używasz tego samego pola ClassLoader co w kodzie Java. Łatwiej ustawić nazwę wątku na potrzeby debugowania w języku Java niż w kodzie natywnym (sprawdź pthread_setname_np(), jeśli masz pthread_t lub thread_t oraz 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 „głównego” elementu ThreadGroup, dzięki czemu jest on widoczny dla debugera. Nie można użyć wywołania AttachCurrentThread() w już załączonym wątku.

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

Wątki dołączone przez JNI muszą wywoływać metodę DetachCurrentThread() przed zakończeniem. Jeśli kodowanie tego bezpośrednio jest niezręczne, w Androidzie 2.0 (Eclair) i nowszych możesz użyć pthread_key_create(), aby zdefiniować funkcję destruktora, która zostanie wywołana przed zakończeniem wątku, a następnie wywołaj DetachCurrentThread(). (Użyj tego klucza wraz z pthread_setspecific(), aby zapisać JNIEnv w wątku-local-storage. Dzięki temu zostaną one przekazane do destruktora jako argument.

jclass, jmethodID i jfieldID

Jeśli chcesz uzyskać dostęp do pola obiektu w kodzie natywnym, wykonaj te czynności:

  • Pobierz odwołanie do obiektu klasy za pomocą polecenia FindClass
  • Uzyskaj identyfikator pola za pomocą GetFieldID
  • Pobierz odpowiednią zawartość pola, np. GetIntField

Podobnie, aby wywołać metodę, należy najpierw uzyskać odwołanie do obiektu klasy, a następnie identyfikator metody. Identyfikatory są często tylko wskaźnikami do wewnętrznych struktur danych środowiska wykonawczego. Wyszukiwanie ich może wymagać kilku porównań ciągów znaków, ale gdy już je otrzymasz, wywołanie metody umożliwiające pobranie pola lub wywołanie metody przebiega bardzo szybko.

Jeśli zależy Ci na wydajności, warto raz przejrzeć wartości i zapisać wyniki w pamięci podręcznej w kodzie natywnym. Ponieważ obowiązuje limit 1 maszyny wirtualnej na proces, warto przechowywać te dane w statycznej strukturze lokalnej.

Odwołania do klas oraz identyfikatory pól i metod są ważne do momentu wyładowania klasy. Klasy są wyładowywane tylko wtedy, gdy wszystkie klasy powiązane z ClassLoader mogą zostać usunięte, co jest rzadkie, ale nie jest możliwe na Androidzie. Pamiętaj jednak, że jclass jest odwołaniem do klasy i musi być zabezpieczony wywołaniem NewGlobalRef (zobacz następną sekcję).

Jeśli chcesz zapisywać identyfikatory w pamięci podręcznej po załadowaniu klasy i automatycznie ponownie je buforować, gdy klasa zostanie kiedykolwiek wyładowana i ponownie załadowana, poprawnym sposobem na zainicjowanie identyfikatorów jest dodanie do odpowiedniej klasy fragmentu kodu podobnego do poniższego:

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 po zainicjowaniu klasy. Jeśli klasa zostanie kiedykolwiek wyładowana i załadowana ponownie, zostanie wykonana jeszcze raz.

Odwołania lokalne i globalne

Każdy argument przekazany do metody natywnej i niemal każdy obiekt zwrócony przez funkcję JNI jest „odwołaniem lokalnym”. Oznacza to, że obowiązuje ona przez czas trwania bieżącej metody natywnej w bieżącym wątku. Nawet jeśli po zwróceniu metody natywnej obiekt nadal będzie żył, odwołanie jest nieprawidłowe.

Dotyczy to wszystkich podklas zajęć jobject, w tym jclass, jstring i jarray. Gdy włączone są rozszerzone testy JNI, środowisko wykonawcze będzie ostrzegać o większości nadużyć dotyczących plików referencyjnych.

Jedynym sposobem na uzyskanie odwołań nielokalnych jest użycie funkcji NewGlobalRef i NewWeakGlobalRef.

Jeśli chcesz przechowywać plik referencyjny przez dłuższy czas, użyj odwołania „globalnego”. Funkcja NewGlobalRef przyjmuje odwołanie lokalne jako argument i zwraca globalne. Globalne plik referencyjny jest ważne, dopóki nie wywołasz funkcji DeleteGlobalRef.

Ten wzorzec jest zwykle używany do buforowania klasy jclass zwróconej 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 z kolejnych wywołań funkcji NewGlobalRef w tym samym obiekcie 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 plików referencyjnych z elementem == w kodzie natywnym.

Jedną z nich jest to, że w kodzie natywnym nie możesz zakładać, że odwołania do obiektów są stałe lub niepowtarzalne. Wartość reprezentująca obiekt może się różnić od jednego wywołania metody do następnego. Może się też zdarzyć, że 2 różne obiekty będą miały tę samą wartość w kolejnych wywołaniach. Nie używaj wartości jobject jako kluczy.

Programiści są zobowiązani do „nieprzydzielania zbyt wielu” odwołań lokalnych. W praktyce oznacza to, że jeśli tworzysz dużą liczbę odwołań lokalnych, np. gdy korzystasz z tablicy obiektów, musisz zwolnić je ręcznie za pomocą DeleteLocalRef, zamiast pozwolić JNI zrobić to za Ciebie. Implementacja jest wymagana jedynie do zarezerwowania przedziałów na 16 lokalnych plików referencyjnych. Jeśli potrzebujesz ich więcej, usuń część plików lub zarezerwuj więcej, korzystając z instrukcji EnsureLocalCapacity/PushLocalFrame.

Pamiętaj, że elementy jfieldID i jmethodID są nieprzejrzyste, nie odnoszą się do obiektów i nie należy ich przekazywać do NewGlobalRef. Wskaźniki nieprzetworzonych danych 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 odpowiedniego wywołania wersji.

O jednym nietypowym przypadku warto wspomnieć oddzielnie. Jeśli dołączysz wątek natywny za pomocą AttachCurrentThread, uruchomiony kod nigdy nie zwolni automatycznie odwołań lokalnych, dopóki wątek nie zostanie odłączony. Wszelkie utworzone lokalne odniesienia należy usunąć ręcznie. Ogólnie rzecz biorąc, każdy kod natywny, który tworzy w pętli lokalne odniesienia, prawdopodobnie wymaga usunięcia ręcznego.

Zachowaj ostrożność podczas korzystania z odwołań globalnych. Odniesienia globalne mogą być nieuniknione, ale są trudne do debugowania i mogą powodować trudne do zdiagnozowania (nieprawidłowe) zachowanie pamięci. Jeśli reszta jest taka sama, lepsze rozwiązanie z mniejszą liczbą odniesień globalnych jest prawdopodobnie lepsze.

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

W języku programowania Java stosuje się kodowanie UTF-16. Dla wygody JNI udostępnia metody, które działają również ze zmodyfikowanym kodowaniem UTF-8. Zmodyfikowane kodowanie jest przydatne w przypadku kodu C, ponieważ zakoduje \u0000 jako 0xc0 0x80 zamiast 0x00. Zaletą jest to, że ciąg znaków w stylu C zawiera ciągi znaków o zakończeniu zerowym, które nadają się do stosowania ze standardowymi funkcjami tekstowymi libc. Wadą jest brak możliwości przekazywania dowolnych danych UTF-8 do JNI i oczekiwanie, że będą one działać prawidłowo.

Aby uzyskać kod String zgodny z kodem UTF-16, użyj GetStringChars. Pamiętaj, że ciągi znaków UTF-16 nie mają zerowej zakończenia i są dozwolone, więc musisz uwzględnić zarówno długość ciągu znaków, jak i wskaźnika jchar.

Nie zapomnij Release ciągu znaków Get. Funkcje ciągów znaków zwracają wartość jchar* lub jbyte*, która jest wskaźnikami typu C do danych podstawowych, a nie odwołań lokalnych. Gwarantujemy ich ważność do czasu wywołania Release, co oznacza, że nie są zwalniane po zwróceniu metody natywnej.

Dane przekazywane do NewStringUTF muszą mieć zmodyfikowany format UTF-8. Częstym błędem jest odczytywanie danych znaków z pliku lub strumienia sieciowego i przekazywanie ich do NewStringUTF bez ich filtrowania. Jeśli nie wiesz, że dane to prawidłowy kod MUTF-8 (lub 7-bitowy kod ASCII, jest zgodny z podzbiorem), musisz usunąć nieprawidłowe znaki lub przekonwertować je do prawidłowego zmodyfikowanego formatu UTF-8. Jeśli tego nie zrobisz, konwersja UTF-16 może przynieść nieoczekiwane rezultaty. Narzędzie CheckJNI – domyślnie włączone w przypadku emulatorów – skanuje ciągi tekstowe i przerywa działanie maszyny wirtualnej w przypadku otrzymania nieprawidłowych danych wejściowych.

Przed Androidem 8 korzystanie z ciągów znaków UTF-16 było zwykle szybsze, ponieważ Android nie wymagał kopiowania w GetStringChars, natomiast GetStringUTFChars wymagał przydziału i konwersji na UTF-8. W Androidzie 8 zmieniono reprezentację String, aby w ciągach ASCII występujących 8 bitów używać 8 bitów na znak (aby oszczędzać pamięć), i zaczęto korzystać z przenoszonego modułu do czyszczenia pamięci. Te funkcje znacznie zmniejszają liczbę przypadków, w których ART może podać wskaźnik do danych String bez tworzenia kopii, nawet w przypadku elementu GetStringCritical. Jeśli jednak większość ciągów tekstowych przetwarzanych przez kod jest krótki, w większości przypadków można uniknąć alokacji i transakcji, używając bufora przydzielonego do stosu oraz funkcji GetStringRegion lub GetStringUTFRegion. Na przykład:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

Tablice podstawowe

JNI zapewnia funkcje umożliwiające dostęp do zawartości obiektów tablicy. Do tablic obiektów należy uzyskiwać dostęp tylko po 1 wpisie naraz, ale tablice obiektów podstawowych można odczytywać i zapisywać bezpośrednio tak, jakby zostały zadeklarowane w C.

Aby interfejs był jak najwydajniejszy bez ograniczania implementacji maszyny wirtualnej, rodzina Get<PrimitiveType>ArrayElements wywołań umożliwia środowisku wykonawczemu zwrócenie wskaźnika do rzeczywistych elementów lub przydzielenie pewnej pamięci i wykonanie kopii. W każdym przypadku zwrócony nieprzetworzony wskaźnik jest gwarantowany do momentu wykonania 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 kompaktowania sterty). Musisz wykonać Release na każdej tablicy Get. Jeśli wywołanie Get nie powiedzie się, musisz dopilnować, aby kod nie próbował później wskazywać wartości Release na wartość NULL.

Aby sprawdzić, czy dane zostały skopiowane, przekaż do argumentu isCopy wskaźnik inny niż NULL. Jest to rzadko przydatne.

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
    • Rzeczywiście: obiekt tablicy nie jest przypięty.
    • Kopiowanie: dane są kopiowane. Bufor z kopią zostanie zwolniony.
  • JNI_COMMIT
    • Rzeczywiście: nic nie robi.
    • Kopiowanie: dane są kopiowane. Bufor z kopią nie jest zwolniony.
  • JNI_ABORT
    • Rzeczywiście: obiekt tablicy nie jest przypięty. Wcześniejsze zapisy nie są przerywane.
    • Kopia: bufor z kopią zostaje zwolniony, a wszelkie zmiany w niej zostaną utracone.

Jednym z powodów, dla których warto sprawdzić flagę isCopy, jest to, czy po wprowadzeniu zmian w tablicy trzeba wywołać metodę Release z JNI_COMMIT. Jeśli na przemian wprowadzasz zmiany i uruchamiasz kod, który korzysta z zawartości tablicy, być może uda Ci się pominąć zatwierdzenie no-op. Inną możliwą przyczyną sprawdzania flagi jest wydajna obsługa atrybutu JNI_ABORT. Na przykład możesz pobrać tablicę, zmodyfikować ją w odpowiednim miejscu, przekazać fragmenty do innych funkcji, a potem odrzucić zmiany. Jeśli wiesz, że JNI tworzy dla Ciebie nową kopię, nie musisz tworzyć kolejnej kopii „edytowalnej”. Jeśli JNI przekazuje oryginał, musisz zrobić własną kopię.

Częstym błędem (powtarzanym w przykładowym kodzie) jest zakładanie, że można pominąć wywołanie Release, jeśli parametr *isCopy ma wartość fałsz. To nieprawda. Jeśli nie przydzielono żadnego bufora kopii, pierwotna pamięć musi być przypięta i nie może jej przenosić przez moduł czyszczenia pamięci.

Pamiętaj też, że flaga JNI_COMMIT nie zwalnia tablicy i w końcu trzeba będzie ponownie wywołać funkcję Release z inną flagą.

Połączenia regionalne

Istnieje alternatywa dla wywołań takich jak Get<Type>ArrayElements i GetStringChars. Może się to okazać bardzo pomocne, gdy chcesz jedynie 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, skopiowanie z niej pierwszych len bajtów, a następnie zwolnienie tablicy. W zależności od implementacji wywołanie Get spowoduje przypięcie lub skopiowanie zawartości tablicy. Kod kopiuje dane (być może po raz drugi), a potem wywołuje metodę Release. W tym przypadku JNI_ABORT dba o uniknięcie trzeciej kopii.

To samo można zrobić prościej:

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

Ma to kilka zalet:

  • Wymaga 1 połączenia JNI zamiast 2, co zmniejsza nakłady pracy.
  • Nie wymaga przypinania danych ani dodatkowych kopii danych.
  • Zmniejsza ryzyko błędów programisty – nie trzeba zapomnieć o wywołaniu funkcji Release w przypadku awarii.

W podobny sposób 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 w trakcie oczekiwania na 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ługiwać.

Jedyne funkcje JNI, które możesz wywoływać w czasie oczekiwania na wyjątek, to:

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

Wiele wywołań JNI może powodować wyjątek, ale często stanowi prostszy sposób sprawdzania awarii. Jeśli np. NewString zwraca wartość inną niż NULL, nie musisz sprawdzać wyjątku. Jeśli jednak wywołujesz metodę (za pomocą funkcji takiej jak CallObjectMethod), zawsze musisz sprawdzić wyjątek, bo po zgłoszeniu wyjątku wartość zwracana nie będzie prawidłowa.

Pamiętaj, że wyjątki zgłaszane przez kod zarządzany nie uruchamiają natywnych ramek stosu. (W przypadku Androida odradzamy stosowanie wyjątków C++ w granicach przejścia JNI z kodu C++ do kodu zarządzanego). Instrukcje JNI Throw i ThrowNew ustawiły wskaźnik wyjątku w bieżącym wątku. Gdy wrócisz do funkcji zarządzanego z poziomu kodu natywnego, wyjątek zostanie odnotowany i odpowiednio przetworzony.

Kod natywny może „przechwycić” wyjątek, wywołując ExceptionCheck lub ExceptionOccurred i wyczyścić go za pomocą ExceptionClear. Jak zwykle odrzucenie wyjątków bez ich obsługi może spowodować problemy.

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

Sprawdzanie rozszerzone

JNI wykonuje bardzo mało kontroli błędów. Błędy zwykle prowadzą do awarii. Android oferuje również tryb o nazwie CheckJNI, w którym wskaźniki tabeli funkcji JavaVM i JNIEnv są przełączane na tabele funkcji, które wykonują rozszerzone testy przed wywołaniem standardowej implementacji.

Są one dodatkowe:

  • Tablice: próba przydzielenia tablicy o ujemnym rozmiarze.
  • Złe wskaźniki: przekazanie do wywołania JNI nieprawidłowego pliku jarray/jclass/jobject/jstring lub przekazanie wskaźnika NULL do wywołania JNI z argumentem niedopuszczającym wartości null.
  • Nazwy klas: przekazywanie do wywołania JNI wszystkiego poza stylem „java/lang/String” nazwy klasy.
  • Wywołania krytyczne: wykonywanie wywołania JNI między „krytycznym” pobieraniem a odpowiednią wersją.
  • Bezpośrednie bufory bajtów: przekazywanie nieprawidłowych argumentów do funkcji NewDirectByteBuffer.
  • Wyjątki: wykonywanie połączenia JNI w trakcie oczekiwania na wyjątek.
  • JNIEnv*s: użycie JNIEnv* z niewłaściwego wątku.
  • jfieldIDs: użycie identyfikatora jfieldID z wartością NULL, ustawienie jfieldID na wartość nieprawidłowego typu (np. próba przypisania obiektu StringBuilder do pola String) lub użycie jfieldID do ustawienia pola instancji statycznej (lub na odwrót) bądź użycie identyfikatora jfieldID z jednej klasy z wystąpieniami innej klasy.
  • jmethodIDs: użycie nieprawidłowego rodzaju identyfikatora jmethodID podczas wykonywania wywołania JNI Call*Method: nieprawidłowy typ zwracany, niezgodność statyczna/niestatyczna, nieprawidłowy typ dla „this” (dla wywołań niestatycznych) lub zła klasa (w przypadku wywołań statycznych).
  • Pliki referencyjne: użycie DeleteGlobalRef/DeleteLocalRef w przypadku nieprawidłowego rodzaju pliku referencyjnego.
  • Tryby wydania: przekazywanie nieprawidłowego trybu wydania do wywołania wersji (innej niż 0, JNI_ABORT lub JNI_COMMIT).
  • Type safety: zwracanie niezgodnego typu z metody natywnej (zwracanie obiektu StringBuilder z metody zadeklarowanej w celu zwrócenia ciągu znaków).
  • UTF-8: przekazywanie do wywołania JNI nieprawidłowej sekwencji bajtów zmodyfikowanej sekwencji UTF-8.

(Ułatwienia dostępu dotyczące metod i pól nadal nie są zaznaczone: ograniczenia dostępu nie dotyczą kodu natywnego).

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

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

Jeśli masz urządzenie z dostępem do roota, możesz ponownie uruchomić środowisko wykonawcze z włączoną funkcją CheckJNI, korzystając z poniższych sekwencji poleceń:

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

W każdym z tych przypadków po uruchomieniu środowiska wykonawczego w danych wyjściowych logcat pojawi się 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 będzie to miało wpływu 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ą wartość lub zrestartowanie spowoduje ponowne wyłączenie CheckJNI). W takim przypadku przy kolejnym uruchomieniu aplikacji w danych wyjściowych logcat pojawi się coś takiego:

D Late-enabling CheckJNI

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

Biblioteki natywne

Kod natywny z bibliotek udostępnionych możesz wczytać przy użyciu standardowego środowiska System.loadLibrary.

W starszych wersjach Androida występowały błędy w narzędziu PackageManager, które powodowały nieprawidłowe instalowanie i aktualizowanie bibliotek natywnych. Projekt ReLinker zawiera obejścia tego i innych problemów z wczytywaniem biblioteki natywnej.

Wywołaj System.loadLibrary (lub ReLinker.loadLibrary) ze statycznego inicjatora klasy. Argument to „nierozdzielczona” nazwa biblioteki, więc wczytanie elementu libfubar.so przekazane w funkcji "fubar" jest konieczne.

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

Środowisko wykonawcze może znaleźć metody natywne na 2 sposoby. Możesz zarejestrować je jawnie w usłudze RegisterNatives lub pozwolić, aby środowisko wykonawcze mogło je dynamicznie wyszukiwać za pomocą dlsym. Zaletą RegisterNatives jest to, że od razu sprawdzisz, czy symbole istnieją, a jeśli chcesz mieć mniejsze i szybsze biblioteki udostępnione, nie eksportujesz niczego oprócz JNI_OnLoad. Dzięki temu, że środowisko wykonawcze wykrywa Twoje funkcje, wymaga nieco mniej kodu do pisania.

Aby użyć aplikacji RegisterNatives:

  • Podaj funkcję JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved).
  • W JNI_OnLoad zarejestruj wszystkie metody natywne za pomocą RegisterNatives.
  • Kompiluj za pomocą -fvisibility=hidden, aby z biblioteki eksportowane było tylko JNI_OnLoad. Pozwala to generować szybszy i mniejszy kod oraz uniknąć potencjalnych kolizji z innymi bibliotekami wczytywanymi do aplikacji (ale tworzy mniej przydatne zrzuty stosu, jeśli aplikacja ulegnie awarii w kodzie natywnym).

Statyczny inicjator powinien wyglądać tak:

Kotlin

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

Java

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

Jeśli zostanie zapisana w C++, funkcja JNI_OnLoad 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;
}

Aby zamiast tego używać metody „discovery” (odkrywanie), musisz nadać im określoną nazwę (szczegóły znajdziesz w specyfikacji JNI). Oznacza to, że jeśli podpis metody jest nieprawidłowy, nie dowiesz się o nim, dopóki metoda nie zostanie wywołana po raz pierwszy.

Wszystkie wywołania FindClass wykonane z poziomu JNI_OnLoad zakończą obsługę zajęć w kontekście modułu ładowania zajęć, który został użyty do wczytania biblioteki udostępnionej. W przypadku wywołania z innych kontekstów FindClass używa modułu ładowania klas powiązanego z metodą znajdującą się na górze stosu Java, a jeśli takiego nie ma (ponieważ pochodzi z podłączonego właśnie wątku natywnego), korzysta z systemu wczytującego klasy „systemowe”. Moduł wczytujący klasę systemową nie wie o klasach aplikacji, więc nie możesz wyszukać własnych klas z użyciem funkcji FindClass w tym kontekście. Dzięki temu JNI_OnLoad jest wygodnym miejscem do wyszukiwania i buforowania klas. Gdy masz prawidłowe odwołanie globalne jclass, możesz go użyć z dowolnego dołączonego wątku.

Szybsze wywołania natywne dzięki @FastNative i @CriticalNative

Do metod natywnych możesz dodać adnotacje @FastNative lub @CriticalNative (ale nie obiema to jednocześnie), aby przyspieszyć przejście między kodem zarządzanym a kodem natywnym. Jednak korzystanie z takich adnotacji wiąże się z pewnymi zmianami w zachowaniu, które należy dokładnie przemyśleć. O tych zmianach wspominamy w skrócie poniżej, ale szczegółowe informacje można znaleźć w dokumentacji.

Adnotację @CriticalNative można stosować tylko do metod natywnych, które nie używają obiektów zarządzanych (w parametrach lub zwracanych wartościach albo jako niejawna this), a ta adnotacja zmienia interfejs ABI przejścia JNI. Implementacja natywna musi wykluczać parametry JNIEnv i jclass ze podpisu funkcji.

Podczas wykonywania metody @FastNative lub @CriticalNative funkcja czyszczenia pamięci nie może zawiesić wątku na potrzeby niezbędnych zadań i może zostać zablokowana. Nie używaj tych adnotacji w przypadku długotrwałych metod, w tym metod zwykle szybkich, ale zasadniczo nieobjętych ograniczeniami. W szczególności kod nie powinien wykonywać znaczących operacji wejścia-wyjścia ani tworzyć blokad natywnych, które mogą być utrzymywane na dłuższy czas.

Te adnotacje były wdrażane w systemie od Androida 8, a w Androidzie 14 stały się publicznymi interfejsami API testowanymi przez CTS. Te optymalizacje prawdopodobnie będą działać również na urządzeniach z Androidem 8–13 (chociaż bez mocnych gwarancji CTS), ale dynamiczne wyszukiwanie metod natywnych jest obsługiwane tylko w Androidzie 12 i nowszych. W przypadku Androida w wersji 8–11 bezwzględnie wymagana jest jawna rejestracja w JNI RegisterNatives. Te adnotacje są ignorowane w Androidzie 7. Niezgodność interfejsu ABI w @CriticalNative prowadzi do niewłaściwej organizacji argumentów i prawdopodobnych awarii.

W przypadku metod, które wymagają tych adnotacji, zalecamy bezpośrednie zarejestrowanie tych metod za pomocą JNI RegisterNatives, zamiast korzystać z „wykrywania” opartego na nazwie metod natywnych. Aby uzyskać optymalną wydajność uruchamiania aplikacji, zalecamy uwzględnienie obiektów wywołujących metody @FastNative lub @CriticalNative w profilu bazowym. Od Androida 12 wywołanie metody natywnej @CriticalNative ze skompilowanej metody zarządzanej jest prawie tak samo tanie, jak wywołanie niewbudowane w C/C++, pod warunkiem że wszystkie argumenty pasują do rejestrów (np. do 8 całek i 8 argumentów zmiennoprzecinkowych w armii Arm64).

Czasami korzystne jest podzielenie metody natywnej na 2 metody – bardzo szybką, która może zawieść, i drugą, która obsługuje powolne 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);

Uwagi na temat wersji 64-bitowej

Aby obsługiwać architektury korzystające ze wskaźników 64-bitowych, do przechowywania wskaźnika do struktury natywnej w polu Javy używaj pola long zamiast int.

Nieobsługiwane funkcje lub zgodność wsteczna

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

  • Element DefineClass nie został zaimplementowany. Android nie używa kodu bajtowego Java ani plików klas, więc przekazywanie danych klas binarnych nie działa.

Aby uzyskać zgodność wsteczną ze starszymi wersjami Androida, może być konieczne zapoznanie się z tymi informacjami:

  • Dynamiczne wyszukiwanie funkcji natywnych

    Do wersji Androida 2.0 (Eclair) znak „$” nie był prawidłowo konwertowany na „_00024” podczas wyszukiwania nazw metod. Rozwiązanie tego problemu wymaga użycia wyraźnej rejestracji lub przeniesienia metod natywnych z klas wewnętrznych.

  • Odłączanie wątków

    Do Androida 2.0 (Eclair) nie można było używać funkcji niszczyciela pthread_key_create w celu uniknięcia sprawdzenia, czy „wątek musi zostać odłączony przed zakończeniem”. (Środowisko wykonawcze wykorzystuje również funkcję niszczyciela kluczy Pthread, więc trwa wyścig, aby zobaczyć, który z nich zostaje wywołany jako pierwszy).

  • Słabe odwołania globalne

    Do wersji Androida 2.2 (Froyo) słabe odniesienia globalne nie były stosowane. Starsze wersje energicznie odrzucają próby ich użycia. Do testowania możesz używać stałych wersji platformy Androida.

    Do wersji Androida 4.0 (Ice Cream Sandwich) słabe odwołania globalne można przekazywać tylko do interfejsów NewLocalRef, NewGlobalRef i DeleteWeakGlobalRef. Ta specyfikacja zdecydowanie zachęca programistów do tworzenia trudnych odniesień do słabych globalnych ustawień przed wykonaniem jakichkolwiek czynności z nimi, więc nie powinno to w ogóle ograniczać.

    Począwszy od Androida 4.0 (Ice Cream Sandwich) słabe odniesienia globalne można używać tak jak wszelkich innych odwołań do JNI.

  • Lokalne odniesienia

    Do Androida w wersji 4.0 (Ice Cream Sandwich) odwołania lokalne były tak naprawdę bezpośrednimi wskaźnikami. Ice Cream Sandwich dodał usługę pośrednią niezbędną do obsługi lepszych modułów do pobierania śmieci. Oznacza to jednak, że w starszych wersjach nie można wykryć wielu błędów JNI. Więcej informacji znajdziesz w artykule o zmianach dotyczących lokalnych plików referencyjnych JNI w ICS.

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

  • Określanie typu pliku referencyjnego za pomocą funkcji GetObjectRefType

    Do momentu używania Androida w wersji 4.0 (Ice Cream Sandwich) prawidłowe wdrożenie funkcji GetObjectRefType nie było możliwe ze względu na używanie bezpośrednich wskaźników (patrz wyżej). Zamiast tego korzystamy z metody heurystycznej, która analizuje tabelę słabych globalnych tabel, argumenty, tabelę lokalnych oraz tabelę globalnych w tej kolejności. Gdy po raz pierwszy znajdzie Twój bezpośredni wskaźnik, stwierdzi, że plik referencyjny jest tego typu, które dokładnie sprawdza. Oznaczało to na przykład, że jeśli wywołasz GetObjectRefType w globalnej klasie jclass, która jest taka sama jak jclass przekazywana jako niejawny argument dla statycznej metody natywnej, otrzymasz JNILocalRefType zamiast JNIGlobalRefType.

  • @FastNative i @CriticalNative

    Do Androida 7 te adnotacje dotyczące optymalizacji były ignorowane. Niezgodność interfejsu ABI dla interfejsu @CriticalNative prowadzi do niewłaściwej organizacji argumentów i prawdopodobnych awarii.

    Dynamiczne wyszukiwanie funkcji natywnych dla metod @FastNative i @CriticalNative nie zostało wdrożone na Androidzie 8–10 i zawiera znane błędy w Androidzie 11. Korzystanie z tych optymalizacji bez wyraźnej rejestracji w JNI RegisterNatives może spowodować awarie na Androidzie 8–11.

Najczęstsze pytania: dlaczego otrzymuję UnsatisfiedLinkError?

Podczas pracy z kodem natywnym często występują takie błędy:

java.lang.UnsatisfiedLinkError: Library foo not found

W niektórych przypadkach oznacza to, że tak jest – nie znaleziono biblioteki. W innych przypadkach biblioteka istnieje, ale nie udało się jej otworzyć przez dlopen(3). Szczegółowe informacje o błędzie znajdziesz w szczegółach wyjątku.

Najczęstsze przyczyny występowania wyjątków „nie znaleziono biblioteki”:

  • Biblioteka nie istnieje lub jest niedostępna dla aplikacji. Użyj adb shell ls -l <path>, aby sprawdzić jej obecność i uprawnienia.
  • Ta biblioteka nie została utworzona z pakietu NDK. Może to prowadzić do 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 znajdziesz:

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

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

  • Biblioteka się nie wczytuje. Sprawdź w danych wyjściowych logcat komunikaty o wczytywaniu biblioteki.
  • Nie można znaleźć metody z powodu niezgodności nazwy lub podpisu. Zwykle jest to spowodowane przez:
    • W przypadku leniwego wyszukiwania metody występuje błędy w zadeklarowaniu funkcji C++ z właściwością extern "C" i odpowiednią widocznością (JNIEXPORT). Pamiętaj, że przed wprowadzeniem „Inie” Sandwich makro JNIEXPORT było nieprawidłowe, więc użycie nowego GCC ze starym jni.h nie będzie działać. Możesz użyć arm-eabi-nm, aby zobaczyć symbole w bibliotece. Jeśli są zniekształcone (np. _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass zamiast Java_Foo_myfunc) lub jeśli symbolem jest małe litery „nie” zamiast dużego „T”, musisz dostosować deklarację.
    • W przypadku wyraźnej rejestracji: drobne błędy przy wpisywaniu podpisu metody. Sprawdź, czy informacje przekazywane do wywołania rejestracji są zgodne z podpisem w pliku logu. Pamiętaj, że „B” to byte, a „Z” to boolean. Komponenty nazw klas w podpisach zaczynają się znakiem „L”, kończą się znakiem „;”, znakiem „/” oddzielającym nazwy pakietów/klas, a znakiem „$” oddzielaj nazwy klasy wewnętrznej (np. Ljava/util/Map$Entry;).

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

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

(Większość tych porad dotyczy też problemów ze znalezieniem metod za pomocą właściwości GetMethodID lub GetStaticMethodID oraz pól z wartością GetFieldID lub GetStaticFieldID).

Ciąg nazwy zajęć musi mieć prawidłowy format. Nazwy klas JNI zaczynają się od nazwy pakietu i są rozdzielone ukośnikami, np. java/lang/String. Jeśli szukasz klasy tablicy, musisz rozpocząć od odpowiedniej liczby nawiasów kwadratowych i opakować klasę znakami „L” i „;”, w związku z czym jednowymiarowa tablica String będzie miała postać [Ljava/lang/String;. Jeśli szukasz klasy wewnętrznej, użyj znaku „$” zamiast „.”. Ogólnie dobrym sposobem na znalezienie wewnętrznej nazwy klasy jest użycie javap w pliku .class.

Jeśli włączysz zmniejszanie kodu, pamiętaj, aby skonfigurować kod do zachowania. Skonfigurowanie prawidłowych reguł przechowywania jest ważne, ponieważ skrót kodu może w innym przypadku usunąć klasy, metody lub pola, które są używane tylko z JNI.

Jeśli nazwa klasy wygląda na prawidłową, może to oznaczać, że masz problem z wczytywaniem klas. FindClass chce rozpocząć wyszukiwanie zajęć w module ładowania zajęć powiązanym z Twoim kodem. Sprawdza stos wywołań, który będzie wyglądał mniej więcej tak:

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

Najwyższa metoda to Foo.myfunc. FindClass znajduje obiekt ClassLoader powiązany z klasą Foo i go używa.

Zwykle działa to tak, jak chcesz. Możesz mieć kłopoty, jeśli samodzielnie utworzysz wątek (np. wywołując pthread_create, a następnie dołączając go przy użyciu metody AttachCurrentThread). Teraz z aplikacji nie ma ramek stosu. Jeśli wywołasz funkcję FindClass z tego wątku, JavaVM uruchomi się w programie wczytującym klasy „system”, a nie w kodzie powiązanym z Twoją aplikacją. Próby znalezienia klas specyficznych dla aplikacji zakończą się niepowodzeniem.

Oto kilka sposobów na obejście tego problemu:

  • Przeprowadź wyszukiwanie FindClass raz w obiekcie JNI_OnLoad i zapisz odwołania do klas w pamięci podręcznej do późniejszego użycia. Wszystkie wywołania FindClass wykonywane w ramach wykonywania JNI_OnLoad będą używać modułu ładowania klas powiązanego z funkcją System.loadLibrary (jest to specjalna reguła, która ma ułatwić inicjowanie biblioteki). Jeśli kod aplikacji wczytuje bibliotekę, FindClass użyje odpowiedniego modułu ładowania klas.
  • Przekaż instancję klasy do funkcji, które jej potrzebują, deklarując metodę natywną przyjmującą argument klasy, a następnie przekazując Foo.class.
  • Przechowuj odniesienie do obiektu ClassLoader w pamięci podręcznej w jakimś miejscu i bezpośrednio wysyłaj wywołania loadClass. Wymaga to nieco wysiłku.

Najczęstsze pytania: jak udostępniać nieprzetworzone dane w kodzie natywnym?

Może się zdarzyć, że będziesz potrzebować dostępu do dużego bufora nieprzetworzonych danych zarówno w kodzie zarządzanym, jak i natywnym. Typowym przykładem jest manipulowanie mapami bitowymi lub próbkami dźwiękowymi. Można to zrobić na 2 podstawowe sposoby.

Dane możesz przechowywać w usłudze byte[]. Zapewnia to bardzo szybki dostęp z poziomu kodu zarządzanego. W przypadku technologii natywnej nie ma jednak gwarancji, że uzyskasz dostęp do danych bez konieczności ich kopiowania. W niektórych implementacjach GetByteArrayElements i GetPrimitiveArrayCritical zwracają rzeczywiste wskaźniki do nieprzetworzonych danych na stercie zarządzanej, ale w innych przydzieli bufor na stercie natywnej i kopiuje dane.

Alternatywnym jest przechowywanie danych w bezpośrednim buforze bajtów. Można je tworzyć za pomocą java.nio.ByteBuffer.allocateDirect lub funkcji JNI NewDirectByteBuffer. W przeciwieństwie do zwykłych buforów bajtów pamięć nie jest przydzielana na stercie zarządzanej i zawsze można uzyskać do niej dostęp bezpośrednio z kodu natywnego (za pomocą GetDirectBufferAddress). W zależności od tego, jak zaimplementowany jest bezpośredni dostęp do bufora bajtów, dostęp do danych z poziomu kodu zarządzanego może być bardzo powolny.

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

  1. Czy dostęp do danych jest uzyskiwany za pomocą kodu napisanego w Javie czy w C/C++?
  2. Jeśli dane będą ostatecznie przekazywane do systemowego interfejsu API, w jakiej formie muszą być? Jeśli na przykład dane są w końcu przekazywane do funkcji, która zajmuje bajt[], przetwarzanie bezpośrednio na poziomie ByteBuffer może być niewskazane.

Jeśli nie uda się wyłonić jednoznacznego zwycięzcy, użyj bufora bajtów. Ich obsługa jest wbudowana bezpośrednio w JNI, a wydajność powinna wzrosnąć w kolejnych wersjach.