Zpět na domů

JNI obal C++ pro Android: třídy a callbacky

Článek popisuje vytváření Java-obalů pro C++ třídy v Androidu přes JNI s ukládáním ukazatelů jako long. Implementována asynchronní zpětná volání z nativních vláken s použitím ThreadPool a slabých globálních odkazů. Uvedeny kompletní příklady kódu a doporučení pro správu paměti.

C++ JNI obaly v Androidu: od tříd po nativní callbacky
Advertisement 728x90

JNI obálka pro C++ třídy v Androidu: tvorba a zpětná volání

Pro integraci objektově orientovaného C++ kódu do Android aplikace přes JNI je nutné převést na C-funkce s uložením ukazatele na objekt. Adresa C++ objektu se uchovává jako long v Java třídě, což umožňuje správu životního cyklu a více instancí.

Podívejme se na základní C++ třídu:

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

JNI funkce pro práci s třídou:

Google AdInline article slot
  • Inicializace: jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz) vrací (jlong) new NativeClass();
  • Volání metody: void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass) volá ((NativeClass) cppClass)->fun();
  • Zničení: void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass) provede delete (NativeClass) cppClass;

Java obálka:

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

Implementace zpětných volání z nativního kódu

Pro asynchronní scénáře, jako je síťový skener, musí nativní kód volat Java metody během běhu. Používá se rozhraní Listener v C++ a ThreadPool pro serializaci volání v jednom vlákně.

Rozšířená C++ třída s podporou zpětných volání:

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 rozhraní a obálka:

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

public class NativeClass {
    long cppClass;
    // konstruktor, finalize, free jako výše
    public void scan() { Native.NativeClassScan(cppClass); }
    public void stop() { Native.NativeClassStop(cppClass); }
    public void setListener(NativeListener listener) {
        Native.NativeClassSetListener(cppClass, listener);
    }
}

Klíčová komponenta — implementace Listener v C++ s 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 funkce pro nastavení:

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

Doporučení pro implementaci

  • ThreadPool s jedním vláknem: Zajišťuje sekvenční volání JNI z pevného vlákna.
  • Slabé reference (jweak): Zabraňují únikům paměti; lokální silné reference se vytvářejí pouze na dobu volání.
  • Správa životního cyklu: Destruktor Listener uvolňuje prostředky přes ThreadPool.

Seznam klíčových postupů:

  • Vždy kontrolujte NULL pro lokální reference před použitím.
  • Používejte NewWeakGlobalRef pro dlouhodobé reference a včas je odstraňujte.
  • Volání AttachCurrentThread/DetachCurrentThread pouze v ThreadPool.
  • Pro synchronní zpětná volání přidejte condition variables.
  • Vyhněte se UI-vláknu pro blokující operace jako scan().

Co je důležité

  • Uchovávání ukazatele C++ objektu v Java jako long zajišťuje plnou objektovou sémantiku.
  • ThreadPool s jedním vláknem je klíčový pro stabilní JNI volání z nativních vláken.
  • Slabé globální reference (jweak) + lokální ref minimalizují riziko úniků paměti.
  • Finalizace Java obálky automaticky uvolňuje nativní prostředky.
  • Asynchronní zpětná volání jsou vhodná pro úlohy reálného času, jako je síťové skenování.

— Editorial Team

Advertisement 728x90

Číst dál