Неактуально, кроме постановки задачи. Правильное решение в следующей статье.
Добрый день, уважаемые хабровчане! В этой статье я хочу рассказать о своём опыте использования qt и android studio. А именно о том, как мне надо было в qt нарисовать текст и передать в андроид студию. Несмотря на простоту задачи, решение её заняло у меня довольно много времени и может быть кому-нибудь когда-нибудь где-нибудь сэкономит массу времени. Статья в каком-то смысле претендует на изобретение велосипеда, но в интернете я не нашёл решения. Кому интересно — добро пожаловать под кат!
Недавно встала передо мною задача портировать приложение с ios на андроид. Основной болью при портировании была работа с SDK приложения. Оно было написано на Qt и исопльзовалось для рисования текста/стрелочек/областей и всего прочего. То есть, приложение было написано на objective c, и использовало qt библиотеку, а не было qt проектом. Поэтому, первым делом встал вопрос среды разработки. Поскольку, я совсем в этом деле новичок, мой выбор пал на андроид студию. Всё-таки весь графический интерфейс, как мне показалось, лучше делать в андроид студии, а вычислительные задачи пущай делает наше qtшное SDK. В интернете не так уж много пишут об использовании qt под андроид, а здесь была задача подружить qt и андроид студию. Для работы с плюсами используется Android NDK и реализуется всё через использование JNI. Работа с JNI — вещь сама по себе довольно интересная. В нете можно найти массу статей на эту тему (например этот замечательный цикл). Однако меня интересует JNI в разрезе использования его с Qt. Опять же, в чём проблема, спросите вы? Берём сишные сорсы, делаем шаред либу, подключаем к проекту в андроид студии и получаем profit! Вот, как например тут. А вот здесь и начинается самое интересное…
Как вы помните, я указал выше, что
Чтобы нарисовать графический примитив в QT, нам не требуется создавать экземпляр QApplication или QGuiApplication. Даже QCoreApplication — и тот не нужен! А вот для рисования текста без QApplication или QGuiApplication уже никак нельзя обойтись. Так в чём проблема, спросите вы? Проблема наступает как раз на момент вызова конструктора:
Если вы создадите билиблиотеку, в ней какую-либо функцию, вызывающую конструктор QApplication, а затем вызовите её через JNI из приложения андроид студии, то сразу же словите:
Первое, что я решил сделать — нагуглить решение проблемы в интернете. Точного совпадения я не нашёл, но в довольно большом количестве постов люди жаловались на похожие проблемы для плагина под винду. Вот и я перепробовал всё, что было указано здесь, но, увы, решения (работающего для меня! ) не было найдено.
В поисках ответа на свои вопросы, я наткнулся на такой довольно любопытный блог, как я понял автора qt под андроид. Блог весьма интересный, но в нём автор делает акцент (опять же, моё имхо) на разработку со стороны с++ и запуска всего добра из qt creator. Меня такой подход, если честно, не очень устраивал по одной причине: отладка Java части из Qt практически невозможна (можно только компилировать код, потом ждать приаттачивания из андроид студии и уже оттуда наблюдать происходящее), а также у меня довольно большое количество различных layoutov, кастомных вьюх, асинхронных задач, а как это добро засунуть в qt проект и нормально отлаживать? Честно говоря, я не знаю.
Я попробовал создать также Qt приложение и запустить его на андроиде. Запускал я его через qt-creator и как ни странно оно благополучно запустилось. Я стал смотреть более подробно как устроен манифест, граддл, код приложения. Я обнаружил такую интересную вещь в манифесте:
Вкратце смысл его понятен. Когда я собирал apk приложения, я указал, что qt библиотеки должны находиться внутри apk и именно оттуда надо их грузить своему приложению. Подключение соответстсвующих jar-ов в проект на андроиде, прописывание в андроидовском манифесте того, что было в qt, размещение qtшных .so плагинов в папке jniLibs не дало никакого эффекта.
Я попробовал уже наконец грузить самостоятельно со стороны java этот несчастный плагин libqtforandroid.so (до создания QApplication) путём
По крайней мере есть у нас зацепка, где можно смотреть. Оперативно по qt start находим интересующий нас метод:
Судя по логу, он упал где-то в каком-то из registerNatives.Так и было (я прописал логи в каждом из registerNatives). Он падал в
А именно:
Падение произошло на последней строчке. classLoaderObject оказался равен null. А это произошло, что activityObject тоже равен null. Окей. Перед тем, как грузить этот злосчастный плагин попробуем создать активити для JNI. Для этого пропишем в Java коде следующие строчки:
Небольшое отступление. Класс QtNative лежит в jar файлах, которые мы подключаем к проекту. Более того, это весьма любопытный класс. В нём есть методы:
которые и должны подгружать требуемые плагины. Пока запомним это, и вернёмся к подключению нашего плагина вручную. Вызов методов QtNative setActivity и setClassLoader помог проскочить:
но засада была уже в QtAndroidInput::registerNatives(env). Не совпадали сигнатуры функций для события keyDown. В принципе, мне ничего не нужно кроме шрифтов и я закомментировал следующий участок кода:
и вроде как-бы благополучно загрузил этот плагин. Запускаем приложение, грузим плагин, вызываем QApplication и… ловим нашеостознакомое исключение:
Более того, вызов
тоже не решил проблемы. Хорошо. Ладно. Полезем в сорсы создания конструктора. По исключению быстро находим метод:
Хорошо. Ищем, откуда вызываем сей метод:
То бишь, нам можно через argc и argv передать аргументы, где надо искать этот плагин. Сразу оговорюсь, я пробовал в дебаггере qt запускать приложение под андроид, и там argc и argv соответственно равны: 1 и имя_нашей_библиотеки_которую_собирает_qt, но никак не плагин. Попробуем присвоить argc и argv соответствующие значения:
Неа, не сработало.
Честно говоря, сроки поджимают, а дальше заниматься изучением что и где не сработало — у меня нетусилвремени. Решение, которое мне помогло, следующее:
Затем я перенёс всё jni, которое было у меня в андроид студии, в qt. Попробовал снова создать QApplication — и всё заработало.
Я почти что уверен, что есть и другой способ решить эту проблему. Если кто-нибудь укажет, где я ошибся — то было бы просто замечательно. В интернете я не нашёл решения проблемы, поэтому предлагаю своё.
Добрый день, уважаемые хабровчане! В этой статье я хочу рассказать о своём опыте использования qt и android studio. А именно о том, как мне надо было в qt нарисовать текст и передать в андроид студию. Несмотря на простоту задачи, решение её заняло у меня довольно много времени и может быть кому-нибудь когда-нибудь где-нибудь сэкономит массу времени. Статья в каком-то смысле претендует на изобретение велосипеда, но в интернете я не нашёл решения. Кому интересно — добро пожаловать под кат!
Немного о самой задаче
Недавно встала передо мною задача портировать приложение с ios на андроид. Основной болью при портировании была работа с SDK приложения. Оно было написано на Qt и исопльзовалось для рисования текста/стрелочек/областей и всего прочего. То есть, приложение было написано на objective c, и использовало qt библиотеку, а не было qt проектом. Поэтому, первым делом встал вопрос среды разработки. Поскольку, я совсем в этом деле новичок, мой выбор пал на андроид студию. Всё-таки весь графический интерфейс, как мне показалось, лучше делать в андроид студии, а вычислительные задачи пущай делает наше qtшное SDK. В интернете не так уж много пишут об использовании qt под андроид, а здесь была задача подружить qt и андроид студию. Для работы с плюсами используется Android NDK и реализуется всё через использование JNI. Работа с JNI — вещь сама по себе довольно интересная. В нете можно найти массу статей на эту тему (например этот замечательный цикл). Однако меня интересует JNI в разрезе использования его с Qt. Опять же, в чём проблема, спросите вы? Берём сишные сорсы, делаем шаред либу, подключаем к проекту в андроид студии и получаем profit! Вот, как например тут. А вот здесь и начинается самое интересное…
Использование qt в андроид студии
Как вы помните, я указал выше, что
оно было написано на Qt и исопльзовалось для рисования текста/стрелочек/областей и всего прочего.
Чтобы нарисовать графический примитив в QT, нам не требуется создавать экземпляр QApplication или QGuiApplication. Даже QCoreApplication — и тот не нужен! А вот для рисования текста без QApplication или QGuiApplication уже никак нельзя обойтись. Так в чём проблема, спросите вы? Проблема наступает как раз на момент вызова конструктора:
QApplication a(argc, argv);
Если вы создадите билиблиотеку, в ней какую-либо функцию, вызывающую конструктор QApplication, а затем вызовите её через JNI из приложения андроид студии, то сразу же словите:
This application failed to start because it could not find or load the Qt platform plugin «android».
Кто винов Что делать?
Вариант классический. Учить матчасть!
Первое, что я решил сделать — нагуглить решение проблемы в интернете. Точного совпадения я не нашёл, но в довольно большом количестве постов люди жаловались на похожие проблемы для плагина под винду. Вот и я перепробовал всё, что было указано здесь, но, увы, решения (работающего для меня! ) не было найдено.
В поисках ответа на свои вопросы, я наткнулся на такой довольно любопытный блог, как я понял автора qt под андроид. Блог весьма интересный, но в нём автор делает акцент (опять же, моё имхо) на разработку со стороны с++ и запуска всего добра из qt creator. Меня такой подход, если честно, не очень устраивал по одной причине: отладка Java части из Qt практически невозможна (можно только компилировать код, потом ждать приаттачивания из андроид студии и уже оттуда наблюдать происходящее), а также у меня довольно большое количество различных layoutov, кастомных вьюх, асинхронных задач, а как это добро засунуть в qt проект и нормально отлаживать? Честно говоря, я не знаю.
Эксперименты
Я попробовал создать также Qt приложение и запустить его на андроиде. Запускал я его через qt-creator и как ни странно оно благополучно запустилось. Я стал смотреть более подробно как устроен манифест, граддл, код приложения. Я обнаружил такую интересную вещь в манифесте:
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="1"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="1"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs" android:value="plugins/platforms/android/libqtforandroid.so"/>
<meta-data android:name="android.app.load_local_jars" android:value="jar/QtAndroid.jar:jar/QtAndroidAccessibility.jar:jar/QtAndroid-bundled.jar:jar/QtAndroidAccessibility-bundled.jar"/>
<meta-data android:name="android.app.static_init_classes" android:value=""/>
Вкратце смысл его понятен. Когда я собирал apk приложения, я указал, что qt библиотеки должны находиться внутри apk и именно оттуда надо их грузить своему приложению. Подключение соответстсвующих jar-ов в проект на андроиде, прописывание в андроидовском манифесте того, что было в qt, размещение qtшных .so плагинов в папке jniLibs не дало никакого эффекта.
Изучение плагинов
Я попробовал уже наконец грузить самостоятельно со стороны java этот несчастный плагин libqtforandroid.so (до создания QApplication) путём
System.loadLibrary(«plugins_platforms_android_libqtforandroid»);, но всё равно падало! Правда, здесь исключение было уже другое и более интересное:
I/Qt: qt start
05-17 11:12:33.975 11084-11084/имя проекта A/libc: Fatal signal 11 (SIGSEGV) at 0x00000000 (code=1), thread 11084 (ndroid.gribview)
05-17 11:12:33.978 11084-11084/имя проекта A/libc: Send stop signal to pid:11084 in void debuggerd_signal_handler(int, siginfo_t, void)
По крайней мере есть у нас зацепка, где можно смотреть. Оперативно по qt start находим интересующий нас метод:
Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void */*reserved*/)
{
QT_USE_NAMESPACE
typedef union {
JNIEnv *nativeEnvironment;
void *venv;
} UnionJNIEnvToVoid;
__android_log_print(ANDROID_LOG_INFO, "Qt", "qt start");
UnionJNIEnvToVoid uenv;
uenv.venv = Q_NULLPTR;
m_javaVM = Q_NULLPTR;
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
__android_log_print(ANDROID_LOG_FATAL, "Qt", "GetEnv failed");
return -1;
}
JNIEnv *env = uenv.nativeEnvironment;
if (!registerNatives(env)
|| !QtAndroidInput::registerNatives(env)
|| !QtAndroidMenu::registerNatives(env)
|| !QtAndroidAccessibility::registerNatives(env)
|| !QtAndroidDialogHelpers::registerNatives(env)) {
__android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed");
return -1;
}
m_javaVM = vm;
return JNI_VERSION_1_4;
}
Судя по логу, он упал где-то в каком-то из registerNatives.Так и было (я прописал логи в каждом из registerNatives). Он падал в
registerNatives(env)
А именно:
jmethodID methodID;
GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "activity", "()Landroid/app/Activity;");
__android_log_print(ANDROID_LOG_INFO, "Check Class 8", "activity ");
jobject activityObject = env->CallStaticObjectMethod(m_applicationClass, methodID);
__android_log_print(ANDROID_LOG_INFO, "Check Class 9 ", " methodID ");
GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "classLoader", "()Ljava/lang/ClassLoader;");
__android_log_print(ANDROID_LOG_INFO, "Check Class 10", " classLoader ");
if(activityObject!=nullptr)
{
__android_log_print(ANDROID_LOG_INFO, "No tull activityObject", " Not Null ");
}
if(methodID!=nullptr)
{
__android_log_print(ANDROID_LOG_INFO, "No tull methodID", " Not Null ");
}
m_classLoaderObject = env->NewGlobalRef(env->CallStaticObjectMethod(m_applicationClass, methodID));
if(m_classLoaderObject!=nullptr)
{
__android_log_print(ANDROID_LOG_INFO, "No tull m_classLoaderObject", " Not Null ");
}
clazz = env->GetObjectClass(m_classLoaderObject);
Падение произошло на последней строчке. classLoaderObject оказался равен null. А это произошло, что activityObject тоже равен null. Окей. Перед тем, как грузить этот злосчастный плагин попробуем создать активити для JNI. Для этого пропишем в Java коде следующие строчки:
QtNative.setActivity(this, null);
QtNative.setClassLoader(getClassLoader());
Небольшое отступление. Класс QtNative лежит в jar файлах, которые мы подключаем к проекту. Более того, это весьма любопытный класс. В нём есть методы:
QtNative.loadBundledLibraries();
QtNative.loadQtLibraries();
которые и должны подгружать требуемые плагины. Пока запомним это, и вернёмся к подключению нашего плагина вручную. Вызов методов QtNative setActivity и setClassLoader помог проскочить:
registerNatives(env)
но засада была уже в QtAndroidInput::registerNatives(env). Не совпадали сигнатуры функций для события keyDown. В принципе, мне ничего не нужно кроме шрифтов и я закомментировал следующий участок кода:
if (!registerNatives(env)
/* || !QtAndroidInput::registerNatives(env)
|| !QtAndroidMenu::registerNatives(env)
|| !QtAndroidAccessibility::registerNatives(env)
|| !QtAndroidDialogHelpers::registerNatives(env)*/) {
__android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed");
return -1;
}
и вроде как-бы благополучно загрузил этот плагин. Запускаем приложение, грузим плагин, вызываем QApplication и… ловим наше
This application failed to start because it could not find or load the Qt platform plugin «android».
Более того, вызов
QtNative.loadBundledLibraries();
QtNative.loadQtLibraries();
тоже не решил проблемы. Хорошо. Ладно. Полезем в сорсы создания конструктора. По исключению быстро находим метод:
static void init_platform(const QString &pluginArgument, const QString &platformPluginPath, const QString &platformThemeName, int &argc, char **argv)
{
// Split into platform name and arguments
QStringList arguments = pluginArgument.split(QLatin1Char(':'));
const QString name = arguments.takeFirst().toLower();
QString argumentsKey = name;
argumentsKey[0] = argumentsKey.at(0).toUpper();
arguments.append(QLibraryInfo::platformPluginArguments(argumentsKey));
// Create the platform integration.
QGuiApplicationPrivate::platform_integration = QPlatformIntegrationFactory::create(name, arguments, argc, argv, platformPluginPath);
if (QGuiApplicationPrivate::platform_integration) {
QGuiApplicationPrivate::platform_name = new QString(name);
} else {
QStringList keys = QPlatformIntegrationFactory::keys(platformPluginPath);
QString fatalMessage
= QStringLiteral("This application failed to start because it could not find or load the Qt platform plugin \"%1\".\n\n").arg(name);
....
Хорошо. Ищем, откуда вызываем сей метод:
void QGuiApplicationPrivate::createPlatformIntegration()
{
// Use the Qt menus by default. Platform plugins that
// want to enable a native menu implementation can clear
// this flag.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, true);
// Load the platform integration
QString platformPluginPath = QLatin1String(qgetenv("QT_QPA_PLATFORM_PLUGIN_PATH"));
QByteArray platformName;
#ifdef QT_QPA_DEFAULT_PLATFORM_NAME
platformName = QT_QPA_DEFAULT_PLATFORM_NAME;
#endif
QByteArray platformNameEnv = qgetenv("QT_QPA_PLATFORM");
if (!platformNameEnv.isEmpty()) {
platformName = platformNameEnv;
}
QString platformThemeName = QString::fromLocal8Bit(qgetenv("QT_QPA_PLATFORMTHEME"));
// Get command line params
QString icon;
int j = argc ? 1 : 0;
for (int i=1; i<argc; i++) {
if (argv[i] && *argv[i] != '-') {
argv[j++] = argv[i];
continue;
}
const bool isXcb = platformName == "xcb";
QByteArray arg = argv[i];
if (arg.startsWith("--"))
arg.remove(0, 1);
if (arg == "-platformpluginpath") {
if (++i < argc)
platformPluginPath = QLatin1String(argv[i]);
} else if (arg == "-platform") {
if (++i < argc)
platformName = argv[i];
} else if (arg == "-platformtheme") {
if (++i < argc)
platformThemeName = QString::fromLocal8Bit(argv[i]);
} else if (arg == "-qwindowgeometry" || (isXcb && arg == "-geometry")) {
if (++i < argc)
windowGeometrySpecification = QWindowGeometrySpecification::fromArgument(argv[i]);
} else if (arg == "-qwindowtitle" || (isXcb && arg == "-title")) {
if (++i < argc)
firstWindowTitle = QString::fromLocal8Bit(argv[i]);
} else if (arg == "-qwindowicon" || (isXcb && arg == "-icon")) {
if (++i < argc) {
icon = QString::fromLocal8Bit(argv[i]);
}
} else {
argv[j++] = argv[i];
}
}
if (j < argc) {
argv[j] = 0;
argc = j;
}
init_platform(QLatin1String(platformName), platformPluginPath, platformThemeName, argc, argv);
if (!icon.isEmpty())
forcedWindowIcon = QDir::isAbsolutePath(icon) ? QIcon(icon) : QIcon::fromTheme(icon);
}
То бишь, нам можно через argc и argv передать аргументы, где надо искать этот плагин. Сразу оговорюсь, я пробовал в дебаггере qt запускать приложение под андроид, и там argc и argv соответственно равны: 1 и имя_нашей_библиотеки_которую_собирает_qt, но никак не плагин. Попробуем присвоить argc и argv соответствующие значения:
char *SDKEnvironment::argv[] = {"-platform libplugins_platforms_android_libqtforandroid.so:plugins/platforms/android/libqtforandroid.so -platformpluginpath /data/app-lib/папка_для_jniLibs"};
Неа, не сработало.
Решение
Честно говоря, сроки поджимают, а дальше заниматься изучением что и где не сработало — у меня нету
- Создадим в qt не apk, не so, а aar. Для этого идём в qt creator и находим gradle файл, а в нём меняем строчку
apply plugin: 'com.android.applicatioin'
наapply plugin: 'com.android.library'
. Таким образом мы создаём aar файл, а не apk - Теперь добавим его в наше приложение в андроид студии. Идём в New->Module выбираем import aar, затем правой кнопкой мышки щёлкаем на наш модуль, выбиараем Open Module Settings, идём во вкладку dependency и добавляем зависимость к qtному модулю
Затем я перенёс всё jni, которое было у меня в андроид студии, в qt. Попробовал снова создать QApplication — и всё заработало.
Резюме
Я почти что уверен, что есть и другой способ решить эту проблему. Если кто-нибудь укажет, где я ошибся — то было бы просто замечательно. В интернете я не нашёл решения проблемы, поэтому предлагаю своё.