В предыдущей статье мы рассмотрели как собирать простые исполняемые файлы под 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
package net.crystax.examples.testboost;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
public class MainActivity extends ActionBarActivity {
static {
System.loadLibrary("test-boost");
}
private native String getGPSCoordinates(String rootPath);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView field = (TextView)findViewById(R.id.text);
field.setText(getGPSCoordinates(getFilesDir().getAbsolutePath()));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
Мы закончили заниматься Java-частью приложения; давайте теперь займемся кодом на C++.
C++
Первым делом, создайте каталог, в котором будут лежать исходные файлы на C++:
Используйте 'main' в следующем окне и нажмите кнопку "Finish":
Исходники
Затем добавьте следующие файлы в только что созданный каталог (app/src/main/jni):
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test-boost
LOCAL_SRC_FILES := test.cpp gps.cpp
LOCAL_STATIC_LIBRARIES := boost_serialization_static
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
$(call import-module,boost/1.57.0)
gps.hpp
#ifndef GPS_HPP_7D5AF29629F64210BE00F3AF697BA650
#define GPS_HPP_7D5AF29629F64210BE00F3AF697BA650
#include <string>
// include headers that implement a archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
/////////////////////////////////////////////////////////////
// gps coordinate
//
// illustrates serialization for a simple type
//
class gps_position
{
private:
friend class boost::serialization::access;
friend std::ostream &operator<<(std::ostream &, gps_position const &);
// When the class Archive corresponds to an output archive, the
// & operator is defined similar to <<. Likewise, when the class Archive
// is a type of input archive the & operator is defined similar to >>.
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & degrees;
ar & minutes;
ar & seconds;
}
int degrees;
int minutes;
float seconds;
public:
gps_position(){}
gps_position(int d, int m, float s) :
degrees(d), minutes(m), seconds(s)
{}
bool operator==(gps_position const &g) const
{
return degrees == g.degrees &&
minutes == g.minutes &&
seconds == g.seconds;
}
bool operator!=(gps_position const &g) const
{
return !(*this == g);
}
};
void save(std::string const &root, gps_position const &g);
void load(std::string const &root, gps_position &g);
#endif // GPS_HPP_7D5AF29629F64210BE00F3AF697BA650
gps.cpp
#include <fstream>
#include "gps.hpp"
const char *FILENAME = "gps.dat";
std::ostream &operator<<(std::ostream &s, gps_position const &g)
{
s << "GPS(" << g.degrees << "/" << g.minutes << "/" << g.seconds << ")";
return s;
}
void save(std::string const &root, gps_position const &g)
{
// create and open a character archive for output
std::ofstream ofs(root + "/" + FILENAME);
boost::archive::text_oarchive oa(ofs);
// write class instance to archive
oa << g;
// archive and stream closed when destructors are called
}
void load(std::string const &root, gps_position &g)
{
// create and open an archive for input
std::ifstream ifs(root + "/" + FILENAME);
boost::archive::text_iarchive ia(ifs);
// read class state from archive
ia >> g;
// archive and stream closed when destructors are called
}
test.cpp
#include <jni.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <exception>
#include <sstream>
#include <android/log.h>
#define LOG(fmt, ...) __android_log_print(ANDROID_LOG_INFO, "TEST-BOOST", fmt, ##__VA_ARGS__)
#include "gps.hpp"
std::string gps(std::string const &root)
{
const gps_position g(35, 59, 24.567f);
save(root, g);
gps_position newg;
load(root, newg);
std::ostringstream ostr;
if (g != newg)
return std::string();
ostr << "GPS coordinates: " << newg;
return ostr.str();
}
extern "C"
jstring
Java_net_crystax_examples_testboost_MainActivity_getGPSCoordinates( JNIEnv* env,
jobject thiz,
jstring rootPath )
{
const char *s = env->GetStringUTFChars(rootPath, 0);
std::string root(s);
env->ReleaseStringUTFChars(rootPath, s);
LOG("root: %s", root.c_str());
try {
std::string ret = gps(root);
return env->NewStringUTF(ret.c_str());
}
catch (std::exception &e) {
LOG("ERROR: %s", e.what());
abort();
}
catch (...) {
LOG("Unknown error");
abort();
}
}
Сборочный скрипт
Теперь нам надо модифицировать сборочный скрипт, чтобы он правильно собирал наш 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
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "net.crystax.examples.testboost"
minSdkVersion 15
targetSdkVersion 21
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
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
}
Для тех, кому интересно, что именно мы добавили в существующий файл, ниже приводится 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') и стартуйте приложение
на устройстве или эмуляторе.
Ниже приводится снимок экрана с устройства при запущенном приложении: