본문 바로가기

Programming/Android

android studio에서 NDK 빌드하기

[출처] : https://www.davidlab.net/ko/tech/using-the-android-ndk-with-android-studio-part1/


javah를 이용해서 c header 파일을 생성할때 다음과 같이 한다.

java source code의 최상위 폴더로 이동. 여기서는 project/app/src/main으로 이동하고 다음과 같이 사용한다.

javah -classpath ./java/ com.erato.example.examplendk.ExampleNDK

위에서 보면 classpath는 소스코드가 존재하는 폴더를 가리키고, 그 폴더 기준으로 클래스를 입력해준다.

따라서 폴더를 기준으로 com/erato/example/examplendk/ExampleNDK.java가 존재하므로, 클래스명은 

com.erato.example.examplendk.ExampleNDK에 적어주면 된다.


========================================================================


아래 출처에서 제일 중요한 부분만 별도로 언급해보자.

1. gradle.properties

android.useDeprecatedNdk=true

위와 같이 꼭 추가해줘야 ndk-build가 적용된다.


2. app/build.gradle

아래 defaultConfig에 ndk 항목에 ldLibs는 추가해줘야 하는 라이브러를 직접 명시해줘야 한다.

이를테면 jni/Android.mk에서 android_log를 사용하는데, 이곳에 명시를 안하면 'undefined reference' 에러가 발생한다.

명심하자!

defaultConfig {
applicationId "com.erato.example.examplendk"
minSdkVersion 23
targetSdkVersion 24
versionCode 1
versionName "1.0"

ndk {
moduleName "libexamplendk"
ldLibs "log"
}
}

또한 아래 jniLibs의 경로는 지정하지 않으면, app에서 System.loadLibray("examplendk") 시에 라이브러리 못찾는다고 죽는다.

sourceSets.main {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] //disable automatic ndk-build call
}

==============================================================================


NDK는 C/C++과 같은 Native Code를 위한 언어를 이용하여 Android App의 일부분을 구현할 수 있도록 도와주는 Toolset입니다.

이것을 이용하면 개발자는 기존에 Embedded Linux에서 개발하던 방식과 유사하게 C/C++로 구현하여 Toolset으로 Build한 Native Library를 Java의 JNI를 이용하여 Java Code내에서 사용할 수 있게 됩니다.

기존의 Eclipse 기반의 ADT를 이용하는 경우에는 Eclipse의 CDT Plugin을 이용하여 IDE Level의 NDK 지원이 가능했지만, Android Studio의 경우에는 C/C++ Compile 및 Debugging을 제대로 지원하지 않기 때문에 NDK를 적용하는데 어려움이 있습니다.

이번 Post에서는 Android Studio에서 생성한 Project에 NDK로 Build한 Library를 사용하는 방법과 Build System에 통합하여 Build하는 방법 및 Debugging하는 방법에 대해서 알아보도록 하겠습니다.

1. NDK 설치

먼저, Android Studio와 Android SDK가 System에 설치되어야 있어야 합니다. Ubuntu의 경우에는 이 Post를 참고하여 설치하면 됩니다.

설치가 완료되었다면, System에 Android NDK를 설치하기 위해서 Android Developers에서 NDK를 Download합니다.

그리고 다음과 같이 입력하여 NDK를 설치합니다.

  • Ubuntu의 경우:
    $ mkdir $HOME/Android
    $ chmod 755 android-ndk*.bin
    $ ./android-ndk*.bin -o$HOME/Android
    $ mv $HOME/Android/android-ndk* $HOME/Android/Ndk
    $ vi $HOME/.bashrc
    view raw01-install-ndk.sh hosted with ❤ by GitHub
    export ANDROID_NDK_HOME=$HOME/Android/Ndk
    export PATH=$PATH:$ANDROID_NDK_HOME
    view raw02-bashrc.sh hosted with ❤ by GitHub
    $ source $HOME/.bashrc
    view raw03-apply.sh hosted with ❤ by GitHub
  • Mac OS X의 경우:
    $ mkdir -p $HOME/Library/Android
    $ chmod 755 android-ndk*.bin
    $ ./android-ndk*.bin -o$HOME/Library/Android
    $ mv $HOME/Library/Android/android-ndk* $HOME/Library/Android/ndk
    $ vi $HOME/.bash_profile
    view raw01-install-ndk.sh hosted with ❤ by GitHub
    export ANDROID_NDK_HOME=$HOME/Library/Android/ndk
    export PATH=$PATH:$ANDROID_NDK_HOME
    $ source $HOME/.bash_profile
    view raw03-apply.sh hosted with ❤ by GitHub

2. Project에 NDK 적용

이번 항목에서는 설치된 NDK를 Android Studio Project에서 사용하도록 설정하는 방법에 대해서 예제와 함께 설명합니다.

이번 항목은 이해를 위하여 Step-by-Step으로 따라할 수 있도록 작성되었습니다.
Android Studio 1.3 이상에서 새로운 NDK 지원을 사용할 수 있습니다. 하지만 아직 실험적인 상태이기 때문에 실제 개발에 사용하기에는 무리가 있습니다. (자세한 내용은Experimental Plugin User Guide 참조)

떄문에 이 Post에서는 지금까지 Android Studio에서 NDK를 사용하던 방식 그대로를 사용합니다. 추후 Release를 통해 정식으로 지원된다면 이 내용은 Update될 것입니다.

2.1 Project 생성

Create New Project

Create New Project

먼저, 새로운 Project를 생성합니다. Create New Project Dialog에서 위와 같이 App Name에 NDKTest라고 입력하고 Domain은 상황에 맞게 입력한 후 Next를 누릅니다.

Target Android Devices

Target Android Devices

Target Devices는 Default로 두고 Next를 누릅니다.

Add an activity to Mobile

Add an activity to Mobile

Activity 선택에서는 Blank Activity를 선택하고 Next를 누릅니다.

Customize the Activity

Customize the Activity

Activity 설정에서는 Default로 두고 Finish를 눌러 Project 생성을 완료합니다.

2.2 Source Code 수정

이제 생성된 Project의 Source를 수정해 봅시다. app/src/main/res/layout/content_main.xml을 열어서 기본적으로 추가되어 있는 TextView를 다음과 같이 수정합니다.

<TextView
android:id="@+id/textView"
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
view rawcontent_main.xml hosted with ❤ by GitHub

그 다음, MainActivity.java를 열어서 다음과 같은 내용을 추가합니다.

public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("ndktest");
}
public native String getStringFromNative();
@Override
protected void onCreate(Bundle savedInstanceState) {
// TextView에 Native Method를 호출한 결과를 표시한다.
TextView view = (TextView) findViewById(R.id.textView);
view.setText(getStringFromNative());
}
}
view rawMainActivity.java hosted with ❤ by GitHub

위의 Code를 간단히 설명하면 다음과 같습니다.

  • Line 2-4: NDK로 Build한 Native Library를 Load합니다. Argument로는 Library File Name에서 Prefix인 lib와 확장자를 뺀 것을 입력합니다. (libndktest.so  ndktest)
  • Line 6: Native Library에서 구현한 Method의 Interface를 native keyword로 선언합니다. 이렇게 선언된 Method는 실제로 구현되어 있지 않아도 Build가 가능합니다.
  • Line 11-12: Activity 생성 시에 Layout에 정의된 TextView에 Native Method를 호출한 결과를 출력하는 Code입니다.

Source를 모두 수정했다면, Build > Make Project를 눌러 Source를 Build합니다.

2.3 JNI Code 생성

NDK는 Native Code 구현을 위해 Java의 JNI(Java Native Interface)를 사용합니다.

간단하게 말하면, NDK는 Java에서 지원하는 Native Code 호출 규약인 JNI를 이용해서 Native Code를 App에서 사용할 수 있도록 도와줍니다.

떄문에 Java Source에서 선언한 Native Method를 구현하기 위해서는 JNI를 위한 C/C++ Header를 생성하고, 생성된 Header에 선언된 Native Method에 해당하는 Interface를 C/C++ Code로 구현하면 됩니다.

먼저 Android Studio의 Terminal(Alt + F12)을 열고, Build된 Java Class를 이용하여 Header를 생성하기 위해서 다음과 같이 입력합니다.

$ cd app/src/main
$ mkdir jni
$ javah -d jni -classpath $ANDROID_HOME/platforms/android-23/android.jar:$ANDROID_HOME/extras/android/support/v7/appcompat/libs/android-support-v7-appcompat.jar:$ANDROID_HOME/extras/android/support/v4/android-support-v4.jar:../../build/intermediates/classes/debug net.davidlab.ndktest.MainActivity

javah 명령에서 유의할 것은 app/build.gradle에 정의되어 있는 targetSdkVersion에 맞는 경로를 -classpath에 추가해 주어야 합니다. 위의 경우는 API 23(Android 6.0)을 사용할 경우의 예입니다.

그리고 javah의 마지막 인자에 Package Name을 포함한 MainActivity class의 Full Name을 적어야 합니다.

Android Studio - Project Tool Window

Android Studio – Project Tool Window

생성이 제대로 되었다면 위의 그림과 같이 PROJECT_HOME/app/src/main/jni/fully_qualified_class_name.h가 생성됩니다.

이제 생성된 Header를 이용하여 Native Code를 구현해야 합니다. app/src/main/jni/main.cpp를 생성하고 다음과 같이 입력하고 저장합니다.

#include "net_davidlab_ndktest_MainActivity.h"
JNIEXPORT jstring JNICALL Java_net_davidlab_ndktest_MainActivity_getStringFromNative(JNIEnv *env, jobject obj) {
return env->NewStringUTF("Hello from JNI!");
}
view rawmain.cpp hosted with ❤ by GitHub

위의 구현은 다음과 같은 구조로 되어 있습니다.

  • Line 1: javah로 생성된 Header를 Include합니다. Header 이름은 상황에 맞게 변경해야 합니다.
  • Line 3: 생성된 Header에서 MainActivity.java에서 선언한 getStringFromNative()에 해당하는 Interface의 Prototype을 그대로 복사하여 입력하고 인자에 변수명을 추가합니다.
  • Line 4: 실제 구현내용입니다. String을 생성하여 Return합니다.
JNI 구현에 대한 자세한 내용은 이 Post에서 다루지 않습니다. 좀 더 자세한 내용은 Oracle의 JNI Specification에서 확인할 수 있습니다.

2.4 Android.mk 생성

Native Code까지 구현이 완료되었다면, Library로 Build하기 위해 Android.mk를 생성해야 합니다.

NDK에서는 Native Code의 Build를 위해서 GNU Make를 사용합니다. Android.mk는 Make를 위한 Makefile 형식으로 된 Build 설정 File로 Native Library를 Build할 때 사용됩니다.

app/src/main/jni/Android.mk를 생성하고 다음과 같이 입력합니다.

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := ndktest
LOCAL_SRC_FILES := main.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
view rawAndroid.mk hosted with ❤ by GitHub

위의 내용을 간단히 설명하면 다음과 같습니다.

  • Line 5: LOCAL_MODULE에는 생성될 Library의 이름을 입력합니다. 위에서는 MainActivity.java에서 loadLibrary()의 인자로 입력하였던 Library 이름인 ndktest가 사용되었습니다.
  • Line 6: LOCAL_SRC_FILES에는 Build할 Source File을 입력합니다. 여러 개일 경우 Space로 구분하여 입력하면 됩니다. 위의 내용에서는 이전에 생성한 main.cpp를 Source로 지정하였습니다.
  • Line 7: LOCAL_LDLIBS Library Link 시에 사용하는 ld 명령을 위한 Option을 지정합니다. 위의 경우는 Android의 Logcat을 사용하기 위한 Option이 추가되어 있습니다.
자세한 Android.mk의 사용법은 ANDROID_NDK_HOME/docs에 위치한 Documentation에서 확인할 수 있습니다.

2.5 Application.mk 생성

Application.mk도 Android.mk와 마찬가지로 Native Library Build 시에 필요한 File로, Build 시에 Android App과 관련된 설정을 지정할 수 있습니다.

app/src/main/jni/Application.mk를 생성하고 다음과 같이 입력합니다.

APP_ABI=all
view rawApplication.mk hosted with ❤ by GitHub

위의 내용에서는 APP_ABI 변수에 Native Library Build를 위해 Target ABI(Application Binary Interface)를 설정합니다.

ABI는 Binary Level의 호환 Interface를 의미합니다. 이것은 CPU Type에 의존하며, NDK는 armeabi, x86, mips 등의 ABI를 지원합니다.

위와 같이 all로 입력할 경우 Android가 지원하는 모든 ABI를 위한 Native Library를 Build하게 됩니다.

자세한 Application.mk의 사용법은 ANDROID_NDK_HOME/docs에 위치한 Documentation에서 확인할 수 있습니다.

2.6 Gradle 설정

NDK를 위한 Code를 모두 작성했으니 이제 Gradle Build System을 수정해 봅시다.

Project Structure

Project Structure

먼저 그에 앞서, Gradle에서 NDK에 포함된 명령들을 사용하기 위해서 System에 설치된 NDK의 경로를 입력해야 합니다.

File > Project Structure > SDK Location에서 Android NDK location에 System에 설치된 NDK의 경로를 입력합니다.

NDK의 경로는 PROJECT_HOME/local.properties에 저장됩니다.

Android Studio 1.3 이상의 경우, Android Gradle Plugin에서 새로 지원하는 실험적인 NDK 지원 대신에 이전 Version에서 사용하던 방식을 사용하도록 설정해야 합니다.

설정을 위해서 gradle.properties에 다음과 같은 내용을 추가합니다.

android.useDeprecatedNdk=true
view rawgradle.properties hosted with ❤ by GitHub

그 다음, App을 Build할 때 NDK를 이용하여 Native Library도 같이 Build할 수 있도록 build.gradle을 수정해야 합니다.1 2

app/build.gradle에 다음과 같은 내용을 추가합니다.

import org.apache.tools.ant.taskdefs.condition.Os
// 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 {
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 = false
}
// 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 {
debug {
// Debug Build시에 Native Library Debugging이 가능한 APK를 생성하도록 설정합니다.
jniDebuggable true
}
}
}
view rawbuild.gradle hosted with ❤ by GitHub

build.gradle을 수정했다면 Sync Now를 눌러 동기화합니다.

2.7 Project Build 및 Test

이제 모든 작업이 끝났습니다. Android Studio에서 Run > Run을 실행하여 Project를 Build하고 Emulator 또는 Device에서 실행하여 Test합니다.

Android Studio - Gradle Console

Android Studio – Gradle Console

Native Library가 Build되었는지 확인하려면 위의 그림과 같이 Gradle Console Tool Window에 buildNative Task가 실행되었는 지 확인하면 됩니다.

NDKTest

NDKTest

App을 실행하면 위와 같이 TextView에 JNI Code에서 Return한 Text가 표시되는 것을 확인할 수 있습니다.

2.8 Source

지금까지 구현한 NDK Project의 Source를 얻으려면 다음과 같이 Terminal에서 입력합니다.

$ git clone https://github.com/davidhyk/android-examples.git
view rawgit-clone.sh hosted with ❤ by GitHub

Clone 후에 Android Studio를 실행한 다음, Open an existing Android Studio project(또는 File > Open)을 눌러 android-examples/NDKTest를 Import하면 됩니다.

3. 마치면서…

이상으로 System에 NDK를 설치하고 Android Studio에서 NDK를 이용하여 Native Library를 Build하고 Java Source에서 사용하는 방법을 알아보았습니다.

이어지는 Part 2에서는 Android Studio에서 Native Library와 Java Code를 같이 Debugging하는 방법과 몇 가지 Tip을 공유하도록 하겠습니다.