Розробка Android додатків з використанням qt і android studio

Добрий день, шановні хабровчане! У цій статті я хочу розповісти про свій досвід використання qt і android studio. А саме про те, як мені треба було у qt намалювати текст і передати в андроїд студію. Незважаючи на простоту завдання, вирішення її зайняло у мене досить багато часу і може бути кому-небудь коли-небудь де-небудь заощадить масу часу. Стаття в якомусь сенсі претендує на винахід велосипеда, але в інтернеті я не знайшов рішення. Кому цікаво — ласкаво просимо під кат!

Трохи про саму задачі
Нещодавно встала переді мною завдання портувати додаток з ios на андроїд. Основний болем при портуванні була робота з SDK програми. Воно було написано на 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 бібліотеки повинні знаходитися усередині апк і саме звідти треба їх вантажити своєму додатком. Підключення соответстсвующих 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). Не збігалися сигнатури функцій для події натискання. В принципі, мені нічого не потрібно крім шрифтів і я закомментировал наступний фрагмент коду:

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"};

Неа, не спрацювало.

Рішення
Чесно кажучи, терміни підтискають, а далі займатися вивченням що і де не спрацював — у мене немає силчасу. Рішення, яке мені допомогло, наступне:

  1. Створимо в qt не apk, не so, а aar. Для цього йдемо в qt creator і знаходимо gradle файл, а в ньому міняємо рядок
    apply plugin: 'com.android.applicatioin' 
    на
    apply plugin: 'com.android.library' 
    . Таким чином ми створюємо aar файл, а не apk
  2. Тепер додамо його в наше додаток в андроїд студії. Йдемо в New->Module вибираємо import aar, потім клацаємо правою кнопкою мишки на наш модуль, выбиараем Open Module Settings, йдемо у вкладку dependency і додаємо залежність до qtному модулю
Потім я переніс всі jni, яке було у мене в андроїд студії, в qt. Спробував знову створити QApplication — і все запрацювало.

Резюме
Я майже впевнений, що є й інший спосіб вирішити цю проблему. Якщо хто-небудь скаже, де я помилився — то було б просто чудово. В інтернеті я не знайшов рішення проблеми, тому пропоную свій.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.