Добрый день, уважаемыее! В этой статье я хочу рассказать о своём опыте использования 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 библиотеки должны находиться внутри 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".nn").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 — и всё заработало.
Резюме
Я почти что уверен, что есть и другой способ решить эту проблему. Если кто-нибудь укажет, где я ошибся — то было бы просто замечательно. В интернете я не нашёл решения проблемы, поэтому предлагаю своё.
Автор: Rogvold91