안드로이드에서 C++ 클래스용 JNI 래퍼: 생성 및 콜백 구현
객체 지향 C++ 코드를 JNI를 통해 안드로이드 앱에 통합하려면 C 함수로 변환하면서 객체에 대한 포인터를 저장해야 합니다. C++ 객체의 주소는 Java 클래스에서 long으로 저장되어 라이프사이클 관리와 다중 인스턴스화를 가능하게 합니다.
기본 C++ 클래스를 살펴보세요:
class NativeClass {
public:
NativeClass() {}
~NativeClass() {}
void fun() {
__android_log_write(ANDROID_LOG_ERROR, log_tag, "Hello from native");
}
};
클래스를 다루기 위한 JNI 함수:
- 초기화:
jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz)는(jlong) new NativeClass();를 반환합니다. - 메서드 호출:
void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass)는((NativeClass) cppClass)->fun();을 호출합니다. - 소멸:
void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass)는delete (NativeClass) cppClass;를 실행합니다.
Java 래퍼:
public class Native {
static { System.loadLibrary("Native"); }
public static native long initNativeClass();
public static native void destroyNativeClass(long cppClass);
public static native void NativeClassFun(long cppClass);
}
public class NativeClass {
long cppClass;
NativeClass() {
this.cppClass = Native.initNativeClass();
}
@Override protected void finalize() {
free();
}
private void free() {
Native.destroyNativeClass(cppClass);
}
public void fun() {
Native.NativeClassFun(cppClass);
}
}
네이티브 코드에서 콜백 구현하기
네트워크 스캔과 같은 비동기 시나리오에서는 네이티브 코드가 동작 중에 Java 메서드를 호출해야 합니다. C++의 Listener 인터페이스와 ThreadPool을 사용하여 단일 스레드에서 호출을 직렬화합니다.
콜백 지원이 추가된 확장 C++ 클래스:
class NativeClass {
public:
class Listener {
public:
virtual void print(const char* str) = 0;
virtual ~Listener() {}
};
private:
bool started = false;
Listener* listener = nullptr;
static void worker(NativeClass* nc) {
while(nc->started) {
if(nc->listener != nullptr) {
nc->listener->print("From native");
}
sleep(1);
}
}
public:
NativeClass() {}
~NativeClass() {
if(listener != nullptr) delete listener;
}
void setListener(Listener* listener) {
this->listener = listener;
}
void scan() {
started = true;
std::thread thread = std::thread(worker, this);
thread.join();
started = false;
}
void stop() {
started = false;
}
};
Java 인터페이스 및 래퍼:
public interface NativeListener {
void print(String str);
}
public class NativeClass {
long cppClass;
// 생성자, finalize, free는 위와 동일
public void scan() { Native.NativeClassScan(cppClass); }
public void stop() { Native.NativeClassStop(cppClass); }
public void setListener(NativeListener listener) {
Native.NativeClassSetListener(cppClass, listener);
}
}
핵심 구성 요소 — JNI를 사용하여 C++에서 Listener 구현:
class Listener : public NativeClass::Listener {
private:
JavaVM* jvm;
JNIEnv* env;
jweak listenerRef;
jmethodID printMethod;
public:
Listener(JavaVM* jvm, JNIEnv* env, jobject listener) {
this->jvm = jvm;
listenerRef = env->NewWeakGlobalRef(listener);
ThreadPool::execute({
jvm->AttachCurrentThread(&env, NULL);
if(env != NULL) {
jobject localRef = env->NewLocalRef(listenerRef);
if(localRef != NULL) {
jclass clazz = env->GetObjectClass(localRef);
printMethod = env->GetMethodID(clazz, "print", "(Ljava/lang/String;)V");
env->DeleteLocalRef(localRef);
}
}
});
}
~Listener() {
ThreadPool::execute({
env->DeleteWeakGlobalRef(listenerRef);
jvm->DetachCurrentThread();
});
}
void print(const char* msg) {
ThreadPool::execute({
jobject localRef = env->NewLocalRef(listenerRef);
if(localRef != NULL) {
jstring jMsg = env->NewStringUTF(msg);
env->CallVoidMethod(localRef, printMethod, jMsg);
env->DeleteLocalRef(jMsg);
env->DeleteLocalRef(localRef);
}
});
}
};
Listener 설정을 위한 JNI 함수:
JNIEXPORT void JNICALL Java_com_myprog_example_Native_NativeClassSetListener(
JNIEnv* env, jobject thiz, jlong cppClass, jobject listener) {
((NativeClass*) cppClass)->setListener(new Listener(jvm, env, listener));
}
구현 권장사항
- 단일 스레드 ThreadPool: 고정 스레드에서 순차적인 JNI 호출을 보장합니다.
- 약한 참조 (jweak): 메모리 누수를 방지합니다; 강한 로컬 참조는 호출 기간 동안만 생성됩니다.
- 라이프사이클 관리: Listener 소멸자는 ThreadPool을 통해 리소스를 정리합니다.
핵심 실천 사항 목록:
- 사용 전에 항상 로컬 참조의 NULL을 확인하세요.
- 장기 참조에는
NewWeakGlobalRef를 사용하고 적시에 삭제하세요. AttachCurrentThread/DetachCurrentThread는 ThreadPool 내에서만 호출하세요.- 동기 콜백의 경우 조건 변수를 추가하세요.
scan()과 같은 블로킹 작업에는 UI 스레드를 피하세요.
핵심 요약
- C++ 객체 포인터를 Java에서 long으로 저장하면 완전한 객체 의미론을 제공합니다.
- 단일 스레드 ThreadPool은 네이티브 스레드에서 안정적인 JNI 호출에 중요합니다.
- 약한 전역 참조 (jweak) + 로컬 참조는 메모리 누수 위험을 최소화합니다.
- Java 래퍼 finalization은 네이티브 리소스를 자동으로 정리합니다.
- 비동기 콜백은 네트워크 스캔과 같은 실시간 작업에 적합합니다.
— Editorial Team
아직 댓글이 없습니다.