×Закрыть

Реализация JNI callbacks в Android NDK

...
— Достало меня это «внутри нет деталей, обслуживаемых пользователем». Хочется посмотреть, что же там есть.
...
— Русская матрешка до самой глубины. Правда, Ороско? Хуан не стал смотреть, что такое русская матрешка.
— Да это же мусор, профессор Гу. Кому оно надо — с таким возиться?

«Конец радуг» Виндж Вернор

Регулярно возникает надобность в реализации паттерна «Наблюдатель» в проектах. Можно просто подключить ReactiveX или EventBus и не заморачиваться, но все-таки иногда хочется сократить количество зависимостей проекта. Да и лучший способ научиться чему-нибудь — сделать это своими руками.

Немного теории и истории

Паттерн «Наблюдатель-Подписчик» — это механизм, который позволяет объекту получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними. Делается для уменьшения связности и зависимостей между программными компонентами, что позволяет эффективнее их использовать и тестировать. Яркий представитель, в котором языковая концепция построена на этом всем — Smalltalk, весь основанный на идее посылки сообщений. Повлиял на Objective-C.

Допустим, мы любим и умеем писать свои велосипеды. И что мы получим в результате:

  • что-нибудь типа обратной пересылки из C++ кода подписавшимся;
  • управление обработкой в нативном коде, то есть мы можем не загоняться по поводу расчетов, когда нет подписчиков и некому их отправлять;
  • может понадобиться и пересылка данных между разными JVM;
  • и чтобы два раза не вставать, заодно и пересылка сообщений внутри потоков проекта.

Реализация

Попробуем в лучших традициях DIY, так сказать, «помигать светодиодом». Если вы используете JNI, в мире Android NDK вы можете запросить метод Java асинхронно, в любом потоке. Это мы и используем для построения своего «Наблюдателя».

Демопроект построен на шаблоне нового проекта из Android Studio.

Это автосгенерированный метод. Он комментирован.

// Used to load the 'native-lib' library on application startup.
static {
    System.loadLibrary("native-lib");
}

А теперь сами. Первый шаг — метод nsubscribeListener.

private native void nsubscribeListener(JNIListener JNIListener);

Он позволяет C++ коду получить ссылку на java-код для включения обратного вызова к обьекту, реализующему интерфейс JNIListener.

public interface JNIListener {
    void onAcceptMessage(String string);

    void onAcceptMessageVal(int messVal);
}

В реализацию его методов и будут передаваться значения.

Для эффективного кэширования ссылок на виртуальную машину сохраняем и ссылку на объект. Получаем для него глобальную ссылку.

Java_ua_zt_mezon_myjnacallbacktest_MainActivity_nsubscribeListener(JNIEnv *env, jobject instance,
                                                                   jobject listener) {

    env->GetJavaVM(&jvm); //store jvm reference for later call

    store_env = env;

    jweak store_Wlistener = env->NewWeakGlobalRef(listener);

Сразу рассчитываем и сохраняем ссылки на методы. Это значит меньше операций потребуется для выполнения обратного вызова.

jclass clazz = env->GetObjectClass(store_Wlistener);
jmethodID store_method = env->GetMethodID(clazz, "onAcceptMessage", "(Ljava/lang/String;)V");
jmethodID store_methodVAL = env->GetMethodID(clazz, "onAcceptMessageVal", "(I)V");

Данные о подписчике хранятся как записи класса ObserverChain.

class ObserverChain {
public:
    ObserverChain(jweak pJobject, jmethodID pID, jmethodID pJmethodID);

    jweak store_Wlistener = NULL;
    jmethodID store_method = NULL;
    jmethodID store_methodVAL = NULL;

};

Сохраняем подписчика в динамический массив store_Wlistener_vector.

ObserverChain *tmpt = new ObserverChain(store_Wlistener, store_method, store_methodVAL);

store_Wlistener_vector.push_back(tmpt);

Теперь о том, как будут передаваться сообщения из Java-кода.

Сообщение, отправленное в метод nonNext, будет разослано всем подписавшимся.

private native void nonNext(String message);

Реализация.

Java_ua_zt_mezon_myjnacallbacktest_MainActivity_nonNext(JNIEnv *env, jobject instance,
                                                                jstring message_) {
    txtCallback(env, message_);
}

Функция txtCallback(env, message_); рассылает сообщения всем подписавшимся.

void txtCallback(JNIEnv *env, const _jstring *message_) {
    if (!store_Wlistener_vector.empty()) {
        for (int i = 0; i < store_Wlistener_vector.size(); i++) {
            env->CallVoidMethod(store_Wlistener_vector[i]->store_Wlistener,
                                store_Wlistener_vector[i]->store_method, message_);
        }
    }
}

Для пересылки сообщений из С++ или С кода используем функцию test_string_callback_fom_c

void test_string_callback_fom_c(char *val)

Она прямо со старта проверяет, есть ли подписчики вообще.

if (store_Wlistener_vector.empty())
    return;

Легко увидеть, что для посылки сообщений используется все та же функция txtCallback.

void test_string_callback_fom_c(char *val) {
    if (store_Wlistener_vector.empty())
        return;
    __android_log_print(ANDROID_LOG_VERBOSE, "GetEnv:", " start Callback  to JNL [%d]  \n", val);
    JNIEnv *g_env;
    if (NULL == jvm) {
        __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", "  No VM  \n");
        return;
    }
    //  double check it's all ok
    JavaVMAttachArgs args;
    args.version = JNI_VERSION_1_6; // set your JNI version
    args.name = NULL; // you might want to give the java thread a name
    args.group = NULL; // you might want to assign the java thread to a ThreadGroup

    int getEnvStat = jvm->GetEnv((void **) &g_env, JNI_VERSION_1_6);

    if (getEnvStat == JNI_EDETACHED) {
        __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", " not attached\n");
        if (jvm->AttachCurrentThread(&g_env, &args) != 0) {
            __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", " Failed to attach\n");
        }
    } else if (getEnvStat == JNI_OK) {
        __android_log_print(ANDROID_LOG_VERBOSE, "GetEnv:", " JNI_OK\n");
    } else if (getEnvStat == JNI_EVERSION) {
        __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", " version not supported\n");
    }

    jstring message = g_env->NewStringUTF(val);//

    txtCallback(g_env, message);

    if (g_env->ExceptionCheck()) {
        g_env->ExceptionDescribe();
    }

    if (getEnvStat == JNI_EDETACHED) {
        jvm->DetachCurrentThread();
    }
}

В MainActivity создаем два TextView и одно EditView.

<TextView
    android:id="@+id/sample_text_from_Presenter"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:padding="25dp"
    android:text="Hello World!from_Presenter"
    app:layout_constraintBottom_toTopOf="parent"
    app:layout_constraintEnd_toStartOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<TextView
    android:id="@+id/sample_text_from_act"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:text="Hello World from_ac!"
    app:layout_constraintBottom_toTopOf="parent"
    app:layout_constraintStart_toStartOf="@+id/sample_text_from_Presenter"
    app:layout_constraintTop_toTopOf="parent" />

<EditText
    android:id="@+id/edit_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"

    android:text="Hello World!"
    app:layout_constraintBottom_toTopOf="parent"
    app:layout_constraintStart_toStartOf="@+id/sample_text_from_act"
    app:layout_constraintTop_toTopOf="parent" />

В OnCreate связываем View с переменными и описываем, что при изменении текста в EditText, будет рассылаться сообщение подписчикам.

mEditText = findViewById(R.id.edit_text);
mEditText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        nonNext(charSequence.toString());
    }

    @Override
    public void afterTextChanged(Editable editable) {

    }
});
tvPresenter = (TextView) findViewById(R.id.sample_text_from_Presenter);

tvAct = (TextView) findViewById(R.id.sample_text_from_act);

Заводим и регистрируем подписчиков.

mPresenter = new MainActivityPresenterImpl(this);
nsubscribeListener((MainActivityPresenterImpl) mPresenter);

nlistener = new JNIListener() {
    @Override
    public void onAcceptMessage(String string) {
        printTextfrActObj(string);
    }

    @Override
    public void onAcceptMessageVal(int messVal) {

    }
};
nsubscribeListener(nlistener);

Выглядит это примерно так:

Код примера лежит здесь: github.com/NickZt/MyJNACallbackTest

В общем пока все, sapienti sat.

LinkedIn

2 комментария

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Спасибо. Ещё изменения в gradle файле можно было упомянуть

там особых изменений, по сравнению с автогенерируемым проектом вроде и нет. Гораздо полезней изменения в cmake.txt

Подписаться на комментарии