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

Похожие статьи:
There is a tussle at the top of the headphone market as Sennheiser and Bose battle to become market leader as we kick off 2019. Sennheiser and Bose offer truly excellent headphone products that offer a revolutionary sound experience that other brands...
Привіт, мене звати Олександр Гончар. Я займаюся машинним навчанням в українському стартапі MAWI solutions та консультую компанії щодо ML....
У проєкті закону йдеться про ліквідацію Комісії з регулювання азартних ігор та лотерей та обмеження реклами грального бізнесу....
В выпуске: учимся репортить баги в Apple, ускоряем сборку проекта и почему каждая строчка кода на самом деле имеет...
Оператор Tele2 в октябре 2015 года запустил в коммерческую эксплуатацию сети скоростного мобильного интернета в...
Яндекс.Метрика