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

от автора

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

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

Существует довольно много приложений под Android, которые совмещают C++ и Java код. Java выступает оберткой/прослойкой, реализует бизнес логику, а C++ выполняет всю грязную работу по распознаванию, обработке например аудио, а наверх передаются результаты. Ну и так как RX это уже привычно, то Регулярно возникает надобность в реализации паттерна «Наблюдатель» в проектах. Можно просто подключить ReactiveX или EventBus и не заморачиваться, но все-таки иногда хочется сократить количество зависимостей проекта да и пересылке туда обратно через JNI они не особо помогут. Да и лучший способ научиться чему-нибудь — сделать это своими руками.
Полный полностью рабочий код доступен на GitHub. В статье приводятся только выдержки из него.

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

Недавно был на митапе по RX и был поражен количеству вопросов об: насколько это быстро, и как это вообще работает. Так что немного теории: паттерн «Наблюдатель-Подписчик» — это механизм, который позволяет объекту получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними. Делается для уменьшения связности и зависимостей между программными компонентами, что позволяет эффективнее их использовать и тестировать. Яркий представитель, в котором языковая концепция построена на этом всем – 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);

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

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


ссылка на оригинал статьи https://habr.com/post/420389/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *