На С++ написано огромное количество кода. Мне хотелось бы как-то использовать этот код в своих приложения, но почему-то у меня процесс использования вызывал некий дискомфорт. Скорей всего, это, банально, было связано с тем, что основой моей рабочей средой является Android Studio, в которой работа с нативным кодом, скажем так, не самая лучшая. Но вот мысль о том, чтобы комфортно разрабатывать приложение с нативной частью не оставляла меня никогда. Поэтому я решил попробовать скрестить всю мощь библиотеки Qt и приложение, написаное на родном для Android языке — Java.
На данный момент экосистема Qt разрешает писать приложение полностью средствами этого фреймворка, даже больше — компилировать его для целого набора операционных систем, среди которых находится и Android. Но двигаясь этим путем очень сложно разработать интерфейс, который будет выглядеть и вести себя соответственно гайдам платформ. Да и становится невозможным использовать библиотеки, написаные на Java для оформление внешнего вида и поведения приложения. Именно поэтому хотелось бы испробовать модель разработки, при которой вся бизнес логика писалась на С++, а пользовательский интерфейс — средствами целевой платформы. В дальнейшем это обеспечит простой перенос приложения на другие платформы с внешним видом, характерным для целевой системы и функционалом, который уже проверен и отложен.
Итак, дальше вас ожидает инструкция о том, как написать приложение, которое будет иметь часть, написаную на Qt, и часть на Java.
Для работы нам понадобится библиотека Qt и ide Qt Creator, настроенные для работы с Android-проектами (это сделать довольно просто, подробно об этом можно прочитать по ссылке), Android Studio, а так же Android ndk.
1. Для начала нужно создать андроид проект (назовем его QTtests) и в качестве имени пакета зададим «penguin.in.flight.qttests». На следующих этапах мастера создания проектов можно выбирать любые, удобные для работы, настройки. Главным условием для того, чтобы можно было следовать дальнейшим инструкциям является соблюдение имени пакета.
2. Когда Android проект уже есть, можно на время забыть о нем и перейти к созданию Qt части. Первым шагом будет создание (желательно в корне основного проекта) стандартного проекта типа библиотека. Дадим ему имя cpp_lib. При создании проекта в качестве целевой платформы нужно выбрать armeabi-v7.
3. Шаблон проекта у нас есть, теперь самое время написать наш Hello Word на Qt.
cpp_lib.h
#ifndef CPP_LIB_H
#define CPP_LIB_H
#include "cpp_lib_global.h"
#include <jni.h>
#include <QString>
class CPP_LIBSHARED_EXPORT Cpp_lib
{
public:
Cpp_lib();
QString say(QString subject);
};
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello(JNIEnv *env, jobject obj);
#ifdef __cplusplus
}
#endif
#endif // CPP_LIB_H
cpp_lib.cpp
#include "cpp_lib.h"
Cpp_lib::Cpp_lib()
{
}
QString Cpp_lib::say(QString subject)
{
QString result = QString("Qt say \" %1 \"").arg(subject);
return result;
}
JNIEXPORT jstring JNICALL Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello(JNIEnv *env, jobject obj){
Cpp_lib *lib = new Cpp_lib();
return env->NewStringUTF(lib->say("Hello android!!!").toLatin1().data());
}
Как видно по коду, приложение у нас будет очень простое. Оно содержит две функции — say и Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello. Первая — простая и понятная. Они принимает на вход объект, который в фреймворка является текстовой строкой, и отдает новую строку. Намного интереснее следующая функция. В ней JNIEXPORT и JNICALL обозначаю то, что она может быть вызвана с кода, написаного на джава. В качестве возвращаемого типа она имеет jstring (который доступен нам благодаря этому инклуду #include <jni.h>). Этот тип является типом String в джава.
В завершение разберем аргументы метода. *env — это ссылка на структуру, которая содержит методы, необходимые для работы с сущностями джавы, а obj является обьектом, который вызвал метод.
3. После того, как код написан, нужно сделать так, чтобы наша библиотека корректно собиралась и мы могли использовать ее в своих проектах. Для этого нам нужно убрать вот эту строку TEMPLATE = lib в файле настроек проекта ( в нашем случае это cpp_lib.pro). Если этого не сделать то при сборке мы получим ошибку, вроде этой:
Internal Error: Could not find .pro file.
Error while building/deploying project cpp_lib (kit: Android для armeabi-v7a (GCC 4.9, Qt 5.4.1) )
When executing step «Build Android APK»
4. Дальше нужно сделать так, чтобы Qt Creator собирал библиотеку по таком адресу: {путь_к_проекту}/QTtests/app/src/main/qt_output:
5. Там же, где мы указывали место сборки, нужно проверить настройки сборки приложения. А именно — целевую платформу сборки. Нужно выбрать последнюю (у меня это android 22). Далее поставить галочку напротив Build Qt libraries in APK. Это нужно для того, чтобы система сборки сгенерировала все необходимые библиотеки. Так же нужно поставить галочку напротив Use Gradle — для того, чтобы система сборки сгенерировала проект нужного нам типа.
6. Теперь мы можем собрать библиотеку и получить все необходимые файлы.
7. На этом работа с нативной библиотекой оканчивается. Мы возвращаемся назад в Android studio. Первое, что нужно сделать — отредактировать файл сборки, чтобы подключить Qt библиотеки. Для этого нужно добавить следующие строки в файле /QT_tests/app/build.gradle:
sourceSets {
main {
java.srcDirs += ['/src/main/java']
res.srcDirs += ['src/main/res','src/main/qt_output/android-build/res']
jniLibs.srcDirs += 'src/main/qt_output/android-build/libs'
}
}
src/main/qt_output/android-build/res нужно для того, чтобы иметь доступ к ресурсам, в которые система сборки прописала имена библиотек, от которых зависит основной Qt проект. Путь src/main/qt_output/android-build/libs указывает на место, где лежат сами файлы библиотеки.
Так же модифицируем блок зависимостей следующим образом:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile files('src/main/qt_output/android-build/libs/QtAndroid-bundled.jar')
compile files('src/main/qt_output/android-build/libs/QtAndroidAccessibility-bundled.jar')
//остальные зависимости
}
8. Теперь у нас есть собрание библиотеки и они даже подключены к проекту. Далее нужно подключить их во время старта самой программы. Для этого будем использовать следующий класс:
package penguin.in.flight.qttests.utils;
import android.content.Context;
import android.util.Log;
import penguin.in.flight.qttests.R;
/**
* Created on 11.04.15.
*/
public class JavaNatives {
public native static String sayHello();
public static void init(Context context) {
load(context, R.array.bundled_in_assets);
load(context, R.array.qt_libs);
System.loadLibrary("cpp_lib");
}
static void load(Context context, int arrayResourceId) {
String[] libsToLoad = context.getResources().getStringArray(arrayResourceId);
for (String lib : libsToLoad) {
if (lib.indexOf('/') > -1) {
lib = lib.substring(lib.indexOf('/'));
}
if (lib.indexOf("lib") == 0) {
lib = lib.substring(3);
}
if (lib.endsWith(".so")) {
lib = lib.substring(0, lib.length() - 3);
}
Log.i(JavaNatives.class.getSimpleName(), "loading " + lib);
try {
System.loadLibrary(lib);
} catch (Throwable e) {
Log.i(JavaNatives.class.getSimpleName(), "failed to load " + lib + " " + e);
e.printStackTrace();
}
Log.i(JavaNatives.class.getSimpleName(), "Successfully loaded " + lib);
}
}
}
Как видно из кода, класс находится в пакете penguin.in.flight.qttests.utils и называется JavaNatives. Это важно для того, чтобы обеспечить связь между тем кодом, который мы написали в нативной части. Чтоб понять как, давайте вспомним метод с именем Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello. Его имя состоит из префикса Java_, потом идет имя пакета, где имена саб пакетов разделены символом "_" (penguin_in_flight_qttests_utils). Дальше идет имя класса и, наконец, имя метода. Детальнее о именование методов в нативном кодом можно почитать здесь.
9. Чтобы использовать написаный нами класс в onCreate в стартовой активити, нужно вставить эту строку:
JavaNatives.init(this);
10. Теперь осталось только использовать то, что мы написали. Для этого добавим кнопку и в обработке нажатия на нее вставим следующий код:
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setMessage(JavaNatives.sayHello());
dialog.setPositiveButton("Ok", null);
dialog.show();
На этом инструкции об использовании нативной части, написаной на Qt в Андроид приложениях, окончены.
Всем спасибо за внимание, весь код примера доступен на github.com.
Комментарии (17)
tywonka
13.04.2015 19:15Я бы посоветовал использовать не такие ужасающие названия методов
JNIEXPORT jstring JNICALL Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello(JNIEnv *env, jobject obj)
а сделать так:
static JNINativeMethod methods[] = { {"someMethod", "()Ljava/lang/String;", (void *)someMethod} };
и в методеJNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
зарегистрировать указанные нативные методы
if (env->RegisterNatives(javaClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) return JNI_ERR;
В таком случае, вам не нужно будет переименовывать все эти методы, если вдруг понадобиться, например, изменить имя пакета или что-то другое, ну и исчезнут эти монструозные названия методов.
ram2406
14.04.2015 16:26Да действительно удобно.
А вы пробовали SWIG?
Позволяет избежать неприятных моментов несоответствия версий нативного и клиентского кода.
Дополнительное удобство в обработке исключений, стоит нам дописать к методу " throw(std::exception)".
Плюс — подготовленные шаблоны для vector, wstring.
И самое главное он очень гибкий инструмент и не только под Java.
Имхо, незаменимый инструмент для кросс-платформенной разработки на основе C/C++.
Beltoev
13.04.2015 23:53Qt под андроид всё также требует дополнительно качать 10-12 МБ (министро вроде, точно не помню) на телефон?
bitterman
14.04.2015 11:46можно так, можно эдак, но с Министро работает лучше. Меньше проблем с потоками, поверхностями, инициализацией. В процессе вылавливания проблем с чёрным экраном на ровном месте и теряющимися сообщениями (т.к. parent is in other thread) перешёл на Министро и начал спать спокойно.
ram2406
14.04.2015 16:32А как у вас дела с отладкой нативной библиотеки в таком случае обстоит?
Эти сценарии мне не очень-то помогли:
github.com/mapbox/mapbox-gl-native/wiki/Android-debugging-with-remote-GDB
geekswithblogs.net/raccoon_tim/archive/2011/09/12/working-with-android-on-windows-and-without-cygwin.aspx
comments.gmane.org/gmane.comp.lib.qt.android/3462
Видимо руки кривые… :(VioletGiraffe
14.04.2015 20:58Отладка нативного кода на Андроиде — это боль. У меня она регулярно ломается по непонятным причинам. Т. е. буквально: сегодня работает, прихожу утром на следующий день — не работает. И это в Эклипсе, под который больше гайдов и советов на SO.
Ещё и банальные вещи вроде визуализации std::vector Эклипс не умеет.
Извините, накипело.ram2406
15.04.2015 10:29Ну примерно это я и ожидал услышать. Имхо, это настоящий ад)
Вычитав первое предложение этой статьи, я рассчитывал увидеть как подружить Qt Creator + gradle + Java (о которой возможности было заявлено разработчиками)…
Суть моей проблемы в том, что мне приходится использовать ту же Android Studio. IDE которую используют Android-разработчики, клиенты моей библиотеки. И в этом проекте используется gradle. А так же Google Maps SDK — где я переопределяю тайловый провайдер и заполняю его нативной начинкой. Т.е. локальный рендеринг карты. Да сценарий не самый лучший, но не я его инициатор.
Саму библиотеку я разрабатываю под Qt Creator'ом — удобно, если привыкнуть (тот же alt+enter под Windows). И с отладкой в нем проблем нет, если на выходе приложение, а не .so модуль (std::vector вполне читаем).
В итоге получается что на данный момент есть плавающий баг, который повторяется только Java-приложении с .so библиотекой. И разобраться в чем именно проблема не получается.
Вообщем попробую Eclipse. Спасибо, за комментарий.
IrixV
Добрый день. Прошу прощения за вопрос «не в кассу». Можно ли как-то в Android Studio вести несколько проектов с использованием общего кода. Есть ли в java нечто аналогично #ifdef и куда при этом пихать переменные препроцессора? Использовать статические переменные не получается, так как нужно подсоединять различные ресурсы strings.xml и color.xml. Наверняка многие сталкивались с такой проблемой.
А вот по поводу «дружбы Java и C++ в одном приложение», очень интересно как обстоят дела с отладкой сишного кода из Android Studio? У меня как-то отладка кода из Cocos2dx так и не задалась, хотя компилировалось все и запускалось нормально.
Braiko Автор
По первой части вопроса — то можно что-то вроде этого сделать:
таким образом можно создать несколько файлов с ресурсами( и кодом) и выбирать тип сборки какой нужно. Это максимально близкое что можно придумать.
Нащет отладки — то в описанной схеме она по прежнему не возможно, но используя среду Qt Creator можно параллельно сделать проекто-демку, который можно запускать на андроиде и без проблем отлаживать.
IrixV
Я вижу, что вы отделили ресурсы, но не вижу куда вы подключили refcotor_list и old_list?
Braiko Автор
Извините за неполный ответ)) Еще нужно дописать что-то вроде этого кода.
После этого можно будет собирать соответственный тип сборки
или же выполнить соответствующую задачу, например assembleRefcotor_list
IrixV
Большое спасибо!
silentnuke
в данном случае правильние использовать flavor, а не build type.
IrixV
Согласна. Спасибо за ссылку. Просто, мне картинка выше помогла найти место переключения билдов. Как-то все это не очень очевидно по сравнению с ios), хотя может дело привычки.
toxicdream
Методом тыка нашел в первый день работы с Android Studio.
silentnuke
По первому вопросу прочитайте про flavor, flavorDimensions и build type. Начать можно остюда. developer.android.com/tools/building/configuring-gradle.html