Pull to refresh

Разработка Android приложений с использованием qt и android studio

Reading time 9 min
Views 25K
Неактуально, кроме постановки задачи. Правильное решение в следующей статье.
Добрый день, уважаемые хабровчане! В этой статье я хочу рассказать о своём опыте использования 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"};

Неа, не сработало.

Решение


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

  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 — и всё заработало.

Резюме


Я почти что уверен, что есть и другой способ решить эту проблему. Если кто-нибудь укажет, где я ошибся — то было бы просто замечательно. В интернете я не нашёл решения проблемы, поэтому предлагаю своё.
Tags:
Hubs:
+4
Comments 19
Comments Comments 19

Articles