해당 포스트는 "안드로이드 ndk의 모든 것", "안드로이드 NDK 네이티브 프로그래밍" 책의 내용을 요약한 것이다.



※ JNI(Java Native Interface)

: 자바 코드가 Java VM에서 동작할 때 C/C++ 또는 어셈블리어로 작성된 네이티브 라이브러리를 사용할 수 있도록 한 프로그래밍 인터페이스이다. JNI를 통해 자바에서 native 키워드를 사용한 함수를 네이티브 라이브러리와 연결한다. 이런 역할을 통해서 안드로이드 NDK 프로그래밍을 할 수 있다.


- JNI 구현 순서

1. java 코드 작성한다.

2. javac로 소스를 컴파일해 class 파일을 얻는다.

3. 자바 class 파일에서 javah로 JNI 규약에 맞는 헤더를 생성한다.

4. 생성한 헤더 파일을 토대로 네이티브 소스(C/C++) 코드를 작성한다.

5. 네이티브 소스 파일과 헤더 파일을 컴파일해 Shared Object 라이브러리(*.so)를 만든다.

6. 안드로이드 자바 애플리케이션에 라이브러리를 포함하여 실행한다.


- 자바 코드에서 네이티브 메서드 호출

//자바 코드
public native int sum(int a, int b);
//네이티브 코드
#include 
jint Java_com_example_user_MainActivity_sum(JNIEnv *, jobject, jint, jnt);

자바 코드에서 네이티브 라이브러리와 연결하고 싶은 함수가 있으면 native 키워드를 붙여준다. 그러면 Dalvik VM이 애플리케이션을 실행할 때 라이브러리의 symbol 테이블을 검사하여 native 키워드로 선언되어 있는 함수를 찾아 동작시킨다. 또한 반드시 헤더는 C 타입으로 구성해야 한다. C++ 타입은 symbol 테이블의 함수명이 오버로딩 문제 때문에 원래 함수명에 파라미터 타입 등을 추가해서 VM이 찾을 수 없기 때문이다. VM은 symbol 테이블의 함수명을 토대로 native 함수를 찾기 때문에 네이티브 함수 명을 정확히 맞춰서 코드를 작성해야 한다. 이 때 javah로 함수명을 자동으로 생성할 수 있다. 위에서와 같이 자바 코드를 작성한 후 javah를 실행하게 되면 네이티브 함수명의 선언이 위와 같이 헤더 파일에 자동으로 생성된다. 함수를 구현할 시에는 헤더 파일에 생성된 함수명을 복사/붙여넣기 하면 된다.

함수명 형식은 'JNIEXPORT <함수 타입> JNICALL Java_<패키지 명>_<클래스 명>_<함수명>(JNIEnv*, jobject, 파라미터) 이다. 여기서 JNIEnv*, jobject는 네이티브에서 자바로 접근하는 포인트가 된다. 뒤에 파라미터는 위 자바 코드에서 인자 두 개가 된다. 'JNIEXPORT'와 'JNICALL'은 생략 가능하다. 또한 함수를 오버로딩 해야할 경우가 있다. 그럴시 함수명을 'Java_패키지명_클래스명_함수명__<aguments타입>'으로 해야한다. 예를 들어 다음과 같다.

jint Java_com_example_user_MainActivity_show__(JNIEnv *, jobject, jint);
jint Java_com_example_user_MainActivity_show__I(JNIEnv *, jobject, jint, jjnt);
jint Java_com_example_user_MainActivity_show__LJava_lang_String_2(JNIEnv *, jobject, jint, jstring);



- 네이티브 코드에서 자바 메서드 호출 

void Java_kr_or_aesop_HelloWorld_show(JNIEnv * a, jobject b) { //자바 클래스를 적재한다. jclass cls = (*a)->GetObjectClass(a,b); // c++ => a->GetObjectClass(b); //메서드 ID를 구한다. func가 함수명이면 함수 타입은 ()V다. jmethodID funcM = (*a)->GetMethodID(cls, "func", "()V"); // c++ => a->GetMethodID(a, cls, "func", "()V"); if(funcM == 0) { printf("None method"\n"); } else { //메서드를 호출한다. (*a)->CallVoidMethod(a, b, funcM);

// c++ => CallVoidMethod(b, funcM); } }

위 예를 통해 네이티브 코드에 대해서 알아볼 텐데 먼저 네이티브에서 사용되는 타입은 아래와 같다.

- boolean => jboolean(Z)

- byte => jbyte(B)

- char => jchar(C)

- short => jshort(S)

- int => jint(I)

- long => jlong(J)

- float => jfloat(F)

- double => jdouble(D)

- void => void(V)

- Object => jobject(L<class>);

- String => jstring(Ljava/lang/String;)

- Class => jclass

- Array => j<type>Array([<type>)

- Throwable => jthrowable

- jFiledID : 필드에 접근

- jmethodID : 메서드에 접근

왼쪽 타입은 자바에서 사용되는 타입이고 오른쪽은 네이티브에서 사용되는 타입이다. '()'안에 명시해 놓은 것들은 Type Signature이다. 위 코드에서 GetMetodID의 인자를 보면 "()V"라는 게 있다. 이것은 함수 타입을 나타낸다. 만약 함수 타입으로 (Ljava/lang/String;I)V라고 되어 있다면 리턴타입은 'V' 즉, void라는 뜻이다. 또한 '()안에 있는 Ljava/lang/String;은 String 객체가 함수 파라미터로 전달되었다는 뜻이고 "I"는 int 타입이 함수 파라미터로 전달되었다는 뜻이다. 위 예제에서는 함수의 반환형이 void여서 CallvoidMetod로 메서드를 호출했는 데 반환형이 int라면 CallIntMethod를 사용해야 하고 만약 메서드에 파라미터를 전달해야 할 시 CallIntMethod(a,b,funcM,(jint)num,(jint)num2);라고 하면 된다. 

위 예제는 C로 작성한 건데 C++로 작성할 때는 예제에서 주석에 명시된 걸로 바꾸면 된다. 또한 라이브러리의 심볼이 항상 C형태로 되어 있어야 해서 헤더파일안에 함수들의 선언들을 extern "C"{ 함수 선언 } 형식으로 해줘야 한다.


- 자바 배열을 C 배열로 변환하는 방법

: 'getObjectArrayElement'를 이용하여 배열을 얻을 수 있다. Object에는 Int, Boolean, Byte, Char, Long 등이 들어간다. 배열을 다 사용했다면 ReleaseObjectArrayElements()로 풀어줄 수 있다. 문자열 같은 경우 'getStringUTFChars'를 통하여 element를 C 문자열(char*)로 변환 후 메모리에 저장하고 할당된 메모리로 문자를 복사한다. 그리고 처음 얻은 배열을 해제하면 자바 형태의 배열을 C 형태의 배열로 변환하는 작업이 완료된다. 다음의 예들을 보자.

JNIEXPORT void JNICALL Java_kr_or_aesop_HelloWorld_show3(JNIEnv * a, jobject b, jintArray c)
{
   jint *A = NULL;
   A = (*a)->GetIntArrayElements(a,c,NULL);
   if (A == NULL)
        printf("Error");
   printf("show3 : %d %d %d", A[0], A[1], A[2]);
}
void Java_kr_or_aesop_HelloWorld_show6__Ljava_lang_String_2(JNIEnv * a, jobject b, jstring C)
{
  char strbuf[128];
  const char * sz = (*a)->GetStringUTFChars(a,c,0);
  strcpy(strbuf, sz);
  printf("show6 : %s", strbuf);
  (*a)->ReleaseStringUTFChars(a,c,sz);
}



※ Android.mk

리눅스에서는 Makefile로 컴파일하게 된다. 안드로이드에서는 Makefile 대신 Android.mk로 소스를 컴파일한다. 다음의 에제를 보고 Android.mk 파일에 대해서 알아보자.

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)

- LOCAL_PATH := $(call my-dir)

: Android.mk 파일은 반드시 LOCAL_PATH 변수를 정의하면서 시작해야 한다. 이 것은 소스 파일의 위치를 지정하는 것이다. my-dir은 현재 디렉토리를 리턴하는 매크로이다. 매크로함수는 $(call <function>) 형태로 되어 있다. function에는 현재 Android.mk 파일이 있는 경로를 리턴해 주는 'my-dir', 현재 디렉토리 아래에 있는 모든 Android.mk 리스트를 리턴하는 'all-subdir-makefiles', 현재 Makefile의 경로를 리턴하는 'this-makefile' 등이 있다.

- include $(CLEAR_VARS) 

: LOCAL_PATH 이외의 다른 LOCAL_XXX변수(LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES 등)를 지운다. 라이브러리를 여러 개 사용해 먼저 읽은 Android.mk의 LOCAL_XXX값이 중복되어 사용되는 것을 방지하기 위해 반드시 정의한다.

- LOCAL_MODULE := hello-jni

: 라이브러리 모듈 이름을 지정한다. Android.mk에 반드시 설정해줘야 한다. NDK 빌드 시스템은 여기에서 주어진 이름에 접두사와 접미사를 자동으로 부여하며, libHelloNDK.so 공유 라이브러리를 생성한다. 

- LOCAL_SRC_FILES := hello-jni.c 

: C 또는 C++ 소스 파일 목록을 저장한다. 헤더는 포함하지 않는다.

- include $(BUILD_SHARED_LIBRARY)

: include $(CLEAR_VARS)를 선언한 이후의 LOCAL_XXX 변수를 모두 모아서 적용하고 빌드한다. 만약 BUILD_SHARED_LIBRARY 변수가 아닌 BUILD_STATIC_LIBRARY를 사용할 시 정적 라이브러리가 만들어진다. 다만 NDK는 결과물로 so로만 나온다. 따라서 BUILD_STATIC_LIBRARY 로 지정한다고 해서 .a 파일이 생성되지 않는다. 단지 Shared Object를 만들기 위해서 정적 라이브러리를 지원한다.


※ NDK 로그 출력

자바 SDK에서 Log.e(태그, 로그 메시지);로 로그를 사용할 수 있는 것처럼 NDK도 로그 출력이 가능하다. 우선 Android.mk에 LOCAL_LDLIBS := -llog 를 추가한다. LOCAL_LDLIBS 는 라이브러리를 빌드할 때 필요한 추가 링크 플래그를 지정한다.  코드에는 #include <android/log.h>를 추가해 log.h 파일을 포함한다. 그리고 원하는 곳에 다음 코드를 사용해 로그를 출력한다.

__android_log_print(ANDROID_LOG_INFO, "---------", "AAAAA");

__android_log_print 함수는 printf처럼 사용할 수 있는 함수이다. __android_log_write는 간단한 문자열을 출력할 때 사용하고 __android_log_vprint는 va_list를 사용할 수 있다. 로그의 유형 또한 ANDROID_LOG_INFO 외에 ANDROID_LOG_WARN, ANDROID_LOG_ERROR 등이 있다.

'안드로이드 > 기본' 카테고리의 다른 글

NDK(3) - 라이브러리 의존성 문제  (0) 2017.07.01
NDK(2) - 안드로이드 스튜디오 NDK JNI 기본 예제  (2) 2017.07.01
안드로이드 mvp  (0) 2017.06.15
안드로이드 mvc, mvp, mvvm  (0) 2017.06.09
SharedPreferences  (0) 2017.06.04

+ Recent posts