JNI-Wrapper für C++-Klassen in Android: Erstellung und Callbacks
Die Integration von objektorientiertem C++-Code in eine Android-App über JNI erfordert die Umwandlung in C-Funktionen, während ein Zeiger auf das Objekt gespeichert wird. Die Adresse des C++-Objekts wird als long in einer Java-Klasse gespeichert, was Lebenszyklusverwaltung und mehrere Instanzen ermöglicht.
Betrachten Sie eine einfache C++-Klasse:
class NativeClass {
public:
NativeClass() {}
~NativeClass() {}
void fun() {
__android_log_write(ANDROID_LOG_ERROR, log_tag, "Hello from native");
}
};
JNI-Funktionen für die Arbeit mit der Klasse:
- Initialisierung:
jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz)gibt(jlong) new NativeClass();zurück - Methodenaufruf:
void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass)ruft((NativeClass) cppClass)->fun();auf - Zerstörung:
void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass)führtdelete (NativeClass) cppClass;aus
Java-Wrapper:
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);
}
}
Implementierung von Callbacks aus nativem Code
Für asynchrone Szenarien, wie Netzwerkscans, muss nativer Code Java-Methoden während des Betriebs aufrufen. Eine Listener-Schnittstelle in C++ und ein ThreadPool werden verwendet, um Aufrufe in einem einzelnen Thread zu serialisieren.
Erweiterte C++-Klasse mit Callback-Unterstützung:
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-Schnittstelle und Wrapper:
public interface NativeListener {
void print(String str);
}
public class NativeClass {
long cppClass;
// Konstruktor, finalize, free wie oben
public void scan() { Native.NativeClassScan(cppClass); }
public void stop() { Native.NativeClassStop(cppClass); }
public void setListener(NativeListener listener) {
Native.NativeClassSetListener(cppClass, listener);
}
}
Schlüsselkomponente – Implementierung von Listener in C++ mit JNI:
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);
}
});
}
};
JNI-Funktion zum Setzen des Listeners:
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));
}
Implementierungsempfehlungen
- Single-threaded ThreadPool: Stellt sequenzielle JNI-Aufrufe von einem festen Thread sicher.
- Schwache Referenzen (jweak): Verhindern Speicherlecks; starke lokale Referenzen werden nur für die Dauer des Aufrufs erstellt.
- Lebenszyklusverwaltung: Der Listener-Destruktor bereinigt Ressourcen über den ThreadPool.
Liste wichtiger Praktiken:
- Immer auf NULL bei lokalen Referenzen vor der Verwendung prüfen.
NewWeakGlobalReffür langlebige Referenzen verwenden und zeitgerecht löschen.AttachCurrentThread/DetachCurrentThreadnur innerhalb des ThreadPool aufrufen.- Für synchrone Callbacks Bedingungsvariablen hinzufügen.
- UI-Thread für blockierende Operationen wie
scan()vermeiden.
Wichtige Erkenntnisse
- Das Speichern eines C++-Objektzeigers als long in Java bietet volle Objektsemantik.
- Ein single-threaded ThreadPool ist entscheidend für stabile JNI-Aufrufe von nativen Threads.
- Schwache globale Referenzen (jweak) + lokale Referenzen minimieren Speicherleck-Risiken.
- Java-Wrapper-Finalisierung bereinigt native Ressourcen automatisch.
- Asynchrone Callbacks eignen sich für Echtzeitaufgaben wie Netzwerkscans.
— Editorial Team
Noch keine Kommentare.