Реализация паттерна “Наблюдатель-Подписчик” используя JNI callbacks в Android (NDK)

  • Tutorial

Реализация в Android (NDK) JNI callbacks, паттерн "Наблюдатель-Подписчик" с NDK и callback, самописный EventBus или Rx


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

Существует довольно много приложений под Android, которые совмещают C++ и Java код. Java реализует бизнес логику, а C++ выполняет всю работу по расчетам, часто это встречается в обработке аудио. Аудио поток обрабатывается где-то внутри, а наверх выведены тормоз с газом и сцеплением, и данные для всяких веселых картинок.
Ну и так как ReactiveX, это уже привычно, то, чтоб так сказать руку не менять, и работать с подземельем JNI знакомыми способами, регулярно возникает надобность в реализации паттерна «Наблюдатель» в проектах с NDK. Ну и заодно понятность кода для археологов тех, кому не повезет "разбираться в чужом коде" возрастает.
Итак, лучший способ научиться чему-нибудь — сделать это своими руками.
Допустим, мы любим и умеем писать свои велосипеды. И что мы получим в результате:


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

Полный рабочий код доступен на GitHub. В статье приводятся только выдержки из него.


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


Недавно был на митапе по RX и был поражен количеству вопросов об: насколько ReactiveX быстр, и как это вообще работает.
За ReactiveX скажу только, что для Java его скорость очень зависит от того насколько разумно его применяют, при правильном применении его скорости вполне достаточно.
Наш велосипедик гораздо более легковесен, но зато если потребуется, например, очередь сообщений (как flowable) то ее надо писать самому. За то знаешь, что все глюки — только твои.
Немного теории: паттерн "Наблюдатель-Подписчик" — это механизм, который позволяет объекту получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними. Делается для уменьшения связности и зависимостей между программными компонентами, что позволяет эффективнее их использовать и тестировать. Яркий представитель, в котором языковая концепция построена на этом всем – Smalltalk, весь основанный на идее посылки сообщений. Повлиял на Objective-C.


Реализация


Попробуем в лучших традициях 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 https://github.com/NickZt/MyJNACallbackTest
Сообщайте в комментах, что описать подробнее.

Поделиться публикацией

Похожие публикации

Комментарии 2
    0
    Ссылка на гитхаб?
      0
      Добавил в статью ссылку на git еще и в конце.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое