홈으로 돌아가기

Android용 JNI C++ 래퍼: 클래스 및 콜백

이 기사는 Android에서 JNI를 통해 C++ 클래스용 Java 래퍼를 생성하는 방법을 설명하며 포인터를 long으로 저장합니다. ThreadPool과 약한 전역 참조를 사용한 네이티브 스레드からの 비동기 콜백을 구현했습니다. 전체 코드 예제와 메모리 관리 권장 사항이 제공됩니다.

Android의 C++ JNI 래퍼: 클래스부터 네이티브 콜백까지
Advertisement 728x90

안드로이드에서 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 함수:

Google AdInline article slot
  • 초기화: 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++ 클래스:

Google AdInline article slot
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 함수:

Google AdInline article slot
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

Advertisement 728x90

다음 읽기