Реализация в 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/
Добавить комментарий