В предыдущей статье мы рассмотрели как собирать простые исполняемые файлы под Android с использованием библиотек Boost. Это хороший пример для понимания того, как все работает "изнутри"; однако для практических целей хорошо было бы уметь собирать готовые к использованию приложения, которые можно залить в магазин приложений Google Play, к примеру.
Официально предлагаемый способ для создания таких приложений - использование Android Studio. К сожалению, Android Studio не поддерживает сборку C/C++ кода так же хорошо, как Java кода. Поддержка NDK в ней на данный момент очень ограничена. Так, единственно поддерживаемыми NDK приложениями являются только те, которые состоят из одного собираемого модуля (финальной динамической библиотеки), держат все исходные коды на C/C++ в каталоге 'jni', в которых также отсутствуют любые зависимости от других библиотек, и которые нельзя разбить на несколько модулей (т.е. набор статических и динамических библиотек). Не предоставляется никаких возможностей для настройки сборки нативных модулей, за исключением очень ограниченного набора опций:
defaultConfig { ... ndk { moduleName "my-module-name" cFlags "-std=c++11 -fexceptions" ldLibs "log" stl "gnustl_shared" abiFilter "armeabi-v7a" } } }
Для сборки нативных модулей доступны только опции moduleName, cFlags, ldLibs, stl и abiFilter; мы не можем указать дополнительные зависимости (такие как библиотеки Boost). Мы не можем указать пути к библиотекам, чтобы линкер знал, где их искать. Список можно продолжить - недоступно очень много настроек.
Это происходит оттого, что gradle plug-in (используемый Android Studio для сборки проектов) игнорирует существующие файлы Application.mk и Android.mk из каталога 'jni'. Вместо этого он генерирует собственный Android.mk на лету, используя настройки из сборочного скрипта.
С практической точки зрения единственный рабочий способ собирать такие приложения в Android Studio - это полностью отключить ее ограниченную поддержку NDK и вызывать $NDK/ndk-build самостоятельно. В этой статье мы опишем шаг за шагом, как это сделать.
Мы создадим с нуля простое приложение в Android Studio, и затем добавим к нему части, написанные на C++. Мы предполагаем, что вы уже установили Android Studio и Android SDK; мы также предполагаем, что вы скачали и распаковали CrystaX NDK
Первым делом, запустите Android Studio и создайте новый Android проект:
Выберите "Android 4.0.3" в качестве целевой версии Android:
Выберите "пустую" activity:
Оставьте все имена как есть и нажмите кнопку "Finish":
Теперь откройте файл app/res/layout/activity_main.xml и поправьте его, чтоб он выглядел так:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout>
Теперь добавьте такие строки в MainActivity.onCreate():
TextView field = (TextView)findViewById(R.id.text); field.setText(getGPSCoordinates(getFilesDir().getAbsolutePath()));
Добавьте объявление нативного метода в класс MainActivity:
private native String getGPSCoordinates(String rootPath);
Также, не забудьте добавить загрузку динамической библиотеки в статический блок инициализации:
static { System.loadLibrary("test-boost"); }
В результате содержимое файла MainActivity.java должно стать таким:
Мы закончили заниматься Java-частью приложения; давайте теперь займемся кодом на C++.
Первым делом, создайте каталог, в котором будут лежать исходные файлы на C++:
Используйте 'main' в следующем окне и нажмите кнопку "Finish":
Затем добавьте следующие файлы в только что созданный каталог (app/src/main/jni):
Теперь нам надо модифицировать сборочный скрипт, чтобы он правильно собирал наш C++ код вместе с Java. Для этого нам сперва надо открыть файл local.properties и добавить туда путь к CrystaX NDK:
sdk.dir=/opt/android/android-sdk-mac ndk.dir=/opt/android/crystax-ndk-10.1.0
Пользователям Windows: обратные слэши и двоеточия должны быть экранированы:
sdk.dir=C\:\\android\\android-sdk-mac ndk.dir=C\:\\android\\crystax-ndk-10.1.0
И наконец, откроем и отредактируем файл build.gradle:
Его содержимое должно быть следующим:
Для тех, кому интересно, что именно мы добавили в существующий файл, ниже приводится diff:
diff --git a/build.gradle b/build.gradle index a6b8c98..08dce1c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import org.apache.tools.ant.taskdefs.condition.Os + apply plugin: 'com.android.application' android { @@ -17,9 +19,50 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + sourceSets.main.jni.srcDirs = [] // disable automatic ndk-build call, which ignore our Android.mk + sourceSets.main.jniLibs.srcDir 'src/main/libs' + + // call regular ndk-build(.cmd) script from app directory + task ndkBuild(type: Exec) { + workingDir file('src/main') + commandLine getNdkBuildCmd() + } + + tasks.withType(JavaCompile) { + compileTask -> compileTask.dependsOn ndkBuild + } + + task cleanNative(type: Exec) { + workingDir file('src/main') + commandLine getNdkBuildCmd(), 'clean' + } + + clean.dependsOn cleanNative } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:21.0.3' } + +def getNdkDir() { + if (System.env.ANDROID_NDK_ROOT != null) + return System.env.ANDROID_NDK_ROOT + + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + def ndkdir = properties.getProperty('ndk.dir', null) + if (ndkdir == null) + throw new GradleException("NDK location not found. Define location with ndk.dir in the local.properties file or with an ANDROID_NDK_ROOT environment variable.") + + return ndkdir +} + +def getNdkBuildCmd() { + def ndkbuild = getNdkDir() + "/ndk-build" + if (Os.isFamily(Os.FAMILY_WINDOWS)) + ndkbuild += ".cmd" + + return ndkbuild +}
Файловое дерево каталога TestBoost/app должно теперь выглядеть так:
. ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest │ └── java │ └── net │ └── crystax │ └── examples │ └── testboost │ └── ApplicationTest.java └── main ├── AndroidManifest.xml ├── java │ └── net │ └── crystax │ └── examples │ └── testboost │ └── MainActivity.java ├── jni │ ├── Android.mk │ ├── Application.mk │ ├── gps.cpp │ ├── gps.hpp │ └── test.cpp └── res .......
Дело сделано! Теперь запустите сборку проекта как обычно (Build -> Make Module 'app') и стартуйте приложение на устройстве или эмуляторе. Ниже приводится снимок экрана с устройства при запущенном приложении: