Реализация 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.