Внимание! Все исходники основаны на версии репозитария на момент 23 июня (последний push на текущий момент сделан 14 июня).
Краткий экскурс: JNI
JNI - это коннектор между нативным кодом и кодом на Java. В настоящее время проект заброшен и не поддерживается, несмотря на большое количество багов в нем. Тем не менее, этот коннектор позволяет без проблем обращаться к ява-машине из нативного кода, пусть и делать это приходится очень аккуратно.
Постановка задачи
Работа с андроидом накладывает несколько ограничений:
- Мы не можем создать еще одну ява-машину, нам надо работать в той же (на процесс разрешена только одна ява-машина)
- ClassLoader для ява-машины в андроиде кастомизирован и не позволяет искать свои классы (системные ищутся и находятся на ура) из любого места отличного от функции OnLoad().
Второе ограничение похуже и по сути не дает нам просто найти интересующие нас классы из нашего Qt-приложения и работать с ними. Опять же, делать поиск в OnLoad() и позже использовать найденные классы не получится, так как OnLoad() находится в lighthouse и кастомизировать его под каждое приложение не очень хорошо.
Следовательно, нам нужно:
- Найти общее решение для вызова своих Java-методов
- Реализовать его
Поиск решения
Первое что приходит на ум, это сделать общее название класса-хелпера и использовать его во всех проектах. Но это опять же не очень хорошо в том плане, что это непереносимое на других пользователей библиотеки исправление.
Вместо этого мы будем передавать объект нужного класса в lighthouse, а оттуда уже будем забирать его из нашего кода на C++/Qt.
Общая структура запуска приложения на Qt под Android'ом
В FAQ'е от автора проекта есть несколько способов запуска Qt-приложений, но мне больше всего нравится способ с подгрузкой библиотеки с Qt-приложением из Java-приложения для Android'а.
Для этого надо создать класс-наследник от com.nokia.qt.QtActivity и в его конструкторе вызвать метод setApplication("appName"), где appName - это название нашего приложения на Qt. Ну и не забыть положить .so-файл в Java-проект. При запуске приложения оно само все подгрузит и выполнит main() из нашего .so.
Теперь рассмотрим что происходит "под капотом".
QtActivity - наследник от обычного Activity и по сути является оберткой для класса QtApplication, который и делает все необходимое. Необходимое заключается, во-первых, в подгрузке Qt-библиотек; во-вторых, в подгрузке библиотеки с нашим приложением и, в-третьих, в запуске нашего приложения. Первое выполняется методом loadLibraries(String[] libraries), а второе и третье методом loadApplication(String lib). Первый этап загрузки нас не интересует, как и второй, а вот третий как раз то что нам надо.
Выполнен запуск приложения также через JNI, посредством вызова нативного метода startQtApp() из Java-кода. Этот нативный метод в свою очередь выполняет некоторые подготовки и запускает отдельный поток с нашим приложением, где уже выполняется наша функция main().
Реализация: изменения в Java-части
Нам нужно вызвать нативный метод запуска с дополнительным параметром. Для этого заведем в com.nokia.qt.QtActivity поле jniProxyObject и сеттер для него, который будем вызывать из конструктора нашего наследника этого класса (там же где выставляется имя подгружаемого нативного приложения). В сигнатуру метода QtApplication.loadApplication() добавим еще один параметр Object jniProxyObject, который собственно и будем передавать в startQtApp(). Также надо поправить сигнатуру объявления нативного метода startQtApp() в этом же классе на
public static native void startQtApp(Object jniProxyObject);
* This source code was highlighted with Source Code Highlighter.
И также поменяем вызов метода loadApplication() в QtActivity.onCreate() на
QtApplication.loadApplication(appName, (jniProxyObject != null) ? jniProxyObject : this);
* This source code was highlighted with Source Code Highlighter.
На этом Java-часть закончена.Реализация: изменения в Android-Lighthouse
Нам нужно сохранить переданный объект и потом его отдавать нашему приложению. Все нужные нам файлы лежат в директории src/plugins/platforms/android. Если конкретнее, то это файлы androidjnimain.cpp и androidjnimain.h. По сути это и есть то, что вызывается из Java-кода. В хедер в неймспейс QtAndroid сразу добавим объявления функций для получения переданного из Java объекта и ява-машины:
JavaVM *getJavaVM();
jobject getJniProxyObject();
* This source code was highlighted with Source Code Highlighter.
В cpp-шнике добавим переменную для нашего объекта и реализации методов, описанных в хедере:
static jobject m_jniProxyObject = NULL;
//...
namespace QtAndroid
{
//...
JavaVM *getJavaVM()
{
return m_javaVM;
}
jobject getJniProxyObject()
{
return m_jniProxyObject;
}
}
* This source code was highlighted with Source Code Highlighter.
Теперь нам нужно поменять метод startQtApp() и его регистрацию в JNI. Второе делается исправлением строковой константы в массиве JNINativeMethod methods[], найдем там элемент {"startQtApp", "()V", (void *)startQtApp} и замением его на {"startQtApp", "(Ljava/lang/Object;)V", (void *)startQtApp}. Метод startQtApp() будет выглядеть следующим образом:
static jboolean startQtApp(JNIEnv* env, jobject /*object*/, jobject jniProxyObject)
{
qDebug()<<"startQtApp";
m_surfaces.clear();
mAndroidGraphicsSystem=0;
m_requestResize=false;
m_pauseApplication=false;
m_applicationControl = new ApplicationControl();
m_jniProxyObject = env->NewGlobalRef(jniProxyObject);
pthread_t appThread;
return pthread_create(&appThread, NULL, startMainMethod, NULL)==0;
}
* This source code was highlighted with Source Code Highlighter.
Осталось только пробросить этот хедер в инклюды, видимые в приложениях. Добавим файл include/QtAndroid/AndroidJniMain, в котором заинклюдим androidjnimain.h.Тестовое приложение
Java-часть
package org.example;
import com.nokia.qt.QtActivity;
public class ExampleMainActivity extends QtActivity
{
public ExampleMainActivity() {
setApplication("jnitest");
setJniProxyObject(this);
}
public static int testMethod(String value) {
return Integer.parseInt(value);
}
public int testMethod2(String value) {
return Integer.parseInt(value)+5;
}
}
* This source code was highlighted with Source Code Highlighter.
C++/Qt-часть
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include < QtAndroid/AndroidJniMain >
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
showMaximized();
JavaVM *jvm = QtAndroid::getJavaVM();
int res;
env = NULL;
if (jvm != NULL)
{
JavaVMAttachArgs args;
args.name = NULL;
args.group = NULL;
args.version = JNI_VERSION_1_4;
res = jvm->AttachCurrentThread(&env, &args);
if (isJavaExceptionOccured())
return;
}
jobject jniProxyObject = QtAndroid::getJniProxyObject();
jclass jniProxyClass = env->GetObjectClass(jniProxyObject);
if (isJavaExceptionOccured())
return;
jmethodID staticMethodId = env->GetStaticMethodID(jniProxyClass,"testMethod","(Ljava/lang/String;)I");
if (isJavaExceptionOccured())
return;
jint res1 = env->CallStaticIntMethod(jniProxyClass, staticMethodId, env->NewStringUTF("42"));
if (isJavaExceptionOccured())
return;
jmethodID methodId = env->GetMethodID(jniProxyClass,"testMethod2","(Ljava/lang/String;)I");
if (isJavaExceptionOccured())
return;
jint res2 = env->CallIntMethod(jniProxyObject, methodId, env->NewStringUTF("56"));
if (isJavaExceptionOccured())
return;
QString output = QString("%1;%2").arg(res1).arg(res2);
ui->plainTextEdit->appendPlainText(output);
}
MainWindow::~MainWindow()
{
delete ui;
}
inline bool MainWindow::isJavaExceptionOccured()
{
if (env->ExceptionOccurred()) {
env->ExceptionDescribe();
env->ExceptionClear();
ui->plainTextEdit->appendPlainText("Exception occured");
return true;
}
return false;
}
* This source code was highlighted with Source Code Highlighter.
То есть, у нас есть два Java-метода (static и обычный), которые возвращают число, переданное в строке (не статичный метод при этом еще прибавляет 5, чтобы отличаться от static-метода). Qt-приложение, просто вызывает эти два метода и выводит результат в текстовое поле на формочке. При запуске, как и ожидалось, выдастся нужный нам результат "42;61".UPD: Сейчас эти изменения влиты в Android-Lighthouse в виде QtAndroidBridge. Эти меры временные, до появления полноценного порта QtMobility под Android.
Ну неплохо как введение в JNI. Конечно для серьёзного применения нужно делать умный указатель для jobject, пробрасывать исключения, транслировать строки, и т.д. Большая работа, на самом деле.
ОтветитьУдалить