JNI 도움말

JNI는 Java Native Interface(자바 네이티브 인터페이스)의 약어입니다. 이는 Android가 관리 코드 (자바 또는 Kotlin 프로그래밍 언어로 작성됨)에서 컴파일하여 네이티브 코드(C/C++로 작성됨)와 상호작용하는 방법을 정의합니다. JNI는 공급업체 중립적이며 동적 공유 라이브러리에서 코드를 로드할 수 있도록 지원하며 번거로운 작업도 가능합니다.

참고: Android는 자바 프로그래밍 언어와 비슷한 방식으로 Kotlin을 ART 친화적인 바이트 코드로 컴파일하므로 JNI 아키텍처 및 관련 비용 측면에서 이 페이지의 안내를 Kotlin과 자바 프로그래밍 언어 모두에 적용할 수 있습니다. 자세한 내용은 Kotlin 및 Android를 참고하세요.

잘 모르겠다면 자바 네이티브 인터페이스 사양을 참고하여 JNI의 작동 방식과 사용할 수 있는 기능을 알아보세요. 인터페이스의 일부 측면은 처음 읽을 때 바로 이해되지 않는 부분이 있으므로 다음 몇 가지 섹션이 도움이 될 수 있습니다.

글로벌 JNI 참조를 살펴보고 글로벌 JNI 참조가 생성 및 삭제된 위치를 확인하려면 Android 스튜디오 3.2 이상에서 메모리 프로파일러에서 JNI 힙 뷰를 사용합니다.

일반적인 도움말

JNI 레이어의 공간을 최소화합니다. 여기서 고려해야 할 몇 가지 측정기준이 있습니다. JNI 솔루션은 다음 가이드라인 (아래에 가장 중요한 것부터 중요도 순으로 나열됨)을 따라야 합니다.

  • JNI 레이어에서 리소스의 마샬링을 최소화합니다. JNI 레이어에서 마샬링하는 작업에는 적지 않은 비용이 발생합니다. 마셜링해야 하는 데이터의 양과 데이터를 마샬링해야 하는 빈도를 최소화하는 인터페이스를 설계해 보세요.
  • 가능한 경우 관리형 프로그래밍 언어로 작성된 코드와 C++로 작성된 코드 간에 비동기 통신을 사용하지 않습니다. 이렇게 하면 JNI 인터페이스를 더 쉽게 유지 관리할 수 있습니다. 일반적으로 비동기 업데이트를 UI와 동일한 언어로 유지하여 비동기 UI 업데이트를 단순화할 수 있습니다. 예를 들어 JNI를 통해 Java 코드의 UI 스레드에서 C++ 함수를 호출하는 대신 Java 프로그래밍 언어로 된 두 스레드 간에 콜백을 실행하는 것이 좋습니다. 스레드 중 하나에서 C++ 차단 호출을 실행한 다음 차단 호출이 완료되면 UI 스레드에 알립니다.
  • JNI를 터치하거나 터치해야 하는 스레드 수를 최소화합니다. 자바 언어와 C++ 언어 모두에서 스레드 풀을 활용해야 하는 경우 개별 작업자 스레드가 아닌 풀 소유자 간에 JNI 통신을 유지해 보세요.
  • 향후 리팩터링을 용이하게 하려면 쉽게 식별할 수 있는 소수의 C++ 및 자바 소스 위치에 인터페이스 코드를 유지합니다. 필요에 따라 JNI 자동 생성 라이브러리를 사용해 보세요.

JavaVM 및 JNIEnv

JNI는 두 개의 주요 데이터 구조인 'JavaVM'과 'JNIEnv'를 정의합니다. 둘 다 본질적으로 함수 테이블에 대한 포인터를 가리키는 포인터입니다. (C++ 버전에서는 함수 테이블에 대한 포인터 및 테이블을 통해 간접적으로 전달되는 각 JNI 함수의 멤버 함수가 있는 클래스입니다.) JavaVM은 JavaVM을 만들고 삭제할 수 있는 '호출 인터페이스' 함수를 제공합니다. 이론적으로는 프로세스당 여러 개의 JavaVM을 사용할 수 있지만 Android에서는 하나만 허용합니다.

JNIEnv는 대부분의 JNI 함수를 제공합니다. 네이티브 함수는 모두 JNIEnv를 첫 번째 인수로 수신합니다. 단, @CriticalNative 메서드는 예외입니다. 더 빠른 네이티브 호출을 참고하세요.

JNIEnv는 스레드 로컬 저장소에 사용됩니다. 따라서 스레드 간에 JNIEnv를 공유할 수 없습니다. 코드 조각에서 JNIEnv를 가져올 다른 방법이 없다면 JavaVM을 공유하고 GetEnv를 사용하여 스레드의 JNIEnv를 검색해야 합니다. 스레드에 JNIEnv가 있다고 가정합니다. 아래 AttachCurrentThread를 참조하세요.

JNIEnv 및 JavaVM의 C 선언은 C++ 선언과 다릅니다. "jni.h" include 파일은 C 또는 C++에 포함되는지에 따라 다른 typedef를 제공합니다. 따라서 두 언어에 모두 포함된 헤더 파일에 JNIEnv 인수를 포함하는 것은 좋지 않습니다. 즉, 헤더 파일에 #ifdef __cplusplus가 필요한 경우 이 헤더에 JNIEnv를 참조하는 항목이 있다면 추가 작업을 해야 할 수 있습니다.

Threads

모든 스레드는 커널을 통해 예약된 Linux 스레드입니다. 일반적으로 Thread.start()를 사용하여 관리 코드에서 시작되지만 다른 곳에서 만든 후 JavaVM에 연결할 수도 있습니다. 예를 들어 pthread_create() 또는 std::thread로 시작된 스레드는 AttachCurrentThread() 또는 AttachCurrentThreadAsDaemon() 함수를 사용하여 연결할 수 있습니다. 스레드가 연결될 때까지는 JNIEnv가 없으며 JNI를 호출할 수 없습니다.

일반적으로 자바 코드를 호출해야 하는 스레드를 만들려면 Thread.start()를 사용하는 것이 가장 좋습니다. 이렇게 하면 충분한 스택 공간이 있고, 올바른 ThreadGroup에 있고, 자바 코드와 동일한 ClassLoader를 사용할 수 있습니다. 또한 네이티브 코드보다 자바에서 디버깅할 스레드 이름을 설정하는 것이 더 쉽습니다. pthread_t 또는 thread_t가 있으면 pthread_setname_np()을 참고하고 std::thread이 있고 pthread_t를 원한다면 std::thread::native_handle()를 참고하세요.

네이티브에서 생성된 스레드를 연결하면 java.lang.Thread 객체가 생성되어 '기본' ThreadGroup에 추가되어 디버거에 표시됩니다. 이미 연결된 스레드에서는 AttachCurrentThread()을 호출해도 작동하지 않습니다.

Android는 네이티브 코드를 실행하는 스레드를 정지하지 않습니다. 가비지 컬렉션이 진행 중이거나 디버거에서 정지 요청을 실행한 경우 Android는 다음에 JNI 호출을 할 때 스레드를 일시중지합니다.

JNI를 통해 연결된 스레드는 종료하기 전에 DetachCurrentThread()를 호출해야 합니다. 이를 직접 코딩하는 것이 어렵다면 Android 2.0 (Eclair) 이상에서 pthread_key_create()를 사용하여 스레드가 종료되기 전에 호출할 소멸자 함수를 정의하고 이 함수에서 DetachCurrentThread()를 호출할 수 있습니다. 이 키를 pthread_setspecific()와 함께 사용하여 JNIEnv를 스레드 로컬 저장소에 저장하면 소멸자에 인수로 전달됩니다.

jclass, jmethodID 및 jfieldID

네이티브 코드에서 객체의 필드에 액세스하려면 다음 절차를 따릅니다.

  • FindClass를 사용하여 클래스의 클래스 객체 참조 가져오기
  • GetFieldID를 사용하여 필드의 필드 ID 가져오기
  • GetIntField와 같은 적절한 항목을 사용하여 필드의 콘텐츠 가져오기

마찬가지로, 메서드를 호출하려면 먼저 클래스 객체 참조를 가져온 다음 메서드 ID를 가져옵니다. ID는 내부 런타임 데이터 구조를 가리키는 포인터인 경우가 많습니다. 이러한 문자열을 찾으려면 여러 문자열 비교가 필요할 수 있지만, 일단 필드를 가져오거나 메서드를 호출하는 실제 호출은 매우 빠르게 실행됩니다.

성능이 중요한 경우 값을 한 번 검색하고 결과를 네이티브 코드에 캐시하는 것이 유용합니다. 프로세스당 JavaVM 한 개로 제한되므로 이 데이터를 정적 로컬 구조에 저장하는 것이 합리적입니다.

클래스 참조, 필드 ID 및 메서드 ID는 클래스가 언로드될 때까지 유효합니다. ClassLoader와 관련된 모든 클래스가 가비지 컬렉션이 가능한 경우에만 클래스가 언로드됩니다. 이는 드물지만 Android에서 불가능한 것은 아닙니다. 그러나 jclass는 클래스 참조이며 NewGlobalRef 호출로 보호해야 합니다 (다음 섹션 참고).

클래스가 로드될 때 ID를 캐시하고 클래스가 로드 취소되고 새로고침되면 자동으로 다시 캐시하려는 경우 ID를 초기화하는 올바른 방법은 다음과 같은 코드를 적절한 클래스에 추가하는 것입니다.

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

C/C++ 코드에서 ID를 검색하는 nativeClassInit 메서드를 만듭니다. 이 코드는 클래스가 초기화될 때 한 번 실행됩니다. 클래스가 언로드되었다가 다시 로드되면 다시 실행됩니다.

로컬 참조 및 글로벌 참조

네이티브 메서드에 전달되는 모든 인수와 JNI 함수에서 반환하는 거의 모든 객체는 '로컬 참조'입니다. 즉, 현재 스레드에서 현재 네이티브 메서드의 기간 동안 유효합니다. 네이티브 메서드가 반환된 후 객체 자체가 계속 실행되더라도 참조는 유효하지 않습니다.

이는 jclass, jstring, jarray를 포함한 jobject의 모든 서브클래스에 적용됩니다. 확장된 JNI 검사가 사용 설정되면 런타임에서 대부분의 참조 오용에 관해 경고합니다.

비로컬 참조를 가져오는 유일한 방법은 NewGlobalRefNewWeakGlobalRef 함수를 사용하는 것입니다.

참조를 더 오래 보관하려면 '전역' 참조를 사용해야 합니다. NewGlobalRef 함수는 로컬 참조를 인수로 사용하고 전역 참조를 반환합니다. 전역 참조는 DeleteGlobalRef를 호출할 때까지 유효합니다.

이 패턴은 일반적으로 FindClass에서 반환된 jclass를 캐싱할 때 사용됩니다.예를 들면 다음과 같습니다.

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

모든 JNI 메서드는 로컬 참조와 글로벌 참조를 모두 인수로 허용합니다. 동일한 객체의 참조 값이 서로 다를 수 있습니다. 예를 들어 동일한 객체에서 NewGlobalRef를 연속으로 호출했을 때의 반환 값이 다를 수 있습니다. 두 참조가 동일한 객체를 참조하는지 확인하려면 IsSameObject 함수를 사용해야 합니다. 네이티브 코드에서 ==와 참조를 비교하지 마세요.

결과적으로 네이티브 코드에서 객체 참조를 상수 또는 고유하다고 가정해서는 안 됩니다. 객체를 나타내는 값은 메서드 호출마다 다를 수 있으며 두 개의 다른 객체가 연속 호출에서 동일한 값을 가질 수 있습니다. jobject 값을 키로 사용하지 마세요.

프로그래머는 로컬 참조를 '과도하게 할당하지 않도록' 해야 합니다. 실질적으로 이는 객체 배열을 실행하는 동안 다수의 로컬 참조를 만드는 경우 JNI에서 자동으로 처리하도록 하는 대신 DeleteLocalRef를 사용하여 수동으로 로컬 참조를 해제해야 한다는 의미입니다. 구현 시 16개의 로컬 참조 슬롯만 예약하면 되므로 16개 이상의 로컬 참조 슬롯이 필요한 경우 작업하면서 로컬 참조 슬롯을 삭제하거나 EnsureLocalCapacity/PushLocalFrame를 사용하여 로컬 참조 슬롯을 더 예약해야 합니다.

jfieldIDjmethodID는 객체 참조가 아닌 불투명한 유형이므로 NewGlobalRef에 전달해서는 안 됩니다. GetStringUTFCharsGetByteArrayElements와 같은 함수에서 반환된 원시 데이터 포인터도 객체가 아닙니다. 스레드 간에 전달될 수 있으며 일치하는 Release 호출 시까지 유효합니다.

단, 한 가지 특별한 경우가 있습니다. AttachCurrentThread를 사용하여 네이티브 스레드를 연결하면 실행 중인 코드는 스레드가 분리될 때까지 자동으로 로컬 참조를 해제하지 않습니다. 생성한 모든 로컬 참조를 수동으로 삭제해야 합니다. 일반적으로 루프에서 로컬 참조를 만드는 네이티브 코드는 수동으로 삭제해야 합니다.

글로벌 참조를 사용할 때는 주의해야 합니다. 전역 참조는 불가피할 수 있지만 디버그하기 어려우며 진단하기 어려운 메모리 (오작동) 동작을 일으킬 수 있습니다. 다른 모든 조건이 동일하다면, 글로벌 참조가 적은 솔루션이 더 좋을 것입니다.

UTF-8 및 UTF-16 문자열

자바 프로그래밍 언어는 UTF-16을 사용합니다. 편의를 위해 JNI는 Modified UTF-8에서도 작동하는 메서드를 제공합니다. 수정된 인코딩은 \u0000을 0x00이 아닌 0xc0 0x80으로 인코딩하므로 C 코드에 유용합니다. 이 방법의 장점은 표준 libc 문자열 함수와 함께 사용하기에 적합한 C 스타일의 0으로 끝나는 문자열을 사용할 수 있다는 것입니다. 단점은 임의의 UTF-8 데이터를 JNI에 전달하여 올바르게 작동할 것으로 기대할 수 없다는 것입니다.

String의 UTF-16 표현을 가져오려면 GetStringChars를 사용하세요. UTF-16 문자열은 0으로 종료되지 않고 \u0000이 허용되므로 문자열 길이와 jchar 포인터를 계속 유지해야 합니다.

Get하는 문자열은 Release해야 합니다. 문자열 함수는 로컬 참조가 아닌 프리미티브 데이터에 대한 C 스타일 포인터인 jchar* 또는 jbyte*를 반환합니다. Release가 호출될 때까지 유효하므로 네이티브 메서드가 반환될 때 해제되지 않습니다.

NewStringUTF에 전달되는 데이터는 Modified UTF-8 형식이어야 합니다. 파일 또는 네트워크 스트림에서 문자 데이터를 읽고 필터링하지 않고 NewStringUTF에 전달하는 실수를 흔히 저지릅니다. 데이터가 유효한 MUTF-8 (또는 호환되는 하위 집합인 7비트 ASCII)임을 모르는 경우 유효하지 않은 문자를 제거하거나 적절한 Modified UTF-8 형식으로 변환해야 합니다. 그러지 않으면 UTF-16 변환에서 예기치 않은 결과가 나올 수 있습니다. 에뮬레이터에 기본적으로 사용 설정되어 있는 CheckJNI는 문자열을 스캔하고 잘못된 입력을 수신한 경우 VM을 취소합니다.

Android 8 전에는 일반적으로 UTF-16 문자열로 작업하는 것이 더 빨랐습니다. Android에서는 GetStringChars의 사본이 필요하지 않은 반면 GetStringUTFChars에는 할당 및 UTF-8로의 변환이 필요했기 때문입니다. Android 8에서는 메모리를 절약하기 위해 ASCII 문자열에 문자당 8비트를 사용하도록 String 표현을 변경하고 이동 가비지 컬렉터를 사용하기 시작했습니다. 이러한 기능을 사용하면 GetStringCritical의 경우에도 ART가 사본을 만들지 않고 String 데이터의 포인터를 제공할 수 있는 사례가 크게 줄어듭니다. 그러나 코드에서 처리되는 대부분의 문자열이 짧다면 대부분의 경우 스택에 할당된 버퍼와 GetStringRegion 또는 GetStringUTFRegion를 사용하여 할당 및 할당 해제를 방지할 수 있습니다. 예:

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

프리미티브 배열

JNI는 배열 객체의 내용에 액세스하는 함수를 제공합니다. 객체 배열은 한 번에 한 항목씩 액세스해야 하지만, 프리미티브 배열은 C에서 선언된 것처럼 직접 읽고 쓸 수 있습니다.

VM 구현을 제한하지 않고 최대한 효율적인 인터페이스를 만들기 위해 Get<PrimitiveType>ArrayElements 호출 모음을 사용하면 런타임에서 실제 요소의 포인터를 반환하거나 일부 메모리를 할당하고 사본을 만들 수 있습니다. 어떤 방법을 사용하든, 반환된 원시 포인터는 상응하는 Release 호출이 실행될 때까지 유효합니다. 즉, 데이터가 복사되지 않으면 배열 객체가 고정되고 힙 압축의 일부로 재배치될 수 없습니다. Get하는 모든 배열을 Release해야 합니다. 또한 Get 호출이 실패하면 코드에서 나중에 NULL 포인터를 Release하지 않도록 해야 합니다.

isCopy 인수에 NULL이 아닌 포인터를 전달하면 데이터가 복사되었는지 확인할 수 있습니다. 이는 거의 유용하지 않습니다.

Release 호출은 세 가지 값 중 하나를 가질 수 있는 mode 인수를 사용합니다. 런타임에서 수행하는 작업은 실제 데이터에 대한 포인터를 반환했는지 또는 데이터 사본을 반환했는지에 따라 달라집니다.

  • 0
    • 실제 데이터: 배열 객체가 고정 취소됩니다.
    • 사본: 데이터가 다시 복사됩니다. 사본이 있던 버퍼가 해제됩니다.
  • JNI_COMMIT
    • 실제 데이터: 아무 작업도 수행되지 않습니다.
    • 사본: 데이터가 다시 복사됩니다. 사본이 포함된 버퍼가 해제되지 않습니다.
  • JNI_ABORT
    • 실제 데이터: 배열 객체가 고정 취소됩니다. 이전 쓰기가 취소되지 않습니다.
    • 사본: 사본이 있던 버퍼가 해제되고, 사본의 변경 사항이 손실됩니다.

isCopy 플래그를 확인하는 한 가지 이유는 배열을 변경한 후 JNI_COMMITRelease를 호출해야 하는지 확인하기 위해서입니다. 배열의 콘텐츠를 사용하는 코드를 변경하고 실행하는 경우 노옵스(no-ops) 커밋을 건너뛸 수 있습니다. 플래그를 검사하는 또 다른 이유는 JNI_ABORT를 효율적으로 처리하기 위해서입니다. 예를 들어 배열을 가져와 제자리에서 수정하고 조각을 다른 함수에 전달한 후 변경사항을 삭제할 수 있습니다. JNI가 새 사본을 만든다는 것을 알고 있다면 다른 '수정 가능한' 사본을 만들 필요가 없습니다. JNI가 원본을 전달하는 경우, 자신만의 사본을 만들어야 합니다.

*isCopy이 false이면 Release 호출을 건너뛸 수 있다고 가정하는 실수가 흔히 일어납니다 (예제 코드에서 반복됨). 호출은 건너뛸 수 없습니다. 복사 버퍼가 할당되지 않은 경우 원본 메모리는 고정되어야 하며 가비지 컬렉터가 원래 메모리를 이동할 수 없습니다.

또한 JNI_COMMIT 플래그는 배열을 해제하지 않으므로 결국에는 다른 플래그를 사용하여 Release를 다시 호출해야 합니다.

리전 호출

데이터를 안팎으로 복사하려는 경우 Get<Type>ArrayElementsGetStringChars와 같은 호출의 대안이 매우 유용할 수 있습니다. 다음을 고려하세요.

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

이렇게 하면 배열을 가져와 배열에서 처음 len 바이트 요소를 복사한 후 배열을 해제합니다. 구현에 따라 Get 호출은 배열 콘텐츠를 고정하거나 복사합니다. 코드는 데이터를 두 번째로 복사한 다음 Release를 호출합니다. 이 경우 JNI_ABORT는 세 번째 사본이 생성되지 않도록 합니다.

아래 코드를 사용하면 같은 작업을 더 간단하게 할 수 있습니다.

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

이 코드에는 다음과 같은 몇 가지 장점이 있습니다.

  • JNI 호출이 두 번이 아닌 한 번만 필요하므로 오버헤드가 줄어듭니다.
  • 고정 또는 추가 데이터 사본이 필요하지 않습니다.
  • 프로그래머 오류의 위험이 줄어듭니다. 오류가 발생한 후 Release 호출을 잊어버릴 위험이 없습니다.

마찬가지로 Set<Type>ArrayRegion 호출을 사용하여 데이터를 배열에 복사하고 GetStringRegion 또는 GetStringUTFRegion를 사용하여 String에서 문자를 복사할 수 있습니다.

예외

예외가 대기중인 동안에는 대부분의 JNI 함수를 호출하면 안 됩니다. 코드는 함수의 반환 값인 ExceptionCheck 또는 ExceptionOccurred를 통해 예외를 확인하고 반환하거나 예외를 지우고 처리해야 합니다.

예외가 대기 중인 동안 호출할 수 있는 유일한 JNI 함수는 다음과 같습니다.

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

많은 JNI 호출이 예외를 발생시킬 수 있지만 오류를 확인하는 더 간단한 방법을 제공하는 경우가 많습니다. 예를 들어 NewString가 NULL이 아닌 값을 반환하는 경우 예외를 확인할 필요가 없습니다. 그러나 CallObjectMethod와 같은 함수를 사용하여 메서드를 호출하는 경우 예외가 발생할 경우 반환 값이 유효하지 않으므로 항상 예외를 확인해야 합니다.

관리 코드에서 발생한 예외는 네이티브 스택 프레임을 해제하지 않습니다. (또한 Android에서 일반적으로 권장되지 않는 C++ 예외는 C++ 코드에서 관리 코드로의 JNI 전환 경계를 통해 발생해서는 안 됩니다.) JNI ThrowThrowNew 명령어는 단순히 현재 스레드에 예외 포인터를 설정합니다. 네이티브 코드에서 관리로 돌아가면 예외가 확인되고 적절하게 처리됩니다.

네이티브 코드는 ExceptionCheck 또는 ExceptionOccurred를 호출하여 예외를 '포착'하고 ExceptionClear를 사용하여 예외를 지울 수 있습니다. 평소와 같이 예외를 처리하지 않고 삭제하면 문제가 발생할 수 있습니다.

Throwable 객체 자체를 조작하는 기본 제공 함수가 없으므로 예외 문자열을 가져오려면 Throwable 클래스를 찾아 getMessage "()Ljava/lang/String;"의 메서드 ID를 조회하여 호출해야 합니다. 결과가 NULL이 아닌 경우 GetStringUTFChars를 사용하여 printf(3) 또는 이와 동등한 항목에 사용할 수 있는 항목을 가져옵니다.

확장 검사

JNI는 오류 검사를 거의 하지 않습니다. 오류가 있으면 일반적으로 비정상 종료가 발생합니다. Android에서는 CheckJNI라는 모드도 제공됩니다. 이 모드에서는 JavaVM 및 JNIEnv 함수 테이블 포인터가 표준 구현을 호출하기 전에 일련의 확장 검사를 수행하는 함수 테이블로 전환됩니다.

추가 검사에는 다음이 포함됩니다.

  • 배열: 음수 크기의 배열을 할당하려는 시도
  • 잘못된 포인터: 잘못된 jarray/jclass/jobject/jstring을 JNI 호출에 전달 또는 nullable이 아닌 인수를 사용하여 NULL 포인터를 JNI 호출에 전달
  • 클래스 이름: 'java/lang/String' 스타일의 클래스 이름이 아닌 값을 JNI 호출에 전달
  • 중요한 호출: '중요한' get과 그에 상응하는 release 간에 JNI 호출 실행
  • Direct ByteBuffer: 잘못된 인수를 NewDirectByteBuffer에 전달
  • 예외: 대기중인 예외가 있는 동안 JNI 호출 실행
  • JNIEnv*s: 잘못된 스레드의 JNIEnv* 사용
  • jfieldID: NULL jfieldID 사용, jfieldID를 사용하여 필드를 잘못된 형식의 값으로 설정(예: String 필드에 StringBuilder 할당 시도), 정적 필드에 jfieldID를 사용하여 인스턴스 필드를 설정하거나 그 반대의 경우, 한 클래스의 jfieldID를 다른 클래스의 인스턴스에서 사용
  • jmethodID: Call*Method JNI 호출을 실행할 때 잘못된 종류의 jmethodID 사용: 잘못된 반환 유형, 정적/비정적 불일치, 잘못된 'this' 형식(비정적 호출의 경우) 또는 잘못된 클래스(정적 호출의 경우)
  • 참조: 잘못된 종류의 참조에 DeleteGlobalRef/DeleteLocalRef 사용
  • 해제 모드: 잘못된 해제 모드를 해제 호출에 전달(0, JNI_ABORT 또는 JNI_COMMIT 이외의 값)
  • 형식 안전성: 네이티브 메서드에서 호환되지 않는 형식 반환(예: String을 반환하는 것으로 선언된 메서드에서 StringBuilder 반환)
  • UTF-8: 잘못된 Modified UTF-8 바이트 시퀀스를 JNI 호출에 전달

메서드 및 필드 접근성은 여전히 검사하지 않습니다. 네이티브 코드에는 액세스 제한이 적용되지 않습니다.

CheckJNI를 사용 설정하는 방법에는 여러 가지가 있습니다.

에뮬레이터를 사용하는 경우 CheckJNI가 기본적으로 켜져 있습니다.

루팅된 기기가 있는 경우 다음 명령 시퀀스를 통해 CheckJNI를 사용 설정하여 런타임을 다시 시작할 수 있습니다.

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

두 경우 모두, 런타임을 시작할 때 logcat 출력에 다음과 같은 내용이 표시됩니다.

D AndroidRuntime: CheckJNI is ON

일반 기기가 있는 경우 다음 명령을 사용할 수 있습니다.

adb shell setprop debug.checkjni 1

이 명령은 이미 실행 중인 앱에는 영향을 주지 않지만, 그 시점부터 시작된 앱에서 CheckJNI를 사용할 수 있게 됩니다. 속성을 다른 값으로 변경하거나 단순히 재부팅하면 CheckJNI가 다시 사용되지 않습니다. 이 경우, 다음에 앱을 시작할 때 logcat 출력에 다음과 같은 내용이 표시됩니다.

D Late-enabling CheckJNI

애플리케이션의 매니페스트에서 android:debuggable 속성을 설정하여 자신의 앱에만 CheckJNI를 사용 설정할 수도 있습니다. Android 빌드 도구는 특정 빌드 유형에 대해 자동으로 이 작업을 실행합니다.

네이티브 라이브러리

표준 System.loadLibrary를 사용하여 공유 라이브러리에서 네이티브 코드를 로드할 수 있습니다.

실제로 이전 버전의 Android에는 PackageManager에 버그가 있어 네이티브 라이브러리의 설치 및 업데이트를 신뢰할 수 없었습니다. ReLinker 프로젝트는 이 문제와 기타 네이티브 라이브러리 로드 문제에 대한 해결 방법을 제공합니다.

정적 클래스 이니셜라이저에서 System.loadLibrary (또는 ReLinker.loadLibrary)를 호출합니다. 인수는 '데코레이트되지 않은' 라이브러리 이름이므로 libfubar.so를 로드하려면 "fubar"를 전달합니다.

네이티브 메서드가 있는 클래스가 하나뿐인 경우 System.loadLibrary 호출이 해당 클래스의 정적 이니셜라이저에 있는 것이 좋습니다. 또는 라이브러리가 항상 로드되고 항상 조기에 로드되도록 Application에서 호출하는 것이 좋습니다.

런타임에서 네이티브 메서드를 찾을 수 있는 두 가지 방법이 있습니다. RegisterNatives를 사용하여 명시적으로 객체를 등록하거나 dlsym을 사용하여 런타임에서 동적으로 찾도록 할 수 있습니다. RegisterNatives의 장점은 기호가 있는지 미리 확인할 뿐만 아니라 JNI_OnLoad만 내보내지 않으므로 더 작고 빠른 공유 라이브러리를 가질 수 있다는 점입니다. 런타임에서 함수를 검색할 수 있는 장점은 작성할 코드가 약간 줄어든다는 것입니다.

RegisterNatives 앱을 사용하려면 다음 안내를 따르세요

  • JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 함수를 제공합니다.
  • JNI_OnLoad에서 RegisterNatives를 사용하여 모든 네이티브 메서드를 등록합니다.
  • JNI_OnLoad만 라이브러리에서 내보내도록 -fvisibility=hidden를 사용하여 빌드합니다. 이렇게 하면 코드가 더 빨라지고 작아지고, 앱에 로드된 다른 라이브러리와의 잠재적 충돌을 방지할 수 있습니다. 하지만 앱이 네이티브 코드에서 비정상 종료되는 경우에는 덜 유용한 스택 트레이스가 생성됩니다.

정적 초기화 프로그램은 다음과 같아야 합니다.

Kotlin

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

Java

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

JNI_OnLoad 함수는 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;
}

네이티브 메서드의 '검색'을 대신 사용하려면 메서드 이름을 특정 방식으로 지정해야 합니다 (자세한 내용은 JNI 사양 참고). 즉, 메서드 서명이 잘못된 경우 메서드가 실제로 처음 호출될 때까지 알 수 없습니다.

JNI_OnLoad에서 실행된 모든 FindClass 호출은 공유 라이브러리를 로드하는 데 사용된 클래스 로더의 컨텍스트에서 클래스를 확인합니다. FindClass는 다른 컨텍스트에서 호출될 때 자바 스택 맨 위에 있는 메서드와 연결된 클래스 로더를 사용하거나 방금 연결된 네이티브 스레드에서 호출되었기 때문에 클래스 로더가 없는 경우 '시스템' 클래스 로더를 사용합니다. 시스템 클래스 로더는 애플리케이션의 클래스를 알지 못하므로 이 컨텍스트에서는 FindClass로 자체 클래스를 찾을 수 없습니다. 따라서 JNI_OnLoad는 클래스를 찾고 캐시하기에 편리한 장소입니다. 유효한 jclass 전역 참조가 있으면 연결된 모든 스레드에서 사용할 수 있습니다.

@FastNative@CriticalNative를 사용한 더 빠른 네이티브 호출

네이티브 메서드에는 @FastNative 또는 @CriticalNative(둘 다 아님)로 주석을 달아 관리 코드와 네이티브 코드 간의 전환 속도를 높일 수 있습니다. 하지만 이러한 주석에는 동작의 특정 변경사항이 수반되며 사용하기 전에 신중하게 고려해야 합니다. 아래에서 이러한 변경사항을 간략하게 언급하지만 자세한 내용은 문서를 참고하세요.

@CriticalNative 주석은 관리 객체를 사용하지 않는 네이티브 메서드에만 (매개변수 또는 반환 값에 또는 암시적 this로) 적용할 수 있습니다. 이 주석은 JNI 전환 ABI를 변경합니다. 네이티브 구현은 함수 서명에서 JNIEnvjclass 매개변수를 제외해야 합니다.

@FastNative 또는 @CriticalNative 메서드를 실행하는 동안 가비지 컬렉션은 필수 작업을 위해 스레드를 정지할 수 없으며 차단될 수 있습니다. 일반적으로 속도가 빠르지만 제한이 없는 메서드를 비롯하여 장기 실행되는 메서드에는 이러한 주석을 사용하지 마세요. 특히 코드는 중요한 I/O 작업을 실행하거나 오랫동안 유지할 수 있는 네이티브 잠금을 획득해서는 안 됩니다.

이러한 주석은 Android 8부터 시스템용으로 구현되었으며 Android 14에서 CTS 테스트를 거친 공개 API가 되었습니다. 이러한 최적화는 Android 8~13 기기에서도 작동할 가능성이 높지만 (강력한 CTS 보장은 없지만) 네이티브 메서드의 동적 조회는 Android 12 이상에서만 지원됩니다. Android 버전 8~11에서 실행하려면 JNI RegisterNatives를 통한 명시적인 등록이 반드시 필요합니다. 이러한 주석은 Android 7에서 무시되며, @CriticalNative의 ABI 불일치로 인해 잘못된 인수 마샬링이 발생하고 다운될 수 있습니다.

이러한 주석이 필요한 성능이 중요한 메서드의 경우 네이티브 메서드의 이름 기반 '검색'에 의존하는 대신 JNI RegisterNatives로 메서드를 명시적으로 등록하는 것이 좋습니다. 최적의 앱 시작 성능을 얻으려면 기준 프로필@FastNative 또는 @CriticalNative 메서드 호출자를 포함하는 것이 좋습니다. Android 12부터는 모든 인수가 레지스터에 들어가는 경우 컴파일된 관리 메서드에서 @CriticalNative 네이티브 메서드를 호출하는 것이 C/C++의 비 인라인 호출만큼 비용이 저렴합니다 (예: arm64에서 최대 8개의 정수 및 최대 8개의 부동 소수점 인수).

때로는 네이티브 메서드를 두 개로 분할하는 것이 좋을 수 있습니다. 하나는 실패할 수 있는 매우 빠른 메서드이고 다른 하나는 느린 사례를 처리하는 메서드입니다. 예:

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

64비트 고려 사항

64비트 포인터를 사용하는 아키텍처를 지원하려면 자바 필드에 네이티브 구조에 대한 포인터를 저장할 때 int 대신 long 필드를 사용합니다.

지원되지 않는 기능/이전 버전과의 호환성

다음 예외를 제외하고, 모든 JNI 1.6 기능이 지원됩니다.

  • DefineClass가 구현되어 있지 않습니다. Android는 자바 바이트 코드 또는 클래스 파일을 사용하지 않으므로 바이너리 클래스 데이터를 전달할 수 없습니다.

이전 Android 버전과의 호환성을 위해 다음 사항에 유의해야 합니다.

  • 네이티브 함수의 동적 검색

    Android 2.0 (Eclair)까지는 메서드 이름을 검색하는 동안 '$' 문자가 '_00024'로 제대로 변환되지 않았습니다. 이 문제를 해결하려면 명시적 등록을 사용하거나 내부 클래스에서 네이티브 메서드를 이동해야 합니다.

  • 스레드 분리

    Android 2.0 (Eclair)까지는 '종료 전에 스레드를 분리해야 함' 검사를 피하기 위해 pthread_key_create 소멸자 함수를 사용할 수 없었습니다. 런타임은 pthread 키 소멸자 함수도 사용하므로 어떤 함수가 먼저 호출되는지를 확인하기가 어렵습니다.

  • 약한 글로벌 참조

    Android 2.2(Froyo)까지는 약한 글로벌 참조가 구현되지 않았습니다. 이전 버전에서는 약한 글로벌 참조를 사용하려는 시도를 적극적으로 거부합니다. Android 플랫폼 버전 상수를 사용하여 지원 여부를 테스트할 수 있습니다.

    Android 4.0 (Ice Cream Sandwich)까지는 약한 글로벌 참조가 NewLocalRef, NewGlobalRef, DeleteWeakGlobalRef로만 전달될 수 있었습니다. (사양은 프로그래머가 약한 전역 변수에 대해 작업을 수행하기 전에 약한 전역 변수에 대한 하드 참조를 만들 것을 강력히 권장하므로, 이로 인해 제한이 발생하지 않습니다.)

    Android 4.0 (Ice Cream Sandwich)부터는 약한 글로벌 참조를 다른 JNI 참조처럼 사용할 수 있습니다.

  • 로컬 참조

    Android 4.0 (Ice Cream Sandwich)까지는 로컬 참조가 실제로 직접 포인터였습니다. Ice Cream Sandwich는 더 나은 가비지 컬렉터를 지원하는 데 필요한 간접값을 추가했지만, 이는 이전 버전에서 많은 JNI 버그를 감지할 수 없음을 의미합니다. 자세한 내용은 ICS의 JNI 로컬 참조 변경사항을 참고하세요.

    Android 8.0 이전의 Android 버전에서는 로컬 참조 수가 버전별 한도로 제한됩니다. Android 8.0부터는 Android에서 무제한 로컬 참조를 지원합니다.

  • GetObjectRefType로 참조 유형 확인

    Android 4.0 (Ice Cream Sandwich)까지는 직접 포인터를 사용했기 때문에 (위 참조) GetObjectRefType를 올바르게 구현할 수 없었습니다. 대신 약한 전역 변수 테이블, 인수, 로컬 변수 테이블, 전역 변수 테이블을 순서대로 살펴보는 휴리스틱을 사용했습니다. 직접 포인터를 처음 찾았을 때 참조가 조사 중인 유형이라고 보고합니다. 예를 들어 정적 네이티브 메서드에 암시적 인수로 전달된 jclass와 동일한 전역 jclass에서 GetObjectRefType을 호출하면 JNIGlobalRefType 대신 JNILocalRefType이 표시됩니다.

  • @FastNative@CriticalNative

    Android 7까지는 이러한 최적화 주석이 무시되었습니다. @CriticalNative의 ABI 불일치로 인해 잘못된 인수 마샬링이 발생하고 비정상 종료가 발생할 수 있습니다.

    @FastNative@CriticalNative 메서드용 네이티브 함수의 동적 조회가 Android 8~10에서 구현되지 않았으며 Android 11에서 알려진 버그가 포함되어 있습니다. JNI RegisterNatives를 통해 명시적으로 등록하지 않고 이러한 최적화를 사용하면 Android 8~11에서 비정상 종료가 발생할 수 있습니다.

FAQ: UnsatisfiedLinkError가 표시되는 이유는 무엇인가요?

네이티브 코드로 작업할 때 다음과 같은 오류가 표시되는 경우가 많습니다.

java.lang.UnsatisfiedLinkError: Library foo not found

때로는 말 그대로, 라이브러리를 찾을 수 없는 경우입니다. 라이브러리가 있지만 dlopen(3)에서 열 수 없는 경우도 있으며, 실패의 세부정보는 예외 세부정보 메시지에서 확인할 수 있습니다.

'라이브러리를 찾을 수 없음' 예외가 발생할 수 있는 일반적인 이유는 다음과 같습니다.

  • 라이브러리가 없거나 앱에 액세스할 수 없습니다. adb shell ls -l <path>를 사용하여 현재 상태와 권한을 확인하세요.
  • 라이브러리가 NDK로 빌드되지 않았습니다. 이로 인해 기기에 없는 함수나 라이브러리에 대한 종속 항목이 발생할 수 있습니다.

UnsatisfiedLinkError 오류의 또 다른 클래스는 다음과 같습니다.

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

logcat에 다음 메시지가 표시됩니다.

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

이는 런타임이 일치하는 메서드를 찾으려고 시도했지만 실패했음을 의미합니다. 이 오류의 몇 가지 일반적인 이유는 다음과 같습니다.

  • 라이브러리가 로드되지 않습니다. logcat 출력에서 라이브러리 로드에 관한 메시지를 확인하세요.
  • 이름 또는 서명 불일치로 인해 메서드를 찾을 수 없습니다. 이 문제는 일반적으로 다음과 같은 이유로 발생합니다.
    • 지연 메서드 검색의 경우 extern "C" 및 적절한 공개 상태 (JNIEXPORT)를 사용하여 C++ 함수를 선언하지 못합니다. Ice Cream Sandwich 이전에는 JNIEXPORT 매크로가 잘못되었으므로 이전 jni.h와 함께 새 GCC를 사용하면 작동하지 않습니다. arm-eabi-nm를 사용하여 라이브러리에 나타나는 기호를 확인할 수 있습니다. 기호가 훼손되어 있거나 (Java_Foo_myfunc 대신 _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass와 같은 형태) 기호 유형이 대문자 'T'가 아닌 소문자 't'인 경우 선언을 조정해야 합니다.
    • 명시적 등록의 경우 메서드 서명을 입력할 때 사소한 오류가 발생합니다. 등록 호출에 전달하는 내용이 로그 파일의 서명과 일치하는지 확인합니다. 'B'는 byte이고 'Z'는 boolean입니다. 서명의 클래스 이름 구성요소는 'L'로 시작하고 ';'로 끝납니다. '/'를 사용하여 패키지/클래스 이름을 구분하고 '$'를 사용하여 내부 클래스 이름을 구분합니다 (예: Ljava/util/Map$Entry;).

javah를 사용하여 JNI 헤더를 자동으로 생성하면 몇 가지 문제를 방지하는 데 도움이 될 수 있습니다.

FAQ: FindClass에서 내 클래스를 찾지 못한 이유는 무엇인가요?

(이 권장사항은 대부분 GetMethodID 또는 GetStaticMethodID를 사용하여 메서드를 찾지 못한 경우나 GetFieldID 또는 GetStaticFieldID로 필드를 찾지 못한 경우에도 동일하게 적용됩니다.)

클래스 이름 문자열의 형식이 올바른지 확인합니다. JNI 클래스 이름은 패키지 이름으로 시작하고 java/lang/String과 같이 슬래시로 구분됩니다. 배열 클래스를 검색하는 경우 적절한 수의 대괄호로 시작해야 하며 클래스를 'L'과 ';'로 래핑해야 합니다. 따라서 String의 1차원 배열은 [Ljava/lang/String;가 됩니다. 내부 클래스를 검색하는 경우 '.' 대신 '$'를 사용합니다. 일반적으로 .class 파일에서 javap를 사용하면 클래스의 내부 이름을 확인할 수 있습니다.

코드 축소를 사용 설정하는 경우 유지할 코드를 구성해야 합니다. 그렇지 않으면 코드 축소기가 JNI에서만 사용되는 클래스, 메서드 또는 필드를 삭제할 수도 있으므로 적절한 keep 규칙을 구성하는 것이 중요합니다.

클래스 이름이 올바르면 클래스 로더 문제일 수 있습니다. FindClass는 코드와 연결된 클래스 로더에서 클래스 검색을 시작하려고 합니다. 다음과 같은 호출 스택을 검사합니다.

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

맨 위에 있는 메서드는 Foo.myfunc입니다. FindClassFoo 클래스와 연결된 ClassLoader 객체를 찾아 사용합니다.

일반적으로 이 작업은 예상한 대로 실행됩니다. pthread_create를 호출한 다음 AttachCurrentThread로 연결하여 스레드를 직접 만들면 문제가 발생할 수 있습니다. 이제 애플리케이션에 스택 프레임이 없습니다. 이 스레드에서 FindClass을 호출하면 JavaVM이 애플리케이션과 연결된 클래스 로더가 아닌 '시스템' 클래스 로더에서 시작되므로 앱별 클래스를 찾으려고 하면 실패합니다.

이 문제를 해결할 수 있는 몇 가지 방법이 있습니다.

  • JNI_OnLoad에서 FindClass를 한 번 조회하고 나중에 사용할 수 있도록 클래스 참조를 캐시합니다. JNI_OnLoad 실행의 일부로 실행된 모든 FindClass 호출은 System.loadLibrary를 호출한 함수와 연결된 클래스 로더를 사용합니다 (이 특수 규칙은 라이브러리 초기화를 더 편리하게 하기 위해 제공됨). 앱 코드에서 라이브러리를 로드하는 경우 FindClass는 올바른 클래스 로더를 사용합니다.
  • Class 인수를 사용할 네이티브 메서드를 선언한 다음 Foo.class을 전달하여 클래스 인스턴스를 필요한 함수에 전달합니다.
  • ClassLoader 객체에 대한 참조를 편리한 위치에 캐시하고 loadClass를 직접 호출합니다. 그러려면 다소 노력이 필요합니다.

FAQ: 원시 데이터를 네이티브 코드와 공유하려면 어떻게 해야 하나요?

관리 코드와 네이티브 코드 모두에서 원시 데이터의 대규모 버퍼에 액세스해야 하는 상황에 처할 수 있습니다. 일반적인 예로는 비트맵 또는 사운드 샘플 조작이 있습니다. 두 가지 기본 접근 방식이 있습니다.

byte[]에 데이터를 저장할 수 있습니다. 따라서 관리 코드에서 매우 빠르게 액세스할 수 있습니다 그러나 네이티브 측면에서는 데이터를 복사하지 않고는 데이터에 액세스할 수 없다는 보장이 있습니다. 일부 구현에서는 GetByteArrayElementsGetPrimitiveArrayCritical가 관리되는 힙의 원시 데이터에 대한 실제 포인터를 반환하지만, 네이티브 힙에 버퍼를 할당하고 데이터를 복사하는 구현도 있습니다.

또는, 직접 바이트 버퍼에 데이터를 저장할 수 있습니다. java.nio.ByteBuffer.allocateDirect 또는 JNI NewDirectByteBuffer 함수를 사용하여 만들 수 있습니다. 일반 바이트 버퍼와 달리 저장소는 관리되는 힙에 할당되지 않으며 항상 네이티브 코드에서 직접 액세스할 수 있습니다 (GetDirectBufferAddress를 사용하여 주소 가져오기). 직접 바이트 버퍼 액세스가 구현된 방식에 따라 관리 코드에서 데이터에 액세스하는 속도가 매우 느릴 수 있습니다.

다음 두 가지 요인에 따라 사용할 방법을 선택해야 합니다.

  1. 대부분의 데이터 액세스가 자바 또는 C/C++로 작성된 코드에서 발생하나요?
  2. 데이터가 궁극적으로 시스템 API로 전달되는 경우 어떤 형식이어야 하나요? 예를 들어 데이터가 최종적으로 byte[]를 사용하는 함수에 전달되는 경우 직접 ByteBuffer에서 처리하는 것은 부적절할 수 있습니다.

어떤 방법이 더 적합한지 확실하지 않은 경우 직접 바이트 버퍼를 사용합니다. 이러한 지원 기능은 JNI에서 직접 빌드되었으며 향후 버전에서는 성능이 개선될 것입니다.