배고픈 개발자 이야기
[Android Studio] JNI 사용하여 프로젝트 디버깅 해보기 본문
이 포스팅은 안드로이드 스튜디오에서 JNI로 (C/C++)코드를 디버깅 해야할 상황이 생겨 JNI를 포함한 프로젝트 생성 및 간단한 디버깅 테스트 하는 방법을 기록하는 목적으로 작성합니다.
JNI?
JNI란 Java Native Interface의 약자로, 자바 외의 다른 언어들(C/C++ 등)과 자바 사이에 연결을 위한 인터페이스를 뜻합니다. 말 그대로, 자바에서 C/C++을 사용할 수도 있고, 반대로 C/C++에서 자바를 사용할 수도 있습니다.
안드로이드에서 NDK(Native Development Kit)를 제공하여 Native언어를 사용할 수 있도록 지원하고 있습니다.
NDK tool 설치가 되어있다면 아래 그림과 같이, File > Project Structure창을 띄워 ndk-bundle path 설정을 해주세요.
이로써 jni 사용을 위한 준비는 모두 끝났습니다.
JNI 사용하기
모든 준비를 끝마치셨다면, 이제 실제 Native 언어를 작성하여 안드로이드에 어떻게 적용 시켜야 되는지 알아보겠습니다.
예제로 C언어로 구성된 덧셈 함수를 호출하여 연산하도록 구성해보겠습니다.
일단 프로젝트를 하나 생성하여 레이아웃부터 구성해보겠습니다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context="rebuild.com.sample_jni.MainActivity">
<TextView
android:id="@+id/txt_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30dp" />
</LinearLayout>
간단하게 합산 결과를 표시 할 TextView 하나만 배치 하였습니다.
바로 이어서 Activity 코드를 작성해보겠습니다.
MainActivity.java
package com.tistory.testjni;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView txt_result;
static {
System.loadLibrary("jniCalculator");
}
public native int getSum(int num1, int num2);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text_result = (TextView) findViewById(R.id.txt_result);
int num1 = 10;
int num2 = 20;
int sum = getSum(num1, num2);
txt_result.setText("JNI Sample :: 합계:" + sum);
}
}
System.loadLibrary("jniCalculator") C언어로 작성된 모듈을 호출해오는 부분입니다. ()안에는 아직 작성하지 않았지만, 뒤에 사용하실 라이브러리 이름을 지어주시면 됩니다.
public native int getSum(int num1, int num2); 이 부분은 C언어로 작성된 함수와 동일한 형태를 가지고 있어야 합니다.
리턴타입이나 매개변수, 함수명 등등. 네이티브 메서드를 정의하고 있다고 보시면 될 것 같습니다. 이 후 사용은 다른 메소드를
호출하는 것과 같이 사용하시면 되겠습니다.
아직 C언어 함수를 생성하지 않았지만 여기서 먼저 빌드를 한번 해주시기 바랍니다. 이후, 헤더파일 생성 시에 빌드 된 정보를 통해
자동으로 헤더파일을 생성해주도록 하는 방법을 사용하기 위해서 필요합니다.
JNI 폴더 생성
프로젝트를 생성하셨으면, 이번엔 jni파일을 작성할 폴더를 만들겠습니다.
/app/src/main/아래에 jni 폴더를 만들어 주세요.
그림처럼 폴더를 생성하시면 되겠습니다.
이제 저 안에 native 언어를 구현하시면 되겠습니다.
소스코드 작성
그럼 먼저 소스를 작성하기 위해 소스파일을 생성하겠습니다.
파일의 생성은 아래와 같이 jni폴더에서 우클릭하여 New > C/C++ Source File을 선택하여 생성하시면 됩니다.
생성하신 파일에 소스를 작성해보도록 하겠습니다.
JNIEXPORT jint JNICALL java_rebuild_com_sample_1jni_MainActivity_getSum
(JNIEnv *env, jobject thiz, jint num1, jint num2) {
return num1 + num2;
}
함수는 간단하게 두 개의 숫자를 건네받아 합산결과를 리턴하는 함수입니다.
위의 소스는 제 프로젝트를 기준으로 작성하였기 때문에 각자 프로젝트에 맞게 다음과 같이 수정하시면 되겠습니다.
JNIEXPORT <리턴타입> JNICALL Java_<패키지명>_<클래스명>_<함수명>
패키지명을 작성하실 때 "." 은 ""로 대체하셔서 작성하시면 되며, 저와 같이 패키지명에 ""가 있다면 "_1"로 대체하셔서 작성하시면 되겠습니다.
헤더파일 생성
소스를 모두 작성하셨다면, 이번엔 헤더 파일을 작성해보겠습니다.
헤더파일의 생성에는 여러가지 방법이 있지만, 여기서는 설정을 통해 자동으로 헤더파일을 생성해주도록 툴을 만들어 사용하는 방법을 사용하겠습니다.
우선 상단 메뉴의 File > Settings 창을 띄워주세요.
그림과 같이 좌측 메뉴에서 Tools > External Tools 를 선택하신 뒤에 상단 메뉴 중에 + 버튼을 누르면 새로운 툴을 작성하실 수 있습니다.
복잡해 보이지만 작성할 내용은 사실 별건 없습니다. 우선 Name에 파일명을 작성해주시고요, 바로 Tools settings를 보겠습니다.
Program 에는 각자의 javah 파일이 있는 경로를 작성하시면 됩니다. * JDK 8 버전부터 Javah가 Javac로 대체되었습니다.
Prameters 에는 클래스 경로와 jni 폴더 경로를 작성하시면 됩니다. 그림과 동일하게 작성하시면 되겠습니다. 물론 다른 옵션들도 있는 것 같지만 이 부분은 저도 자세히 들여다보지 않아 기본적인 내용만 구성하였습니다.
Working directory 에는 jni 폴더 경로를 작성하시면 됩니다.
이렇게만 설정하시면 끝입니다. 간단하죠?
OK 버튼을 눌러 툴 생성을 마친뒤에 이제 진짜로 헤더파일을 추가해보겠습니다.
아래 그림처럼 JDK를 사용하려는 클래스에서 우클릭 한 뒤에 External Tools 메뉴를 펼치면 아까 작성하셨던 툴이 보이실 텐데요 선택해주시면 끝입니다.
만약 선택하신 뒤에 에러가 난다면 아까 설정하셨던 경로들 중에 문제가 있다는 뜻입니다. 오타가 없는지 다시 한번 확인해주세요.
정상적으로 빌드가 되었다면 jni 폴더 안에 헤더파일이 생성되신 것을 볼 수 있으실 겁니다. 파일 이름은 <패키지명>_<클래스명>.h로 생성되셨을 텐데 이름이 길어 보기 좋지 않으시다고 생각하시면 바꾸셔도 상관없습니다. 저는 소스파일과 동일하게 Calculator.h로 수정하였습니다.
헤더파일이 정상적으로 추가 되셨다면, MainActivity에서 작성하였던 getSum(int num1, int num2)메서드와 같은 형태를 정의하고 있는 소스를 확인하실 수 있습니다.
Calculator.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class rebuild_com_sample_jni_MainActivity */
#ifndef _Included_rebuild_com_sample_jni_MainActivity
#define _Included_rebuild_com_sample_jni_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: rebuild_com_sample_jni_MainActivity
* Method: getSum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_rebuild_com_sample_1jni_MainActivity_getSum
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
여기까지 잘 작성되셨다면, 아까 작성하였던 소스파일에 헤더를 include 하도록 소스를 추가하겠습니다.
Calculator.cpp
#include <Calculator.h>
JNIEXPORT jint JNICALL Java_rebuild_com_sample_1jni_MainActivity_getSum
(JNIEnv *env, jobject thiz, jint num1, jint num2) {
return num1 + num2;
}
Android.mk 파일 생성
그럼 다음으로 Android.mk 파일을 작성해보겠습니다.
Android.mk 파일은 소스 및 라이브러리 파일을 설명하는 파일이라고 이해하시면 되겠습니다.
마찬가지로 jni폴더에서 우클릭하여 New > File로 생성하신 뒤에 작성하시면 됩니다.
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jniCalculator
LOCAL_SRC_FILES := Calculator.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
간단히 각 내용을 설명드리자면,
LOCAL_PATH
- 소스 파일의 위치를 설명합니다. 작성된 매크로 함수를 그대로 사용하시면 됩니다.
CLEAR_VARS
- LOCAL_XXX 변수들을 자동으로 지워주는 역활을 합니다. 각각의 모듈을 설명하기 전에 선언해야 한답니다.
LOCAL_MODULE
- 모듈의 이름을 설명합니다. 이전에 MainActivity에서 작성하셨던 이름과 동일하게 맞춰주시면 됩니다.
LOCAL_SRC_FILES
- 모듈에 추가할 소스파일의 이름을 설명합니다. 좀 전에 작성하신 C언어 파일 이름을 넣어주시면 됩니다.
LOCAL_LDLIBS
- 바이너리 빌드에 사용할 빌드 시스템의 외부 라이브러리 목록을 설명합니다. 각 라이브러리 이름 앞에 -l(link-against) 옵션이 선행됩니다. 그 뒤의 log는 로깅 라이브러리를 뜻합니다.
BUILD_SHARED_LIBRARY
- 가장 최근의 include 이후로 정의 된 LOCAL_XXX 변수에 정의된 정보들을 수집하는 스크립트를 설명합니다.
좀 더 상세한 내용는 디벨로퍼 문서를 참고하시면 좋을 것 같습니다.
Android.mk Developer Guide
https://developer.android.com/ndk/guides/android_mk.html?hl=ko
Application.mk 파일 생성
앱에서 요구하는 네이티브 모듈을 설명하는 Application.mk 파일을 작성해보겠습니다.
경우에 따라, 파일 자체가 필요가 없을 수도 있으며, 옵션 또한 변경될 수 있습니다. 이 부분도 Developer Guide 문서를 참고하시면 이해하시는데 도움이 되실 수 있습니다.
Application.mk
APP_ABI := armeabi-v7a
저는 앱이 사용할 CPU 아키텍처에 대해 ABI(Application Binary Interface)를 지정해주었습니다. 위에서도 말씀드렸다시피 이 부분은 각 앱에 따라 옵션이 달라질 수 있으니 무조건 따라하지 마시고 문서를 참고 하시기 바랍니다.
build.gradle (Module:app)에 소스 추가하기
자 거의다 왔습니다.
이번에는 gradle에 소스 추가를 해줘야 빌드 시에 NDK 라이브러리를 인식해 에러가 발생하지 않습니다.
아래 소스에서 굵게 표시된 부분만 추가하시면 됩니다.
build.gradle
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
// Project Structure에서 설정한 NDK 경로를 읽어 들여 Return합니다.
def getNdkBuildPath() {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def command = properties.getProperty('ndk.dir')
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
command += "\\ndk-build.cmd"
} else {
command += "/ndk-build"
}
return command
}
android {
compileSdkVersion 26
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "rebuild.com.sample_jni"
minSdkVersion 18
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets.main {
// Compile된 Native Library가 위치하는 경로를 설정합니다.
jniLibs.srcDir 'src/main/libs'
// 여기에 JNI Source 경로를 설정하면 Android Studio에서 기본적으로 지원하는 Native
// Library Build가 이루어집니다. 이 경우에 Android.mk와 Application.mk를
// 자동으로 생성하기 때문에 편리하지만, 세부 설정이 어렵기 때문에 JNI Source의
// 경로를 지정하지 않습니다.
jni.srcDirs = []
}
ext {
// 아직은 Task 내에서 Build Type을 구분할 방법이 없기 때문에 이 Property를
// 이용해 Native Library를 Debugging 가능하도록 Build할 지 결정합니다.
nativeDebuggable = true
}
// NDK의 ndk-build 명령을 이용하여 Native Library를 Build하기 위한 Task를 정의합니다.
//noinspection GroovyAssignabilityCheck
task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
if (nativeDebuggable) {
commandLine getNdkBuildPath(), 'NDK_DEBUG=1', '-C', file('src/main').absolutePath
} else {
commandLine getNdkBuildPath(), '-C', file('src/main').absolutePath
}
}
// App의 Java Code를 Compile할 때 buildNative Task를 실행하여 Native Library도 같이
// Build되도록 설정합니다.
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn buildNative
}
// NDK로 생성된 Native Library와 Object를 삭제하기 위한 Task를 정의합니다.
//noinspection GroovyAssignabilityCheck
task cleanNative(type: Exec, description: 'Clean native objs and lib') {
commandLine getNdkBuildPath(), '-C', file('src/main').absolutePath, 'clean'
}
// Gradle의 clean Task를 실행할 떄, cleanNative Task를 실행하도록 설정합니다.
clean.dependsOn 'cleanNative'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
testCompile 'junit:junit:4.12'
}
gradle.properties 소스 추가하기
properties 파일도 열어서 다음 한줄을 추가해주시기 바랍니다.
gradle.properties
android.useDeprecatedNdk=true
여기까지 하셨다면 모두 끝났습니다. 이제 실행해보도록 하겠습니다.
실행화면
실행 화면입니다. 정상적으로 Native 함수를 호출하여 결과를 나타내고 있습니다.
글이 굉장히 길었지만 한번 해보시면 생각보다 별거 없다는 걸 알게 되실 겁니다. 그리 깊이 있는 내용까진 못 다뤘지만 그 기초에 도움이 되셨기를 바라겠습니다.
이번 포스팅은 여기서 마치겠습니다.
reference
'언어 > Android Studio' 카테고리의 다른 글
배포용 안드로이드 프로젝트의 필수 파일 구조 (0) | 2019.12.04 |
---|---|
안드로이드 백그라운드 [Service]를 디버깅하는 방법 (0) | 2019.12.04 |
[Android Studio] 기존 프로젝트(eclipse) 가져와서 JNI(NDK) 빌드하기 (0) | 2019.11.29 |
이클립스에 NDK 개발환경 구축하기 (0) | 2019.11.27 |