JNI 도움말

JNI는 Java Native Interface(자바 네이티브 인터페이스)의 약어입니다. Android가 컴파일하는 바이트 코드 방법을 정의합니다. 네이티브 코드와 상호작용하기 위한 관리형 코드 (Java 또는 Kotlin 프로그래밍 언어로 작성됨) (C/C++로 작성됨) JNI는 공급업체 중립적이며 동적 공유에서 코드 로드를 지원합니다. 때로는 번거롭지만 합리적으로 효율적입니다.

참고: Android는 Kotlin을 ART 친화적인 바이트 코드로 컴파일하기 때문에 Java 프로그래밍 언어와 유사한 방식으로 이 페이지의 지침을 JNI 아키텍처 및 관련 비용 측면에서 Kotlin 및 Java 프로그래밍 언어를 알아봅니다. 자세한 내용은 Kotlin 및 Android

이 기능에 대해 자세히 알아보려면 자바 네이티브 인터페이스 사양 JNI의 작동 방식과 사용할 수 있는 기능을 알아보세요. 다소 유용함 인터페이스 측면이 눈에 잘 띄지 않습니다. 다음 몇 개의 섹션이 유용할 수 있습니다.

글로벌 JNI 참조를 살펴보고 글로벌 JNI 참조가 생성되고 삭제되는 위치를 확인하려면 다음을 사용합니다. 메모리 프로파일러JNI heap 뷰 .

일반적인 도움말

JNI 레이어의 공간을 최소화합니다. 여기서 고려해야 할 몇 가지 측정기준이 있습니다. JNI 솔루션은 이러한 가이드라인 (중요도순, 가장 중요한 것부터 시작하겠습니다.)

  • JNI 레이어에서 리소스의 마샬링을 최소화합니다. 마샬링(Marshall) JNI 레이어에는 적지 않은 비용이 발생합니다. 입력 데이터의 양을 최소화하는 마샬링해야 하는 데이터와 데이터를 마샬링해야 하는 빈도를 나타냅니다.
  • 관리형 프로그래밍으로 작성된 코드 간의 비동기 통신 방지 가능한 경우 C++로 작성된 언어와 코드 이렇게 하면 JNI 인터페이스를 더 쉽게 유지 관리할 수 있습니다. 일반적으로 비동기식으로 비동기 업데이트를 UI와 동일한 언어로 유지하여 UI 업데이트 예를 들어 JNI를 통해 Java 코드의 UI 스레드에서 C++ 함수를 호출하는 경우 Java 프로그래밍 언어로 된 두 스레드 간에 콜백을 실행합니다. 차단 C++ 호출을 수행한 다음 차단 호출이 사용 중지될 때 UI 스레드에 알림 합니다.
  • JNI를 터치하거나 터치해야 하는 스레드 수를 최소화합니다. Java와 C++ 언어 모두에서 스레드 풀을 사용해야 하는 경우 JNI를 유지해 보세요. 개별 작업자 스레드가 아닌 풀 소유자 간의 통신을 지원합니다.
  • 쉽게 식별할 수 있는 적은 수의 C++ 및 Java 소스에 인터페이스 코드를 유지합니다. 향후 리팩터링을 용이하게 할 수 있습니다. JNI 자동 생성 사용 고려 적절하게 선택합니다.

JavaVM 및 JNIEnv

JNI는 두 개의 주요 데이터 구조인 'JavaVM'과 'JNIEnv'를 정의합니다. 이 두 가지 모두 기본적으로 함수 테이블에 대한 포인터에 대한 포인터입니다. (C++ 버전에서 이러한 라이브러리는 다음을 통해 간접적으로 지시하는 각 JNI 함수의 함수 테이블 및 멤버 함수에 대한 포인터 를 참조하세요.) JavaVM은 '호출 인터페이스'를 제공 함수, JavaVM을 만들고 제거할 수 있게 해줍니다. 이론적으로는 프로세스당 여러 개의 JavaVM이 있을 수 있지만 Android에서는 하나만 허용할 수 있습니다

JNIEnv는 대부분의 JNI 함수를 제공합니다. 네이티브 함수는 모두 첫 번째 인수(@CriticalNative 메서드 제외) 더 빠른 네이티브 호출을 확인하세요.

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

JNIEnv 및 JavaVM의 C 선언은 C++의 선언에 사용됩니다. "jni.h" include 파일은 다양한 typedef를 제공합니다. C 또는 C++에 포함되는지에 따라 달라집니다. 따라서 두 언어에서 모두 포함된 헤더 파일에 JNIEnv 인수를 포함합니다. 달리 말하면 헤더 파일에 #ifdef __cplusplus가 필요하지만 해당 헤더가 JNIEnv를 가리킴)

스레드

모든 스레드는 커널을 통해 예약된 Linux 스레드입니다. 일반적으로 관리 코드에서 시작됨 (Thread.start() 사용) 다른 곳에서 만든 다음 JavaVM에 연결할 수도 있습니다. 대상 예: pthread_create() 또는 std::thread로 시작된 스레드 AttachCurrentThread() 또는 AttachCurrentThreadAsDaemon() 함수. 대화목록이 연결된 경우 JNIEnv가 없으며 JNI 호출을 할 수 없습니다.

일반적으로 Thread.start()를 사용하여 Java 코드에 호출할 수 있습니다. 이렇게 하면 충분한 스택 공간이 확보되고 올바른 ThreadGroup로 되어 있고 동일한 ClassLoader를 사용 중인지 확인합니다. 를 사용하는 것이 좋습니다. 또한 Java에서 디버깅을 위해 스레드 이름을 설정하는 것이 네이티브 코드 (pthread_t 또는pthread_setname_np() thread_t, std::thread::native_handle()가 있는 경우 std::thread, pthread_t가 필요한 경우).

기본적으로 생성된 스레드를 연결하면 java.lang.Thread이 발생합니다. 객체를 생성하여 'main'에 ThreadGroup, 디버거에 표시됩니다. AttachCurrentThread()님에게 전화 거는 중 '노옵스(no-ops)'라는 메시지가 표시됩니다.

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

JNI를 통해 연결된 스레드는 DetachCurrentThread()를 반환합니다. 이를 직접 코딩하는 것이 어색하다면 Android 2.0 (Eclair) 이상에서 pthread_key_create()를 사용하여 소멸자를 정의할 수 있음 스레드를 종료하기 전에 호출할 함수와 거기에서 DetachCurrentThread()를 호출합니다. ( 키를 pthread_setspecific() 사용하여 thread-local-storage; 이렇게 하면 합니다.)

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

자바

    /*
     * 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 메서드를 만듭니다. 코드 클래스가 초기화될 때 한 번 실행됩니다. 클래스의 로드가 해제되고 새로고침하면 다시 실행됩니다

로컬 참조 및 글로벌 참조

네이티브 메서드에 전달되는 모든 인수와 거의 모든 객체가 반환됨 '로컬 참조'입니다. 이는 현재 스레드의 현재 네이티브 메서드의 지속 시간을 나타냅니다. 네이티브 메서드 이후에 객체 자체가 계속 실행되더라도 가 반환되면 참조가 유효하지 않습니다.

이는 다음을 포함한 jobject의 모든 서브클래스에 적용됩니다. jclass, jstring, jarray 확장된 JNI를 사용할 때 런타임에서 대부분의 참조 오용에 대해 경고합니다. 사용 설정되어 있는지 확인).

비로컬 참조를 얻는 유일한 방법은 NewGlobalRefNewWeakGlobalRef

참조를 더 오래 보관하려면 '전역' 참조 NewGlobalRef 함수는 로컬 참조를 인수로 사용하고 전역 참조를 반환합니다. 전역 참조는 DeleteGlobalRef

이 패턴은 일반적으로 반환된 jclass를 캐시할 때 사용됩니다. FindClass에서. 예:

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

모든 JNI 메서드는 로컬 참조와 글로벌 참조를 모두 인수로 허용합니다. 동일한 객체의 참조 값이 서로 다를 수 있습니다. 예를 들어 동일한 객체의 NewGlobalRef는 다를 수 있습니다. 두 참조가 동일한 객체를 참조하는지 확인하려면 IsSameObject 함수를 사용해야 합니다. 비교 안함 네이티브 코드에서 ==를 사용하여 참조

이렇게 하면 객체 참조가 상수 또는 고유하다고 가정해서는 안 됨 사용할 수 있습니다. 객체를 나타내는 값은 다를 수 있습니다. 한 메서드 호출에서 다음 호출로 전환하는데, 두 번이 동일한 메서드를 서로 다른 객체가 연속 호출에서 동일한 값을 가질 수 있습니다. 사용 안함 jobject 값을 키로 사용합니다.

프로그래머는 로컬 참조를 '과도하게 할당하지 않도록' 해야 합니다. 실질적으로 이것은 많은 수의 로컬 참조를 만드는 경우(예: 객체를 직접 확보해야 하는 경우 JNI를 통해 자동으로 처리하도록 하는 대신 DeleteLocalRef를 사용합니다. 이 구현이 필요한 경우에만 로컬 참조는 16개이므로 그 이상이 필요한 경우 이동하면서 삭제하거나 추가 예약은 EnsureLocalCapacity/PushLocalFrame입니다.

jfieldIDjmethodID는 불투명합니다. 객체 참조가 아닌 유형이며 NewGlobalRef입니다. 원시 데이터 GetStringUTFChars와 같은 함수에서 반환된 포인터 및 GetByteArrayElements도 객체가 아닙니다. (이는 일치하는 Release 호출 시까지 유효합니다.)

단, 한 가지 특별한 경우가 있습니다. 네이티브 광고에 AttachCurrentThread를 갖는 스레드를 사용하는 경우 실행 중인 코드는 스레드가 분리될 때까지 로컬 참조를 자동으로 해제하지 않습니다. 모든 로컬 새로 만든 참조는 수동으로 삭제해야 합니다. 일반적으로 모든 네이티브 광고는 루프에서 로컬 참조를 만드는 코드는 몇 가지 수동 작업을 해야 할 수 있음 합니다.

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

UTF-8 및 UTF-16 문자열

자바 프로그래밍 언어는 UTF-16을 사용합니다. 편의상 JNI는 수정된 UTF-8도 있습니다. 이 수정된 인코딩은 \u0000을 0x00 대신 0xc0 0x80으로 인코딩하기 때문에 C 코드에 유용합니다. 이 방법의 장점은 C 스타일 0으로 끝나는 문자열을 사용할 수 있다는 것입니다. 표준 libc 문자열 함수에 사용하기에 적합합니다. 단점은 모든 API를 올바르게 작동할 것으로 예상할 수 있습니다.

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

Get하는 문자열은 Release해야 합니다. 이 문자열 함수는 jchar* 또는 jbyte*를 반환합니다. 는 로컬 참조가 아닌 원시 데이터에 대한 C 스타일 포인터입니다. 그들은 Release가 호출될 때까지 유효하므로 해제되어야 합니다.

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

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

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

프리미티브 배열

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에 전화해야 합니다. - 값을 번갈아 가며 반복하는 경우 코드를 변경하고 실행하는 경우 할 수 있는 노옵스(no-ops) 커밋을 건너뜁니다 플래그를 확인하는 또 다른 이유는 JNI_ABORT를 효율적으로 처리할 수 있습니다. 예를 들어 배열을 가져오고, 그 자리에서 수정하고, 조각을 다른 함수에 전달합니다. 변경사항을 삭제합니다 JNI가 별도의 '수정 가능' 복사합니다. JNI 원본이 아닌 경우 나름대로 사본을 만들어야 합니다.

다음과 같은 경우 Release 호출을 건너뛸 수 있다고 가정하는 것은 흔한 실수입니다 (예시 코드에서 반복됨). *isCopy가 거짓입니다. 호출은 건너뛸 수 없습니다. 복사 버퍼가 없는 경우 원래 메모리는 고정되어야 하며 가비지 컬렉터입니다.

또한 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 호출이 예외를 발생시킬 수 있지만 더 간단한 방법을 제공하는 경우가 많습니다. 살펴봤습니다 예를 들어 NewString이 NULL이 아닌 값이면 예외를 확인할 필요가 없습니다. 그러나 CallObjectMethod와 같은 함수를 사용하여 메서드를 호출합니다. 항상 예외를 확인해야 합니다. 반환 값이 예외가 발생한 경우 유효합니다.

관리형 코드에서 발생한 예외는 네이티브 스택을 해제하지 않습니다. 있습니다. (그리고 Android에서 일반적으로 권장되지 않는 C++ 예외는 C++ 코드에서 관리 코드로 JNI 전환 경계에서 발생합니다.) JNI ThrowThrowNew 명령은 현재 스레드에 예외 포인터를 설정해야 합니다. 관리형 서비스로 돌아가면 네이티브 코드에서 발생하는 경우 예외가 기록되고 적절하게 처리됩니다.

네이티브 코드가 '포착'할 수 있음 ExceptionCheck를 호출하거나 ExceptionOccurred로 시작하고 ExceptionClear입니다. 평소와 같이 예외를 처리하지 않고 예외를 삭제하면 문제가 발생할 수 있습니다.

Throwable 객체를 조작하는 기본 제공 함수가 없습니다. 따라서 예외 문자열을 가져오려면 Throwable 클래스를 찾고 getMessage "()Ljava/lang/String;"를 호출하고, 그 결과가 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) 호출 이니셜라이저를 사용하세요. 인수는 'undecorated'입니다. 라이브러리 이름, 따라서 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 함수는 다음과 같이 표시됩니다. 다음과 같습니다.

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는 Java 스택 또는 Java 스택이 없는 경우 (방금 연결된 네이티브 스레드에서 호출되었기 때문) ‘system’을 사용합니다. 클래스 로더 시스템 클래스 로더는 애플리케이션의 클래스에서 FindClass를 사용하여 자체 클래스를 찾을 수 없습니다. 있습니다. 이렇게 하면 JNI_OnLoad가 클래스를 한 번 조회하고 캐시하기에 편리한 장소가 됩니다. 유효한 jclass 글로벌 참조가 있음 모든 연결된 스레드에서 사용할 수 있습니다.

@FastNative@CriticalNative로 더 빨라진 네이티브 호출

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

@CriticalNative 주석은 다음과 같이 지원되지 않는 네이티브 메서드에만 적용될 수 있습니다. (매개변수 또는 반환 값에서 또는 암시적 this으로) 관리 객체를 사용합니다. 주석이 JNI 전환 ABI를 변경합니다. 네이티브 구현에서는 함수 서명의 JNIEnvjclass 매개변수

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

이러한 주석은 시스템 사용을 위해 구현되었습니다. Android 8 CTS 테스트를 거쳐 공개되었습니다. Android 14의 API입니다. 이러한 최적화는 Android 8~13 기기 (일부 강력한 CTS 보장 없음) 하지만 네이티브 메서드의 동적 조회는 Android 12 이상에서는 JNI RegisterNatives에 명시적 등록이 반드시 필요합니다. (Android 버전 8~11에서 실행) 이러한 주석은 ABI 불일치인 Android 7에서 무시됩니다. @CriticalNative에 대해 잘못된 인수 마샬링이 발생하고 비정상 종료가 발생할 수 있습니다.

이러한 주석이 필요한 성능이 중요한 메서드의 경우 에 의존하는 대신 JNI RegisterNatives에 메서드를 명시적으로 등록합니다. 이름 기반의 '검색' 네이티브 메서드의 최적의 앱 시작 성능을 얻으려면 @FastNative 또는 @CriticalNative 메서드의 호출자를 기준 프로필. Android 12부터 컴파일된 관리 메서드에서 @CriticalNative 네이티브 메서드를 호출하는 것은 거의 모든 인수가 레지스터에 맞는 한 (예: 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)

자바

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비트 포인터를 사용하는 아키텍처를 지원하려면long int. Java 필드에 네이티브 구조에 대한 포인터를 저장하는 경우

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

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

  • DefineClass가 구현되어 있지 않습니다. Android는 Java 바이트 코드 또는 클래스 파일이므로 바이너리 클래스 데이터 전달 작동하지 않습니다.

이전 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)부터는 약한 글로벌 참조가 사용됩니다.

  • 로컬 참조

    Android 4.0 (Ice Cream Sandwich)까지는 실제로 직접 포인터를 사용합니다. Ice Cream Sandwich에서 간접을 추가함 더 나은 가비지 컬렉터를 지원하는 데 필요하지만 이것은 이전 버전에서 감지할 수 없는 JNI 버그의 비율 자세한 내용은 <ph type="x-smartling-placeholder"></ph> 자세한 내용은 ICS의 JNI 로컬 참조 변경사항을 참고하세요.

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

  • GetObjectRefType로 참조 유형 결정

    Android 4.0 (Ice Cream Sandwich)까지는 직접 포인터를 사용하고 있다면 (위 참고) GetObjectRefType는 올바르게 입력되었습니다. 대신 휴리스틱을 논쟁, 현지 사정, 지역 사회를 살펴봤습니다 전역 변수 테이블이 이 순서로 표시됩니다. 처음에 참조가 살펴봤습니다 예를 들어, 발생한 전역 jclass에서 GetObjectRefType를 호출했습니다. jclass와 같아야 합니다. 네이티브 메서드를 사용하는 경우 JNILocalRefType 대신 JNIGlobalRefType입니다.

  • @FastNative@CriticalNative

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

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

  • FindClass에서 ClassNotFoundException 발생

    이전 버전과의 호환성을 위해 Android에서 ClassNotFoundException이 발생합니다. 다음에서 클래스를 찾을 수 없는 경우 NoClassDefFoundError 대신 FindClass입니다. 이 동작은 Java Reflection API와 일치합니다. Class.forName(name)

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 출력 확인 라이브러리 로드에 대한 메시지가 표시됩니다.
  • 이름 또는 서명 불일치로 인해 메서드를 찾을 수 없습니다. 이 일반적으로 다음과 같은 이유로 발생할 수 있습니다. <ph type="x-smartling-placeholder">
      </ph>
    • 지연 메서드 조회의 경우 C++ 함수 선언 실패 extern "C"를 적절하게 사용하여 공개 상태 (JNIEXPORT) 아이스크림을 사용하기 전에 JNIEXPORT 매크로가 잘못되었으므로 이전 jni.h는 작동하지 않습니다. arm-eabi-nm를 사용할 수 있습니다. 라이브러리에 표시되는 기호를 확인합니다. CANNOT TRANSLATE 손상 (예: _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass) Java_Foo_myfunc) 또는 기호 유형이 소문자 't' 대문자 'T'가 아닌 선언을 조정합니다.
    • 명시적 등록의 경우 메서드 서명 모든 정보를 로그 파일의 서명과 일치하는지 확인합니다. 'B'는 byte이고 'Z'는 boolean입니다. 서명의 클래스 이름 구성요소는 'L'로 시작하고 ';'로 끝납니다. '/' 사용 패키지/클래스 이름을 구분하고 '$'를 사용합니다. 구분하려면 내부 클래스 이름 (예: Ljava/util/Map$Entry;)

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

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

(이 조언의 대부분은 메서드를 찾지 못한 경우에도 동일하게 적용됩니다. GetMethodID, GetStaticMethodID 또는 필드 포함 GetFieldID 또는 GetStaticFieldID 사용)

클래스 이름 문자열의 형식이 올바른지 확인합니다. JNI 클래스 이름은 패키지 이름으로 시작하고 슬래시로 구분됩니다. java/lang/String 등). 배열 클래스를 찾는 경우 적절한 수의 대괄호로 시작하고 또한 클래스를 'L'로 래핑해야 함 즉, 1차원 배열인 String[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이 '시스템'에서 시작됩니다. 클래스 로더가 따라서 앱별 클래스를 찾으려는 시도는 실패합니다.

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

  • 다음에서 FindClass 조회를 한 번 하세요. JNI_OnLoad, 나중을 위해 클래스 참조를 캐시합니다. 사용합니다 실행의 일부로 실행된 모든 FindClass 호출 JNI_OnLoadSystem.loadLibrary를 호출한 함수입니다 (이것은 특수 규칙이 제공됩니다. 앱 코드에서 라이브러리를 로드하는 경우 FindClass 올바른 클래스 로더가 사용됩니다.
  • 클래스의 인스턴스를 필요한 함수에 전달 클래스 인수를 취하도록 네이티브 메서드를 선언하고, 그런 다음 Foo.class를 전달합니다.
  • 어딘가에 ClassLoader 객체 참조 캐시 loadClass 호출을 바로 실행할 수 있습니다. 이를 위해서는 약간의 노력이 필요합니다

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

대규모 백업이 필요할 수도 있습니다 원시 데이터의 버퍼를 제공합니다. 일반적인 예 비트맵 또는 사운드 샘플의 조작을 포함해서는 안 됩니다. 두 가지 살펴봤습니다

byte[]에 데이터를 저장할 수 있습니다. 이를 통해 액세스할 수 있습니다 하지만 네이티브 쪽에서는 복사하지 않으면 데이터에 액세스할 수 없다는 보장이 없습니다. 포함 일부 구현, GetByteArrayElementsGetPrimitiveArrayCritical는 원시 데이터를 저장하지만 다른 경우에는 버퍼를 할당합니다. 네이티브 힙에 저장하고 데이터를 복사하세요.

또는, 직접 바이트 버퍼에 데이터를 저장할 수 있습니다. 이러한 java.nio.ByteBuffer.allocateDirect로 만들 수 있습니다. JNI NewDirectByteBuffer 함수로 대체되어야 합니다. 일반과 달리 저장소가 관리되는 힙에 할당되지 않고 항상 네이티브 코드에서 직접 액세스 가능 (주소 가져오기 GetDirectBufferAddress) 직접적인 영향을 받는 바이트 버퍼 액세스가 구현되어 관리 코드의 데이터에 액세스합니다. 매우 느릴 수 있습니다

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

  1. 대부분의 데이터 액세스가 Java로 작성된 코드에서 발생할까요? 아니면 C/C++에서 어떻게 해야 할까요?
  2. 데이터가 궁극적으로 시스템 API로 전달되는 경우 어떤 형식으로 있어야 할까요? 예를 들어 데이터가 결국 byte[]를 취하여 직접 ByteBuffer는 현명하지 않을 수 있습니다.)

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