Powrót do strony głównej

JNI wrapper C++ dla Androida: klasy i callbacki

Artykuł opisuje tworzenie Java-wrapperów dla klas C++ w Androidzie przez JNI z przechowywaniem wskaźników jako long. Zaimplementowano asynchroniczne wywołania zwrotne z natywnych wątków z użyciem ThreadPool i słabych globalnych referencji. Podano pełne przykłady kodu i zalecenia dotyczące zarządzania pamięcią.

C++ JNI wrappery w Androidzie: od klas do natywnych callbacków
Advertisement 728x90

JNI-otoka dla klas C++ w Androidzie: tworzenie i wywołania zwrotne

Aby zintegrować obiektowy kod C++ z aplikacją Androidową przez JNI, konieczne jest przekształcenie go w funkcje C z przechowywaniem wskaźnika do obiektu. Adres obiektu C++ jest zapisywany jako long w klasie Java, co umożliwia zarządzanie cyklem życia i wieloma instancjami.

Rozważmy podstawową klasę C++:

class NativeClass {
public:
    NativeClass() {}
    ~NativeClass() {}
    void fun() {
        __android_log_write(ANDROID_LOG_ERROR, log_tag, "Hello from native");
    }
};

Funkcje JNI do pracy z klasą:

Google AdInline article slot
  • Inicjalizacja: jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz) zwraca (jlong) new NativeClass();
  • Wywołanie metody: void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass) wywołuje ((NativeClass) cppClass)->fun();
  • Zniszczenie: void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass) wykonuje delete (NativeClass) cppClass;

Otoka 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);
    }
}

Implementacja wywołań zwrotnych z kodu natywnego

Dla scenariuszy asynchronicznych, takich jak skaner sieciowy, kod natywny musi wywoływać metody Java podczas pracy. Używany jest interfejs Listener w C++ i ThreadPool do szeregowania wywołań w jednym wątku.

Rozszerzona klasa C++ z obsługą callbacków:

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;
    }
};

Interfejs Java i otoka:

public interface NativeListener {
    void print(String str);
}

public class NativeClass {
    long cppClass;
    // konstruktor, finalize, free jak powyżej
    public void scan() { Native.NativeClassScan(cppClass); }
    public void stop() { Native.NativeClassStop(cppClass); }
    public void setListener(NativeListener listener) {
        Native.NativeClassSetListener(cppClass, listener);
    }
}

Kluczowy komponent — implementacja Listener w C++ z 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);
            }
        });
    }
};

Funkcja JNI do ustawiania:

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));
}

Zalecenia implementacyjne

  • ThreadPool z jednym wątkiem: Zapewnia sekwencyjne wywołania JNI z ustalonego wątku.
  • Słabe referencje (jweak): Zapobiegają wyciekom pamięci; lokalne silne referencje tworzone są tylko na czas wywołania.
  • Zarządzanie cyklem życia: Destruktor Listener czyści zasoby przez ThreadPool.

Lista kluczowych praktyk:

  • Zawsze sprawdzaj NULL dla lokalnych referencji przed użyciem.
  • Używaj NewWeakGlobalRef dla długożyjących referencji i usuwaj je w porę.
  • Wywołania AttachCurrentThread/DetachCurrentThread tylko w ThreadPool.
  • Dla synchronicznych callbacków dodaj condition variables.
  • Unikaj UI-thread dla blokujących operacji typu scan().

Co jest ważne

  • Przechowywanie wskaźnika obiektu C++ w Java jako long zapewnia pełną semantykę obiektową.
  • ThreadPool z pojedynczym wątkiem jest kluczowy dla stabilnych wywołań JNI z wątków natywnych.
  • Słabe globalne referencje (jweak) + lokalne ref minimalizują ryzyko wycieków pamięci.
  • Finalizacja otoki Java automatycznie czyści zasoby natywne.
  • Asynchroniczne callbacki nadają się do zadań w czasie rzeczywistym, takich jak skanowanie sieci.

— Editorial Team

Advertisement 728x90

Czytaj dalej