Art of Pr0gr4m

NDK를 이용한 애플리케이션 만들기 (ndk-build) 본문

IT/Android

NDK를 이용한 애플리케이션 만들기 (ndk-build)

pr0gr4m 2016. 11. 8. 06:42

이 전 포스트에서 안드로이드 스튜디오 2.2 상위 버전에서 CMake를 이용하여 애플리케이션 만드는법을 알아보았습니다.

프로젝트 생성하는데 있어서는 Wizard에서 C++ Include에 체크만 해주면 프로젝트 스켈레톤이 제공되므로 무지 간단하고, 만드는 프로젝트의 확장 정도가 기본 스켈레톤에서 크게 벗어나지 않으면 CMakeList.txt 스크립트 수정도 어렵지 않아 쉽게 이용할 수 있습니다.

허나, 외부 라이브러리가 추가된다거나 여러 모듈을 만들고 모듈별로 종속성을 생각해야 한다던가 등 확장 정도가 커지면 기존에 리눅스에서 CMake를 사용하시던 분들 말고는 예전 방식인 ndk-build에 비해 어렵다 느끼고 헤매실 수 있습니다.

따라서 이번엔 ndk-build를 이용하는 - Android.mk와 Application.mk 빌드 스크립트를 이용하는 - 애플리케이션 만들기 포스팅을 하려합니다.


사실 안드로이드 스튜디오 2.2버전부터는 이 전 버전 보다 Gradle에 외부 빌드 스크립트 추가하는 방법이 간단해져서 UI를 사용하던 직접 내용을 수정하던 어렵지 않게 프로젝트를 구성할 수 있게 되었습니다. javah로 헤더파일 만들고 ndk-build 커맨드로 빌드해서 ndk 프로젝트를 추가하는 방식 대신에, Android.mk 빌드 스크립트만 링크하면 프로젝트 구성을 끝낼 수 있기 때문에 사실 이 전 포스트의 번역 부분만 보고도 직접 만드실 수 있긴 할겁니다.




1. 프로젝트 생성

(NDK 설치는 이 전 포스트를 참고해주세요)


일반적인 SDK로 애플리케이션 만드는 것과 같이 프로젝트 Wizard 내용을 전부 디폴트로 (혹은 원하시는 대로) 설정하고 끝내주시면 됩니다.

주의할 점은 Include C++ Support 란에 체크하지 않으셔야 합니다. (체크하셨다면 나중에 수정작업이 필요합니다.)



일반적인 프로젝트 생성과 전혀 다름이 없으므로 아무런 문제가 없을겁니다.



2. JNI 디렉토리 생성


우선 기본적으로 만들어진 프로젝트를 빌드하고 실행시켜서 프로젝트에 문제가 없는지 확인합니다.

확인이 되었으면 IDE 좌측 Project pane에서 View를 Project로 바꾸고, app > src > main 디렉토리를 우클릭하여 새로운 디렉토리를 만들어줍시다. 디렉토리 이름은 jni로 정해줍시다.




3. javah를 이용한 헤더 파일 만들기


과거에는 필요했지만 현재는 스킵해도 되는 단계입니다.

그래도 필요하신 분이 있을 수 있기에 가이드는 생략하지 않겠습니다.


직접 javah 커맨드를 쳐도 되겠지만, 그러기엔 커맨드가 살짝 복잡하고 귀찮기도 합니다.

역시 안전하고 편한길이 좋기 때문에 안드로이드 스튜디오의 external tool UI를 이용하겠습니다.


우선 external tool을 등록해야 하기 때문에 안드로이드 스튜디오의 설정에 들어가야 합니다.

File > Settings 를 직접 눌러주셔도 되고 컴팩트하게 Ctrl + Alt + S 단축키를 눌러주셔도 됩니다.


Settings의 좌측에서 Tools 카테고리의 External Tools를 선택해주시고, 여기에 NDK 툴을 두개 등록할 겁니다.

저는 이미 등록이 되어있기 때문에 아래 그림처럼 보이는게 있지만 아무것도 등록한 툴이 없다면 내용이 비어있을겁니다.

거기서 + 모양의 Add를 클릭해주시면 됩니다.



Create Tool 창이 뜨면 다음과 같은 내용을 입력해주시면 됩니다.

(저는 이미 등록한 내용을 오픈한거라 Edit Tool 창으로 나옵니다.)

javah 등록이 끝났으면 똑같은 방식으로 ndk-build 등록도 해주시면 됩니다.



참고로 위 사진에서 Name은 등록한 프로그램의 식별자로 사용되며 다른 툴 등록한 이름과 중복되지만 않게 (그리고 자신이 알아볼 수 있게) 써주시면 됩니다. Group은 작성한 이름에 해당하는 그룹이 없으면 새로 만들어지고 있으면 해당 그룹에 들어가게 됩니다. 둘 다 NDK로 통일해 줍시다.


중요한 부분은 Tool settings인데 Program, Parameters, Working directory 세 개가 있습니다.

이 중에서 Parameters와 Working directory는 그냥 위에 보이시는 대로 따라 써주시면 됩니다.

오타를 조심해야 되는 부분이라 복사 붙여넣기를 할 수 있도록 따로 텍스트로 빼두겠습니다.


 -classpath $Classpath$ -v -jni $FileClass$

 $ProjectFileDir$\app\src\main\jni

 $ProjectFileDir$\app\src\main\jni


그런데 Program에는 로컬에 존재하는 프로그램을 직접 등록해야 하기 때문에 위와 경로가 달라질 수 있습니다.

우측의 브라우징 버튼을 클릭하여 필요한 프로그램이 존재하는 Path를 지정해줍시다.

javah는 (다운로드 받은 JDK 경로)\bin 디렉토리에 파일명 javah.exe로 존재하고

ndk-build는 (안드로이드 SDK 설치 경로)\ndk-bundle 디렉토리에 파일명 ndk-build.cmd로 존재합니다.

안드로이드 SDK 설치 경로는 디폴트 경로가 윈도우 기준 C:\Users\사용자명\AppData\Local\Android\sdk 인데

보통 AppData 폴더는 숨김이 되어 있습니다. 따라서 SDK 설치 경로를 변경하신 분이면 자신이 설치한 경로로 직접 찾아가 주시고, 디폴트 경로에 설치하신 분이라면 다음 그림과 같이 Select Path에 직접 경로를 입력하시는게 찾기 편하실 겁니다.



external tool 등록이 끝났다면 안드로이드 스튜디오 UI로 간단하게 등록한 툴들을 이용할 수 있습니다.

javah는 아래 그림과 같이 NDK를 사용할 자바 소스 파일을 우클릭하여 등록한 javah를 그냥 클릭해주시면 됩니다.



그러면 exit code 0와 함께 JNI를 사용하는데 있어 필요한 내용들이 담겨져 있는 헤더 파일이 JNI 네이밍 규칙에 맞게 만들어 집니다.

JNI 이론과 네이밍 규칙은 잠시 뒤로 미뤄두고, 헤더 파일이 만들어 졌다는 것만 일단 확인해주시면 됩니다.



4. 네이티브 소스 파일 생성


그럼 이제 네이티브 라이브러리를 만들기 위한 소스 파일을 만들 차례입니다.

jni 디렉토리에다가 소스파일을 넣어주시면 되는데 이 때 c 소스 파일과 cpp 소스 파일 중 어떤 소스 파일을 만드냐에 따라 JNI 함수 코딩 스타일이 달라집니다. 이에 대한 자세한 내용은 나중에 보도록 하고, 우선 저희는 c 소스파일을 만들기로 합시다.



위 사진처럼 jni 디렉토리에 소스 파일을 만들고 javah로 만든 헤더 파일을 인클루드 해준 다음에

소스 코드는 아래와 같이 작성해 주시면 됩니다.

#include "com_android_hellojni_MainActivity.h"

JNIEXPORT jstring JNICALL Java_com_android_hellojni_MainActivity_nativeString(JNIEnv *env, jobject obj)
{
return (*env)->NewStringUTF(env, "Hello JNI!");
}

기본적인 JNI 네이밍 룰에 맞게 만들어진 함수입니다. JNI의 기본적인 원리는 내용이 많기 때문에 따로 포스팅을 하기로 하고, 간단하게 룰만 설명하겠습니다.


JNI 네이티브 함수 프로토타입은 다음과 같은 규칙에 맞게 작성해야 합니다.



JNIEXPORT 반환타입 JNICALL Java_패키지명_클래스명_함수명(JNIEnv *, jobject, ...);



여기서 패키지명_클래스명 부분은 javah로 만들어진 헤더 파일 이름과 같기 때문에 그냥 헤더 파일 이름을 그대로 사용하시는게 가장 좋습니다. 그리고 JNIEXPORT와 JNICALL은 JNI에서 인식하는 매크로인데 실제로 없어도 잘 동작합니다. 즉 위의 프로토타입을 간략화하면 다음과 같습니다. (물론 javah를 사용하지 않으셨다면 그냥 위 규칙대로 작성해주시면 됩니다.)



반환타입 Java_헤더파일명_함수명(JNIEnv *, jobject, ...);



여기서 JNIEnv *와 jobject 두 매개변수는 공통 매개변수이기 때문에 항상 추가해줘야 하며, 자바 코드에서 전달받고 싶은 매개변수는 그 이후에 자바 네이티브 타입으로 추가해주시면 됩니다. 또한 반환 타입도 자바 네이티브 타입으로 작성해주셔서 자바 코드에서 반환받아야 하는데, 자바 네이티브 타입은 다음과 같습니다.


자바 타입 

자바 네이티브 타입 

타입 시그니처

byte

jbyte

B

short

jshort

S

int

jint

I

long

jlong

J

float

jfloat

F

double

jdouble

D

char

jchar

C

boolean

jboolean

Z

void

void

V

Object

jobject

L<class>;

String

jstring

Ljava/lang/String;

Class

jclass

 

Array

j<type>Array

[<type>


참고로 타입 시그니처는 추후 네이티브 코드에서 자바 메소드 호출 등에서 메소드 서명에 사용됩니다.


결국 제가 작성한 소스 코드는 com_android_hellojni 패키지의 MainActivity 클래스에서 String nativeString(); 메소드와 맵핑되는 네이티브 함수를 만든 것입니다. 함수 내용은 단순히 "Hello JNI!"라는 문자열을 포함한 String을 만들어서 반환한 것이구요.



5. Android.mk 빌드 스크립트 만들기


리눅스 환경에 익숙하신 분들이라면 Makefile 빌드 스크립트를 자주 만들어보셨을 겁니다.

그것과 비슷하게 ndk-build를 하기 위해서는 Android.mk 빌드 스크립트 파일이 필요합니다.

단순히 File로 추가하시고 파일 이름은 Android.mk 라고 지정해주시면 됩니다.



빌드 스크립트 구성은 다음과 같습니다.

# 소스 파일의 위치를 지정
LOCAL_PATH := $(call my-dir)

# LOCAL 값 중복 제거
include $(CLEAR_VARS)

# 모듈 이름 지정
LOCAL_MODULE := native-lib

# 소스 파일 지정
LOCAL_SRC_FILES := nativeMain.c

# 공유 라이브러리로 빌드
include $(BUILD_SHARED_LIBRARY)

다른 부분은 그대로 사용하고 모듈 이름은 원하시는 이름을 지정, 소스 파일은 만든 네이티브 소스 파일의 이름을 지정하시면 됩니다.

자세한 스크립트 파일 규칙은 링크를 참고해 주세요.

참고로 특정 ABI 타게팅을 원하신다면 다음 링크와 이 전 포스트의 Specify ABIs를 참고해주세요.


빌드 스크립트 생성이 끝났다면 Gradle에 해당 스크립트를 링크해주면 됩니다.

앱 모듈의 Gradle 파일을 직접 수정해도 되지만 UI를 사용하는게 더 쉽고 안전합니다.



우선 위 그림과 같이 jni 디렉토리를 우클릭하여 Link C++ Project with Gradle을 클릭해줍니다.

그 후 dialog에서 Build System을 ndk-build로 설정하고 방금 만든 Path에 방금 만든 Android.mk 파일의 경로를 지정해줍시다.



정상적으로 링크가 되었다면 자동으로 Gradle 파일이 수정되고 Sync 되었을 겁니다.




6. ndk-build


이전에 등록했던 ndk-build tool을 이용해서 만든 네이티브 소스 파일을 빌드하여 공유 라이브러리를 만들어 줍시다.

사실 굳이 ndk-build를 따로 할 필요 없이 안드로이드 스튜디오의 Build를 누르면 외부 네이티브 프로젝트는 ndk 빌드를 해주지만

ndk-build를 사용해야지 더욱 자세한 네이티브 빌드 내용을 볼 수 있습니다. (즉, 3번 단계를 스킵하셨다면 같이 스킵하셔도 되는 단계입니다.)

아무튼 jni 디렉토리를 우클릭하여 javah를 사용한것과 똑같이 ndk-build를 사용해주시면 됩니다.



별 이상이 없다면 위와 같이 모든 아키텍처를 타겟으로 공유 라이브러리가 만들어진 것을 볼 수 있습니다.

(특정 아키텍처만 타겟으로 라이브러리를 만들고 싶다면 Application.mk를 사용하세요)



7. 자바 코드 수정


이제 MainActivity 클래스에서 만든 네이티브 라이브러리의 네이티브 함수를 사용해야 합니다.


우선 네이티브 메소드를 선언하고, System.loadLibrary 메소드를 이용하여 만들어진 라이브러리를 메모리에 로드한 후, 네이티브 메소드를 사용하면 됩니다. 결국 아래와 같은 소스 코드로 구성됩니다.


public class MainActivity extends AppCompatActivity {

static {
System.loadLibrary("native-lib"); // 네이티브 라이브러리 로드
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView tv = (TextView) findViewById(R.id.tv);
tv.setText(nativeString()); // 네이티브 메소드 사용
}

public native String nativeString(); // 네이티브 메소드 선언
}

물론 xml에는 텍스트를 띄우기 위한 텍스트뷰에 tv로 아이디 값은 따로 줬습니다.



8. 빌드 및 실행


이제 마지막으로 앱 모듈 전체 빌드를 진행하고 실행을 시키시면 끝이 납니다.

아래는 제 실행 결과입니다.



도중에 이상이 없었다면 원하는 결과를 보셨을 겁니다.



9. 마무리


안드로이드 NDK에서 사용하는 JNI는 사실 안드로이드용이 아니더라도 애초에 자바 코드가 JVM에서 동작할 때 네이티브 라이브러리를 사용할 수 있도록 한 프로그래밍 인터페이스입니다.

이에 대한 이론적인 내용을 설명하려면 포스트가 너무 길어지기 때문에 일단은 당장 필요한 네이밍 룰 등만 본문에 소개하였습니다. (사실 단순히 네이티브 라이브러리를 만들어서 사용하는데 있어서는 인터페이스만 아는것으로도 충분히 사용하실 수 있습니다.)


안드로이드 애플리케이션은 기본적으로 JAVA로 작성 되는게 옳습니다. 그럼에도 NDK를 이용하는 대표적인 이유는 다음과 같습니다.


- 기존 C/C++로 작성된 프로그램의 재사용

- 빠른 처리 속도를 요구하는 루틴 작성

- 하드웨어 제어


저 같은 경우엔 첫 번째와 두 번째 이유 때문에 NDK를 사용하고 있습니다. 하지만 사용할수록 JNI에 대해서 공부할게 많아지고 복잡도가 증가함에 따라 헤매는 부분이 늘어나고 있습니다. '난 자바는 잘 모르니 그냥 C/C++로 앱 만들어야지~' 하는 생각으로 건들다간 데일 확률이 무지 크니, 조심하시기 바랍니다.


아, 그리고 애플리케이션에서 지정된 퍼미션이 네이티브에서도 똑같이 적용되므로 파일 오픈을 한다던가 소켓 프로그래밍을 하려면 매니페스트에 다은과 같이 퍼미션 요청을 추가해줘야 합니다.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

그러면 다음 포스트에는 기존에 만든 네이티브 라이브러리를 추가하는 법과 크로스 컴파일을 이용해서 PC에서 만든 네이티브 라이브러리를 추가하는 법을 알아보겠습니다. 감사합니다.

'IT > Android' 카테고리의 다른 글

NDK를 이용한 애플리케이션 만들기 (CMake)  (0) 2016.11.05