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