Использование Android Studio для сборки приложений с NDK и Boost
29.01.2015 11:40

В предыдущей статье мы рассмотрели как собирать простые исполняемые файлы под Android с использованием библиотек Boost. Это хороший пример для понимания того, как все работает "изнутри"; однако для практических целей хорошо было бы уметь собирать готовые к использованию приложения, которые можно залить в магазин приложений Google Play, к примеру.

Официально предлагаемый способ для создания таких приложений - использование Android Studio. К сожалению, Android Studio не поддерживает сборку C/C++ кода так же хорошо, как Java кода. Поддержка NDK в ней на данный момент очень ограничена. Так, единственно поддерживаемыми NDK приложениями являются только те, которые состоят из одного собираемого модуля (финальной динамической библиотеки), держат все исходные коды на C/C++ в каталоге 'jni', в которых также отсутствуют любые зависимости от других библиотек, и которые нельзя разбить на несколько модулей (т.е. набор статических и динамических библиотек). Не предоставляется никаких возможностей для настройки сборки нативных модулей, за исключением очень ограниченного набора опций:

build.gradle
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

Java

Первым делом, запустите Android Studio и создайте новый Android проект:

Выберите "Android 4.0.3" в качестве целевой версии Android:

Выберите "пустую" activity:

Оставьте все имена как есть и нажмите кнопку "Finish":

Layout

Теперь откройте файл 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

Теперь добавьте такие строки в 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 должно стать таким:

MainActivity.java

Мы закончили заниматься Java-частью приложения; давайте теперь займемся кодом на C++.

C++

Первым делом, создайте каталог, в котором будут лежать исходные файлы на C++:

Используйте 'main' в следующем окне и нажмите кнопку "Finish":

Исходники

Затем добавьте следующие файлы в только что созданный каталог (app/src/main/jni):

Application.mk
Android.mk
gps.hpp
gps.cpp
test.cpp

Сборочный скрипт

Теперь нам надо модифицировать сборочный скрипт, чтобы он правильно собирал наш 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:

Его содержимое должно быть следующим:

build.gradle

Для тех, кому интересно, что именно мы добавили в существующий файл, ниже приводится diff:

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') и стартуйте приложение на устройстве или эмуляторе. Ниже приводится снимок экрана с устройства при запущенном приложении:

Back
Home
Map
Back
Home
Map

Наши авторы: