우선,

https://codelabs.developers.google.com/codelabs/android-studio-jni

여기에 속아선 안된다!

아직 Android Studio에서는 공식적으로 지원하지 않는 것으로 생각하는 게 정신건강에 좋다. 오로지 android ndk를 위해서만 작성한 코드만 있다면 상관없지만, 3rd party 라이브러리같은 걸 Android Studio에서 소스 컴파일 해서 사용하는 것은 불가능하다.

gradle 파일에 task를 추가하는 식으로 사용하는 것이 가장 효과적이다.

이 포스팅에서는 다음과 같은 방식을 추천한다.

디렉토리 구조

my_project/
    my_module/
        jni/
        libs/
        src/main/java/

Gradle 파일

my_module/build.gradle에 아래 내용을 추가해서 jni 코드가 있음을 Android Studio가 알아채지 못하도록 하자

android {
    sourceSets {
        main {
            jni.srcDirs = []
            jniLibs.srcDirs 'libs'
        }
    }
}

(참고로 기본적으로 Android Studio가 인식하는 경로는 my_project/my_module/src/main/jni 이다)

jniLibs는 .so 파일(소스코드가 아닌)이 있는 위치로 지정한 것이다. 소스코드는 Android Studio가 빌드할 필요가 없지만 .so 파일들은 패키지에 포함되어야하므로 경로를 지정해주어야한다.

이제 my_project/my_module/jni/의 코드들을 my_project/my_module/libs/로 빌드하는 task를 생성해야한다. (출처)

task ndkBuild(type: Exec) {
     workingDir file(./)
     commandLine getNdkBuildCmd()
}

tasks.withType(JavaCompile) {
     compileTask -> compileTask.dependOn ndkBuild
}

task cleanNative(type: Exec) {
     workingDir file(./)
     commandLine getNdkBuildCmd(), clean'
}

clean.dependsOn cleanNative

마지막 줄에 clean task의 dependsOn을 지정하는 부분 유의

ndk-build의 위치를 가져오는 함수를 선언해준다.

def getNdkDir() {
     if (System.env.ANDROID_NDK_ROOT != null)
          return System.env.ANDROID_NDK_ROOT

     Properties properties = new Properties()
     properties.load(project.rootProject.file(local.properties).newDataInputStream())
     def ndkdir = properties.getProperty(ndk.dir, null)
     if (ndkdir == null)
          throw new GradleException(NDK location not found. Define location with ndk.dir int the local.properties file or with an ANDROID_NDK_ROOT environment variable.)
     return ndkdir
}

def getNdkBuildCmd() {
     def ndkbuild = getNdkDir() + /ndk-build"
     if (Os.isFamily(Os.FAMILY_WINDOWS))
          ndkbuild += .cmd"
     return ndkbuild
}

위 내용들을 모두 적용한 모습은 아래와 같다.

import org.apache.tools.ant.taskdefs.condition.Os

android {
     sourceSets {
          main {
               jni.srcDirs = []
               jniLibs.srcDirs libs'
          }
     }

    task ndkBuild(type: Exec) {
        workingDir file(./)
        commandLine getNdkBuildCmd()
    }
    
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependOn ndkBuild
    }
    
    task cleanNative(type: Exec) {
        workingDir file(./)
        commandLine getNdkBuildCmd(), clean'
    }
    
    clean.dependsOn cleanNative
}

def getNdkDir() {
     if (System.env.ANDROID_NDK_ROOT != null)
          return System.env.ANDROID_NDK_ROOT

     Properties properties = new Properties()
     properties.load(project.rootProject.file(local.properties).newDataInputStream())
     def ndkdir = properties.getProperty(ndk.dir, null)
     if (ndkdir == null)
          throw new GradleException(NDK location not found. Define location with ndk.dir int the local.properties file or with an ANDROID_NDK_ROOT environment variable.)
     return ndkdir
}

def getNdkBuildCmd() {
     def ndkbuild = getNdkDir() + /ndk-build"
     if (Os.isFamily(Os.FAMILY_WINDOWS))
          ndkbuild += .cmd"
     return ndkbuild
}

(맨 윗줄 import 부분 유의)

각종 빌드 옵션이나 외부 프로젝트 경로를 gradle.properties 파일에 설정해두고 build.gradle에서 이를 참조하도록 설정하면 빌드를 관리하기가 편하다. 그러나 여러 명이 다양한 환경에서 개발하게될 경우, gradle.properties 파일이 버전관리 시스템에 들어가면 매우 불편한 상황이 생긴다.

2명의 개발자가 어떤 외부 라이브러리 프로젝트를 참조하기 위해 ext_lib_path라는 property를 아래와 같이 설정해두고 작업을 한다고 가정해보자.

  • 개발자1: ext_lib_path=C:\Users\dev1\Libraries\ext_lib
  • 개발자2: ext_lib_path=/home/dev2/libs/ext_lib

라이브러리의 위치는 물론 경로구분자까지 다르기 때문에 단순히 같은 경로에 라이브러리를 두거나 상대경로로 지정하는 것으로는 해결되지 않는다.

이럴 때 gradle.properties를 홈디렉토리에 만들어두면 편리하다.

OS별 홈디렉토리는 다음과 같다. (출처)

  • Mac OS X
    • /Users/<username>/.gradle/
  • Linux
    • /home/<username>/.gradle/
  • Windows Vista & 7+
    • C:\Users\<username>\.gradle\

다만 이런 방식에서 주의할 점은, 홈 디렉토리에 있는 gradle.properties 파일은 프로젝트 디렉토리에 있는 gradle.properties를 오버라이드 하며, 다른 모든 gradle 프로젝트가 참조하게 되므로, property가 중복되지 않도록 하려면 property 이름이 프로젝트 이름을 포함하도록 짓는 것이 좋다.

PROJECT_FOO_EXT_LIB_PATH=/some/where/
PROJECT_BAR_EXT_LIB_PATH=/some/where/else/

그리고 이들 property를 설정해야한다는 정보를 프로젝트 README 파일의 프로젝트 세팅, 빌드 환경 설정 관련 항목에 적어두어야 한다.

만약 프로젝트 이름을 포함하지 않고 그냥 EXT_LIB_PATH=/elsewhere/로 설정한 상태에서 프로젝트 Foo, Bar가 EXT_LIB_PATH을 참조하면 전혀 엉뚱한 경로를 찾게 되므로 문제가 생긴다.

local.properties(기본적으로 버전관리에서 제외되는 파일)에 지정해두는 방법도 있지만, 엄밀히는 local.properties 파일은 gradle에서 지원하는 것이 아니고 안드로이드 플러그인이 처리하는 파일이다. 즉, 커스텀 플러그인을 작성하거나 하는 경우에는 참조하기가 번거로워진다. (local.properties 파일을 읽어들이는 부분을 구현해야한다.)

BroadcastReceiver는 onReceive가 처리되는 동안만 존재한다. 즉,

  • onReceive()가 처리되는 도중에 또다시 같은 인스턴스의 onReceive()가 호출되지는 않는다.
    • 매번 새로운 인스턴스가 생성된다.
  • 비동기처리를 통해 결과를 가져오는 작업을 못함
    • 비동기처리가 진행되는 도중 onReceive가 리턴하고나면 BroadcastReceiver 인스턴스는 이미 invalid 상태
    • API 11부터는 goAsync()를 통해 가능하긴 하다.
    • PendingResult를 가지고 있다가 다 완료되면 PendingResult.finish()를 호출해서 종료
  • 다이얼로그 같은 거 못 띄움
    • NotificationManager API를 사용하라
  • 서비스 바인드 못함
    • context.startService()로 처리하라. 또는 peekService로 참조.
  • 아니면 아예 BroadcastReceiver 말고 IntentService(API 3)를 사용하자.
  • onReceive는 UI thread에서 실행된다.
    • 오래 걸리는 작업 하면 안된다.

출처: https://developer.android.com/reference/android/content/BroadcastReceiver.html

지역변수를 사용하려면 local 키워드로 선언해야한다.

function my_function() {
    local var="Value"
}

sqilte3 데이터베이스에 파일 경로를 삽입하니 아래와 같은 에러가 발생했다.

sqlite3.ProgrammingError: You must not use 8-bit bytestrings unless you use a text_factory that can interpret 8-bit bytestrings (like text_factory = str). It is highly recommended that you instead just switch your application to Unicode strings.

문자열을 아래와 같이 utf-8로 디코딩하면 해결된다.

'path_to_somewhere'.decode('utf-8')