В прошлой статье кратко рассмотрел Android NDK, показал как вызывать нативные методы из Java. Разумным продолжением той статьи будет описание того, как вызвать Java методы из C++.
Введение
- Определить у класса метод, который хотим вызвать.
- Получить дескриптор нужного класса.
- Описать сигнатуру метода.
- Получить идентификатор метода (ссылку).
- Вызвать метод у нужного объекта.
Определение методов
Просто определить метод у класса можно, но лучше использовать интерфейсы и реализовать их в нашем классе. Для начала придумаем более-менее практическую задачу, ибо писать примеры на какие-то абстрактные темы не интересно. Предположим, у вас в C++ коде есть таймер, который что-то делает и вызывает через определённые промежутки Java метод. Как пример, рендеринг в игровом движке или опрос сервера и т.д.
Для простоты, скажем, этот таймер будет сообщать о том, сколько времени прошло после запуска. Теперь определим наш интерфейс в Java с этим методом.
public interface NativeCalls {
public void sendTime(int time);
}
Теперь реализуем его в нашем главном Activity.
@Override
public void sendTime(int time){
//просто выводим текст в текст бокс
((TextView)findViewById(R.id.timerText)). setText(Long.toString(time));
}
Данному интерфейсу в нативном коде на C++ будет соответствовать класс NativeCallListener:
class NativeCallListener {
public:
//сколько прошло времени
int timeLeft;
//активен ли таймер
bool timerOn;
NativeCallListener(JNIEnv* pJniEnv, jobject pWrapperInstance);
NativeCallListener() {}
//апуск таймера
void startTimer();
//передать значение в Java метод
void sendTime(int time);
//отключаем таймер
void destroy();
~NativeCallListener(){}
private:
JNIEnv* getJniEnv();
//ссылка на метод
jmethodID sendTimeID;
//ссылка на объект
jobject mObjectRef;
JavaVM* mJVM;
};
Далее необходимо получить дескриптор класса.
Получение дескриптора класса
Для этого есть два способа. Первый использует ссылку на объект:
jclass cl = pJniEnv->GetObjectClass(pWrappedInstance);
Второй позволяет получить дескриптор по имени: jclass cl = pEnv->FindClass(«ru/suvitruf/androidndk/MainActivity»);
Второй метод не одобряю, ибо в случае смены реализации на стороне java, придётся менять и нативный код, но зато не требуется ссылка для получения дескриптора. Первый метод более удобен, как я считаю.
Полностью инициализация (получение дескриптора класса и ссылки на метод) будет такой:
NativeCallListener::NativeCallListener(JNIEnv* pJniEnv, jobject pWrappedInstance) {
pJniEnv->GetJavaVM(&mJVM);
mObjectRef = pJniEnv->NewGlobalRef(pWrappedInstance);
jclass cl = pJniEnv->GetObjectClass(pWrappedInstance);
sendTimeID = pJniEnv->GetMethodID(cl, "sendTime", "(I)V");
}
Cсылки на объекты, переданные в JNI-метод, действительны только в пределах времени выполнения этого метода. При попытки обратится к mObjectRef или pJniEnv после выполнения метода, там будет NULL. Поэтому мы и создаём глобальные ссылки на ни, чтобы потом использовать. Теперь нужен метод для получение JNIEnv:
JNIEnv* NativeCallListener::getJniEnv() {
JavaVMAttachArgs attachArgs;
attachArgs.version = JNI_VERSION_1_6;
attachArgs.name = ">>>TimerThread";
attachArgs.group = NULL;
JNIEnv* env;
if (mJVM->AttachCurrentThread(&env, &attachArgs) != JNI_OK) {
env = NULL;
}
return env;
}
Определение сигнатуры и получение идентификатора метода
Для этого у нас написано это:
sendTimeID = pJniEnv->GetMethodID(cl, "sendTime", "(I)V");
В качестве параметров GetMethodID служат дескриптор класса, имя метода, список параметров и возвращаемого типа «(I)V». В скобках указаны входные параметры, после них — возвращаемый тип. Ниже приведена таблица типов параметров и пару примеров описания методов.
| Java | JNI | JNI array | Код | Код массива |
|---|---|---|---|---|
| boolean | jboolean | jbooleanArray | Z | [Z |
| byte | jbyte | jbyteArray | B | [B |
| char | jchar | jcharArray | C | [C |
| double | jdouble | jdoubleArray | D | [D |
| float | jfloat | jfloatArray | F | [F |
| int | jint | jintArray | I | [I |
| long | jlong | jlongArray | J | [J |
| short | jshort | jshortArray | S | [S |
| Object | jobject | jobjectArray | L | [L |
| Class | jclass | нет | L | [L |
| String | jstring | нет | L | [L |
| void | void | нет | V | нет |
Вызвать метод у нужного объекта
Теперь необходимо написать код, который использует эти дескрипторы:
void NativeCallListener::sendTime(int time) {
JNIEnv* jniEnv = getJniEnv();
jniEnv->CallIntMethod(mObjectRef, sendTimeID, time);
}
Осталось написать только метод по инициализации всего этого дела.
JNIEXPORT void JNICALL Java_ru_suvitruf_androidndk_MainActivity_startTimer(JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener) {
listener = NativeCallListener(pEnv, pNativeCallListener);
LOGI("NativeCall");
listener.startTimer();
}
void NativeCallListener::startTimer(){
timerOn = true;
timeLeft = 0;
LOGI("Start Timer");
while (timerOn) {
sleep(1);
timeLeft+=1;
LOGI("timeLeft %i",timeLeft);
sendTime(timeLeft);
}
}
В Jave определяем метод для инициализации и грузим либу:
native public void startTimer(NativeCalls nativeCallListener);
static {
System.loadLibrary("AndroidNDK");
}
//отдельный поток
class MyRunnable implements Runnable {
MainActivity activity;
public MyRunnable(MainActivity activity){
this.activity = activity;
}
public void run() {
activity.startTimer(activity);
}
}
Теперь вешаем обработчик на кнопку, создаём поток, где и вызываем этот метод с бесконечным циклом. Казалось бы, всё сделали но…будет ошибка:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views
Довольно частое явление при работе с ndk, когда вы вызываете java из C++ и что-то делаете во вьюхе. Вот только проблема в том, что это разные потоки. В том же GLSurfaceView, когда рендерер создаёте, он будет в новом потоке. И из него для манипуляций с вьюхой надо немного по другому всё делать. Создаём свой хэндел, и через него вызываем метод:
protected Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
setTime(msg.what);
}
};
@Override
public void sendTime(int time){
handler.sendEmptyMessage(time);
}
Теперь в текст боксе будет отображаться время прошедшее с запуска таймера.
Можете скачать исходники AndroidNDK2.rar.
В функции sendTime(…) имеется ошибочка, необходимо вызывать CallVoidMethod(…). Иначе приложение падает.
Исправленный вариант:
void NativeCallListener::sendTime(int time) {
JNIEnv* jniEnv = getJniEnv();
jniEnv->CallVoidMethod(mObjectRef, sendTimeID, time);
}