JNI는 Java Native Interface(자바 네이티브 인터페이스)의 약어입니다. JNI는 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 업데이트를 간소화할 수 있습니다. 예를 들어 자바 코드로 된 UI 스레드에서 JNI를 통해 C++ 함수를 호출하는 것보다 자바 프로그래밍 언어로 된 두 스레드 간에 콜백을 실행하는 것이 좋습니다. 스레드 중 하나에서 C++ 차단 호출을 실행한 후 차단 호출이 완료되면 UI 스레드에 알립니다.
- JNI를 처리해야 하거나 JNI에서 처리해야 하는 스레드 수를 최소화합니다. 자바 및 C++ 언어 둘 다에서 스레드 풀을 사용해야 하는 경우 개별 작업자 스레드가 아닌 풀 소유자 간에 JNI 통신을 유지합니다.
- 향후 리팩터링을 돕기 위해 쉽게 파악할 수 있는 소수의 C++ 및 자바 소스 위치에 인터페이스 코드를 유지합니다. 적절한 경우 JNI 자동 생성 라이브러리를 사용하는 것이 좋습니다.
JavaVM 및 JNIEnv
JNI는 두 개의 주요 데이터 구조인 'JavaVM'과 'JNIEnv'를 정의합니다. 둘 다 본질적으로 함수 테이블에 대한 포인터의 포인터입니다. C++ 버전에서는 함수 테이블에 대한 포인터와 테이블을 통해 간접적으로 처리되는 각 JNI 함수의 멤버 함수가 있는 클래스입니다. JavaVM은 JavaVM을 만들고 삭제할 수 있는 '호출 인터페이스' 함수를 제공합니다. 이론적으로 프로세스당 여러 개의 JavaVM을 사용할 수 있지만, Android에서는 하나만 허용합니다.
JNIEnv는 대부분의 JNI 함수를 제공합니다. 네이티브 함수는 모두 JNIEnv를 첫 번째 인수로 받습니다.
JNIEnv는 스레드 로컬 저장소에 사용됩니다. 따라서 스레드 간에 JNIEnv를 공유할 수 없습니다.
코드 조각에서 JNIEnv를 가져올 다른 방법이 없다면 JavaVM을 공유하고 GetEnv
를 사용하여 스레드의 JNIEnv를 검색해야 합니다. 스레드에 JNIEnv가 있다고 가정합니다. 아래 AttachCurrentThread
를 참조하세요.
JNIEnv 및 JavaVM의 C 선언은 C++ 선언과 다릅니다. "jni.h"
포함 파일은 C에 포함되었는지 C++에 포함되었는지에 따라 다른 typedef를 제공합니다. 따라서 두 언어에 모두 포함된 헤더 파일에 JNIEnv 인수를 포함하는 것은 좋지 않습니다. 즉, 헤더 파일에 #ifdef __cplusplus
가 필요한 경우 이 헤더에 JNIEnv를 가리키는 내용이 있으면 추가 작업을 해야 할 수 있습니다.
스레드
모든 스레드는 커널을 통해 예약된 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는 단순히 내부 런타임 데이터 구조에 대한 포인터인 경우가 많습니다. ID 검색을 위해 여러 문자열 비교가 필요할 수 있지만, ID가 검색되고 나면 필드를 가져오거나 메서드를 호출하는 실제 호출은 매우 빠르게 실행됩니다.
성능이 중요한 경우, 값을 한 번 검색한 다음 그 결과를 네이티브 코드에 캐시하면 유용합니다. 프로세스당 JavaVM 한 개로 제한되기 때문에 이 데이터를 정적 로컬 구조에 저장하는 것이 합당합니다.
클래스 참조, 필드 ID 및 메서드 ID는 클래스가 언로드될 때까지 유효합니다. ClassLoader와 관련된 모든 클래스를 가비지 컬렉션할 수 있는 경우에만 클래스가 언로드됩니다. 이러한 경우는 드물지만 Android에서 불가능하지는 않습니다. 그러나 jclass
는 클래스 참조이며 NewGlobalRef
호출(다음 섹션 참조)을 사용하여 보호해야 합니다.
클래스가 로드될 때 ID를 캐시하고 클래스가 언로드되었다가 다시 로드되면 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() } }
자바
/* * 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 검사를 사용 설정하면 런타임에서 대부분의 참조 오용을 경고합니다.
비로컬 참조를 가져오는 유일한 방법은 NewGlobalRef
및 NewWeakGlobalRef
함수를 사용하는 것입니다.
더 오랜 기간 동안 참조를 유지하려면 '글로벌' 참조를 사용해야 합니다. NewGlobalRef
함수는 로컬 참조를 인수로 사용하고 글로벌 참조를 반환합니다.
글로벌 참조는 DeleteGlobalRef
를 호출할 때까지 유효합니다.
이 패턴은 일반적으로 FindClass
에서 반환된 jclass를 캐시할 때 사용됩니다. 예를 들면 다음과 같습니다.
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
모든 JNI 메서드는 로컬 참조와 글로벌 참조를 모두 인수로 허용합니다.
동일한 객체의 참조 값이 서로 다를 수 있습니다.
예를 들어 동일한 객체에 관한 연속된 NewGlobalRef
호출의 반환 값이 서로 다를 수 있습니다.
두 참조가 동일한 객체를 참조하는지 확인하려면 IsSameObject
함수를 사용해야 합니다. 네이티브 코드에서 ==
를 사용하여 참조를 비교하지 마세요.
결과적으로, 네이티브 코드에서 객체 참조를 상수 또는 고유한 값으로 가정해서는 안 됩니다. 객체를 나타내는 32비트 값이 메서드 호출마다 다를 수 있으며 두 가지 객체의 값이 연속 호출에서 동일한 32비트 값일 수 있습니다. jobject
값을 키로 사용하지 마세요.
프로그래머는 로컬 참조를 '과도하게 할당하지 않도록' 해야 합니다. 실제 예로 설명하면 객체 배열을 통해 실행하는 동안 다수의 로컬 참조를 만드는 경우 JNI에서 처리하도록 허용하는 것이 아니라 DeleteLocalRef
를 사용하여 수동으로 이러한 참조를 해제해야 합니다. 구현에서는 16개의 로컬 참조 슬롯만 예약하면 되기 때문에 16개보다 많은 로컬 참조 슬롯이 필요한 경우 작업하면서 로컬 참조 슬롯을 삭제하거나 EnsureLocalCapacity
/PushLocalFrame
을 사용하여 로컬 참조 슬롯을 더 예약해야 합니다.
jfieldID
및 jmethodID
는 객체 참조가 아닌 불명확한 유형이므로 NewGlobalRef
에 전달해서는 안 됩니다. GetStringUTFChars
, GetByteArrayElements
등의 함수에서 반환된 원시 데이터 포인터도 객체가 아닙니다. 이러한 포인터는 스레드 간에 전달될 수 있으며 일치하는 릴리스가 호출될 때까지 유효합니다.
단, 한 가지 특별한 경우가 있습니다. AttachCurrentThread
를 사용하여 네이티브 스레드를 연결하는 경우 스레드가 분리되기 전에는 실행 중인 코드에서 자동으로 로컬 참조를 해제하지 않습니다. 직접 만든 로컬 참조를 모두 수동으로 삭제해야 합니다. 일반적으로, 루프를 통해 로컬 참조를 만드는 네이티브 코드도 수동으로 삭제해야 합니다.
글로벌 참조를 사용할 때는 주의해야 합니다. 글로벌 참조는 불가피할 수 있지만 디버그하기가 어려우며, 글로벌 참조로 인해 진단하기 어려운 메모리(mis) 동작이 발생할 수 있습니다. 그 밖의 모든 조건이 같다면, 글로벌 참조가 적은 솔루션이 더 좋습니다.
UTF-8 및 UTF-16 문자열
자바 프로그래밍 언어는 UTF-16을 사용합니다. 편의상, JNI는 Modified UTF-8에서도 작동하는 메서드를 제공합니다. 수정된 인코딩은 \u0000을 0x00이 아닌 0xc0 0x80으로 인코딩하기 때문에 C 코드에 유용합니다. 이 경우 표준 libc 문자열 함수에 사용하기 적합한 C 스타일 0 종료 문자열을 사용할 수 있다는 장점이 있습니다. 단점은 임의의 UTF-8 데이터를 JNI로 전달하여 올바르게 작동할 것으로 기대할 수 없다는 것입니다.
가능한 경우, 일반적으로 UTF-16 문자열로 작업하는 것이 더 빠릅니다. Android에서는 현재 GetStringChars
에는 사본이 필요하지 않지만 GetStringUTFChars
에는 할당 및 UTF-8로의 변환이 필요합니다. 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을 중단합니다.
프리미티브 배열
JNI는 배열 객체의 내용에 액세스하는 함수를 제공합니다. 객체 배열은 한 번에 한 항목씩 액세스해야 하지만, 프리미티브 배열은 C로 선언된 것처럼 직접 읽고 쓸 수 있습니다.
VM 구현을 제한하지 않고 최대한 효율적인 인터페이스를 만들려면 Get<PrimitiveType>ArrayElements
계열의 호출을 통해 런타임에서 실제 요소에 관한 포인터를 반환하거나 일부 메모리를 할당하고 사본을 만들면 됩니다. 어떤 방법을 사용하든, 반환된 원시 포인터는 대응하는 Release
호출을 실행할 때까지 유효하므로 데이터를 복사하지 않은 경우 배열 객체가 고정되고 힙을 압축하는 과정에서 배열 객체를 재배치할 수 없습니다.
Get
하는 모든 배열을 Release
해야 합니다. 또한 Get
호출에 실패하면 코드가 나중에 NULL 포인터의 Release
를 시도하지 않는지 확인해야 합니다.
isCopy
인수에 관해 NULL이 아닌 포인터를 전달하면 데이터가 복사되었는지 여부를 확인할 수 있습니다. 이 인수는 거의 유용하지 않습니다.
Release
호출에서는 세 개의 값 중 하나가 포함된 mode
인수를 사용합니다. 런타임에 실행되는 작업은 실제 데이터에 관한 포인터가 반환되었는지 또는 데이터 사본이 반환되었는지에 따라 달라집니다.
0
- 실제 데이터: 배열 객체가 고정 취소됩니다.
- 사본: 데이터가 다시 복사됩니다. 사본이 있던 버퍼가 해제됩니다.
JNI_COMMIT
- 실제 데이터: 아무 작업도 수행되지 않습니다.
- 사본: 데이터가 다시 복사됩니다. 사본이 있던 버퍼가 해제되지 않습니다.
JNI_ABORT
- 실제 데이터: 배열 객체가 고정 취소됩니다. 이전 쓰기가 중단되지 않습니다.
- 사본: 사본이 있는 버퍼가 해제되고 사본 변경사항이 손실됩니다.
isCopy
플래그를 검사하는 한 가지 이유는 배열을 변경한 후 JNI_COMMIT
을 사용하여 Release
를 호출해야 하는지 확인하기 위해서입니다. 배열을 변경하는 작업과 배열의 콘텐츠를 사용하는 코드를 실행하는 작업을 번갈아 하는 경우 작동하지 않는 커밋을 건너뛰어도 됩니다. 플래그를 검사하는 또 다른 이유는 JNI_ABORT
를 효율적으로 처리하기 위해서입니다. 예를 들어 배열을 가져와 적절하게 수정하고 배열 조각을 다른 함수에 전달한 후 변경사항을 삭제하는 것이 좋습니다. JNI에서 새로운 사본을 만들고 있다는 것을 알고 있다면 다른 '편집 가능한' 사본을 만들 필요가 없습니다. JNI에서 원본을 전달하는 경우 고유한 사본을 만들어야 합니다.
*isCopy
가 false이면 Release
호출을 건너뛸 수 있다고 가정하는 실수를 흔히 범합니다(예제 코드에서 반복적으로 나타남). 호출은 건너뛸 수 없습니다. 복사 버퍼를 할당하지 않은 경우 원래 메모리가 고정되어야 하며 가비지 컬렉터를 통해 원래 메모리를 이동할 수 없습니다.
또한 JNI_COMMIT
플래그는 배열을 해제하지 않으므로 궁극적으로는 다른 플래그를 사용하여 Release
를 다시 호출해야 합니다.
리전 호출
데이터를 현재 위치나 다른 위치에 복사만 하려는 경우 Get<Type>ArrayElements
, GetStringChars
등의 호출 대신 사용할 수 있는 유용한 대안이 있습니다. 다음을 고려하세요.
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 호출이 예외를 throw할 수 있으나 오류를 확인하는 더 간단한 방법을 제공하는 경우도 많습니다. 예를 들어 NewString
이 NULL이 아닌 값을 반환하는 경우 예외를 확인할 필요가 없습니다. 하지만 CallObjectMethod
와 같은 함수를 사용하여 메서드를 호출하는 경우 예외가 throw되면 반환 값이 유효하지 않으므로 항상 예외를 확인해야 합니다.
해석된 코드에서 예외가 throw되는 경우 네이티브 스택 프레임이 해제되지 않으며 Android에서 C++ 예외가 지원되지 않습니다.
JNI Throw
및 ThrowNew
명령은 단순히 현재 스레드에 예외 포인터를 설정합니다. 네이티브 코드에서 관리 대상으로 돌아가면 예외가 확인되고 적절하게 처리됩니다.
네이티브 코드는 ExceptionCheck
또는 ExceptionOccurred
를 호출하여 예외를 'catch'하고 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") } }
자바
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
가 있으면 어떤 연결된 스레드에서든 사용할 수 있습니다.
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
이 표시됩니다.
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에서만 사용되는 클래스, 메소드 또는 필드를 코드 축소기가 삭제할 수 있으므로 적절한 유지 규칙을 구성하는 것이 중요합니다.
클래스 이름이 제대로 표시되는 경우 클래스 로더 문제일 수 있습니다. FindClass
는 코드와 관련된 클래스 로더에서 클래스 검색을 시작하려고 하며 다음과 같은 호출 스택을 검사합니다.
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
맨 위에 있는 메서드는 Foo.myfunc
입니다. FindClass
는 Foo
클래스와 관련된 ClassLoader
객체를 찾아서 사용합니다.
일반적으로 이 작업은 예상한 대로 실행됩니다. pthread_create
를 호출한 후 AttachCurrentThread
를 통해 연결하여 스레드를 직접 만드는 경우 문제가 발생할 수 있습니다.
이 스레드에서 FindClass
를 호출하는 경우 JavaVM이 애플리케이션과 관련된 클래스 로더가 아닌 '시스템' 클래스 로더에서 시작되므로 앱 특정 클래스를 찾으려고 하면 실패합니다.
이 문제를 해결할 수 있는 몇 가지 방법이 있습니다.
JNI_OnLoad
에서FindClass
를 한 번 검색하고 나중에 사용할 수 있도록 클래스 참조를 캐시합니다.JNI_OnLoad
를 실행하는 과정에서 이루어진FindClass
호출은System.loadLibrary
를 호출한 함수와 관련된 클래스 로더를 사용합니다. 이 특수 규칙은 보다 편리한 라이브러리 초기화를 위해 제공됩니다. 앱 코드가 라이브러리를 로드하는 경우FindClass
는 올바른 클래스 로더를 사용합니다.- Class 인수를 사용할 네이티브 메서드를 선언한 다음
Foo.class
를 전달하여 클래스 인스턴스를 필요한 함수에 전달합니다. ClassLoader
객체 참조를 편리한 위치에 캐시하고loadClass
를 직접 호출합니다. 이 경우 추가 작업이 필요합니다.
FAQ: 원시 데이터를 네이티브 코드와 공유하려면 어떻게 해야 하나요?
관리 코드와 네이티브 코드의 원시 데이터가 모두 포함된 대규모 버퍼에 액세스해야 하는 경우도 있습니다. 일반적인 예로, 비트맵 또는 사운드 샘플을 조작하는 경우입니다. 기본 방법에는 두 가지가 있습니다.
byte[]
에 데이터를 저장할 수 있습니다. 관리 코드에서 빠르게 액세스할 수 있는 방법입니다. 그러나 네이티브 코드의 경우 데이터를 복사하지 않으면 데이터에 액세스하지 못할 수도 있습니다. 일부 구현에서는 GetByteArrayElements
및 GetPrimitiveArrayCritical
이 관리형 힙의 원시 데이터에 대한 실제 포인터를 반환하지만, 네이티브 힙에 버퍼를 할당하고 데이터를 복사하는 구현도 있습니다.
또는, 직접 바이트 버퍼에 데이터를 저장할 수 있습니다. java.nio.ByteBuffer.allocateDirect
또는 JNI NewDirectByteBuffer
함수를 사용하여 만들 수 있습니다. 일반 바이트 버퍼와 달리, 관리형 힙에 저장소가 할당되지 않으며 항상 네이티브 코드에서 직접 액세스할 수 있습니다(GetDirectBufferAddress
를 사용하여 주소를 가져옴). 직접 바이트 버퍼 액세스가 구현된 방식에 따라 관리 코드에서 데이터에 액세스하는 속도가 매우 느릴 수 있습니다.
다음 두 가지 요인에 따라 사용할 방법을 선택해야 합니다.
- 대부분의 데이터 액세스가 자바 또는 C/C++로 작성된 코드에서 발생합니까?
- 데이터가 궁극적으로 시스템 API에 전달되는 경우 어떤 형식이어야 합니까? 예를 들어 데이터가 궁극적으로 byte[]를 사용하는 함수에 전달되는 경우 직접
ByteBuffer
에서 처리하는 것은 부적절할 수 있습니다.
어떤 방법이 더 적합한지 확실하지 않은 경우 직접 바이트 버퍼를 사용합니다. 직접 바이트 버퍼 지원이 JNI에 직접 빌드되었으며, 향후 릴리스에서는 성능이 향상될 것입니다.