Ранее мы описали как использовать CrystaX NDK в Android Studio.
С того времени Google анонсировал новый (экпериментальный) Gradle плагин с улучшенной
поддержкой NDK в Android Studio. Давайте посмотрим, как использовать CrystaX NDK
с этим новым плагином.
Будучи последовательными, мы обновим предыдущую статью, добавив в нее новые условия - т.е.
это будет все то же простое UI приложение, использующее Boost.Serialization. Использование
Boost необязательно для понимания новой схемы работы с экспериментальным Gradle плагином;
тем не менее, мы включили пример работы с Boost для большей полноты примера.
В новом Gradle плагине Google отказался от использования старой, основанной на GNU Make, системы
сборки (ndk-build). Теперь для описания каждого аспекта сборки используется Gradle DSL.
Это хорошая новость для тех, кто не знаком со старым добрым GNU Make, и желает, чтобы весь
процесс сборки целиком контролировался IDE, не заботясь о правке файлов для GNU Make.
Однако у этого подхода есть свои минусы. Наиболее важный - это необходимость разбираться в
Gradle DSL и, само собой, уметь хотя бы немного программировать на Groovy.
Давайте посмотрим как это работает.
Java
Первым делом запустите Android Studio и создайте новый Android проект:
Выберите "Android 4.0.3" в качестве целевой версии Android:
Выберите "пустую" activity:
Оставьте все имена как есть и нажмите кнопку "Finish":
Gradle
По умолчанию, Android Studio все еще использует старый Gradle плагин. Давайте переключимся на новый (экспериментальный).
Для начала, откройте и отредактируйте несколько файлов:
build.gradle.diff
diff --git a/build.gradle b/build.gradle
index e0b366a..a4d7068 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.5.0'
+ classpath 'com.android.tools.build:gradle-experimental:0.3.0-alpha7'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
gradle/wrapper/gradle-wrapper.properties.diff
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f23df6e..bfcae0a 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.6-all.zip
app/build.gradle.diff
diff --git a/app/build.gradle b/app/build.gradle
index 3fd571b..d9b2bf0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,20 +1,23 @@
-apply plugin: 'com.android.application'
+apply plugin: 'com.android.model.application'
-android {
- compileSdkVersion 23
- buildToolsVersion "23.0.2"
+model {
+ android {
+ compileSdkVersion = 23
+ buildToolsVersion = "23.0.2"
- defaultConfig {
- applicationId "net.crystax.examples.testboost2"
- minSdkVersion 15
- targetSdkVersion 23
- versionCode 1
- versionName "1.0"
+ defaultConfig.with {
+ applicationId = "net.crystax.testboost2"
+ minSdkVersion.apiLevel = 15
+ targetSdkVersion.apiLevel = compileSdkVersion.asType(Integer)
+ versionCode = 1
+ versionName = "1.0"
+ }
}
- buildTypes {
+
+ android.buildTypes {
release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ minifyEnabled = false
+ proguardFiles += file('proguard-rules.pro')
}
}
}
Теперь нам надо открыть local.properties и добавить путь к CrystaX NDK:
sdk.dir=/opt/android/android-sdk-mac
ndk.dir=/opt/android/crystax-ndk-10.3.1
Пользователям Windows: обратные слэши и двоеточия должны быть экранированы:
sdk.dir=C\:\\android\\android-sdk-mac
ndk.dir=C\:\\android\\crystax-ndk-10.3.1
Теперь нужно синхронизировать изменения в gradle:
Layout
Теперь откройте файл app/res/layout/content_main.xml и поправьте его, чтоб он выглядел так:
content_main.xml.diff
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
index ec3933f..077dacc 100644
--- a/app/src/main/res/layout/content_main.xml
+++ b/app/src/main/res/layout/content_main.xml
@@ -13,7 +13,9 @@
tools:showIn="@layout/activity_main">
<TextView
+ android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Hello World!" />
+ />
+
</RelativeLayout>
content_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="net.crystax.examples.testboost2.MainActivity"
tools:showIn="@layout/activity_main">
<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-boost2");
}
Суммарные изменения:
MainActivity.java.diff
diff --git a/app/src/main/java/net/crystax/examples/testboost2/MainActivity.java b/app/src/main/java/net/crystax/examples/testboost2/MainActivity.java
index f9c0821..0519122 100644
--- a/app/src/main/java/net/crystax/examples/testboost2/MainActivity.java
+++ b/app/src/main/java/net/crystax/examples/testboost2/MainActivity.java
@@ -8,9 +8,16 @@ import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
+import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
+ static {
+ System.loadLibrary("test-boost2");
+ }
+
+ private native String getGPSCoordinates(String rootPath);
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -26,6 +33,9 @@ public class MainActivity extends AppCompatActivity {
.setAction("Action", null).show();
}
});
+
+ TextView field = (TextView) findViewById(R.id.text);
+ field.setText(getGPSCoordinates(getFilesDir().getAbsolutePath()));
}
@Override
В результате содержимое файла MainActivity.java должно стать таким:
MainActivity.java
package net.crystax.examples.testboost2;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("test-boost2");
}
private native String getGPSCoordinates(String rootPath);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
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++
Первым делом включим нативную сборку в app/build.gradle:
app/build.gradle.diff
diff --git a/app/build.gradle b/app/build.gradle
index d9b2bf0..4debb04 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,6 +14,15 @@ model {
}
}
+ android.ndk {
+ moduleName = "test-boost2"
+ cppFlags += "-std=c++11"
+ cppFlags += "-fexceptions"
+ cppFlags += "-frtti"
+ stl = "gnustl_shared"
+ }
+
android.buildTypes {
release {
minifyEnabled = false
Теперь откроем MainActivity.java и добавим реализацию нативного метода:
Это создаст новый файл app/src/main/jni/test-boost2.c. Переименуем его в app/src/main/jni/test.cpp,
а также добавим два новых файла: app/src/main/jni/gps.hpp и app/src/main/jni/gps.cpp.
app/src/main/jni/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
app/src/main/jni/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
}
app/src/main/jni/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_testboost2_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();
}
}
Наконец, добавим новые задачи и вспомогательные функции в app/build.gradle:
app/build.gradle.diff
diff --git a/app/build.gradle b/app/build.gradle
index 3da6874..b5642f9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,5 +1,10 @@
+import org.gradle.internal.os.OperatingSystem;
+
apply plugin: 'com.android.model.application'
+final APP_ABIS = ["armeabi", "armeabi-v7a", "x86"]
+final BOOST_SHARED_LIBS = ["boost_serialization"]
+
model {
android {
compileSdkVersion = 23
@@ -15,10 +20,14 @@ model {
}
android.ndk {
+ toolchain = "gcc"
+ toolchainVersion = "5"
moduleName = "test-boost2"
cppFlags += "-std=c++11"
cppFlags += "-fexceptions"
cppFlags += "-frtti"
+ cppFlags += "-I" + getBoostIncDir()
+ ldLibs.addAll BOOST_SHARED_LIBS
ldLibs += "log"
stl = "gnustl_shared"
}
@@ -29,6 +38,62 @@ model {
proguardFiles += file('proguard-rules.pro')
}
}
+
+ android.productFlavors {
+ APP_ABIS.each { abi ->
+ create(getFlavorName(abi)) {
+ ndk.with {
+ abiFilters += abi
+ getPrebuiltLibPaths(abi).each { path ->
+ ldFlags += "-L" + path
+ }
+ }
+ }
+ }
+ }
+}
+
+tasks.all {
+ task ->
+ if (task.name.startsWith('link')) {
+ task.dependsOn copyNativeLibs, stripNativeLibs
+ }
+}
+
+task copyNativeLibs {
+ ["debug", "release"].each { buildType ->
+ APP_ABIS.each { abi ->
+ def libs = [:]
+ BOOST_SHARED_LIBS.each { name ->
+ libs[name] = "${getBoostLibDir(abi)}/lib${name}.so"
+ }
+ libs.crystax = getLibCrystax(abi)
+
+ libs.each { name, file ->
+ dependsOn tasks.create(name: "copy-native-library-${name}-${abi}-${buildType}", type: Copy) {
+ from file
+ into getTargetLibDir(abi, buildType)
+ }
+ }
+ }
+ }
+}
+
+task stripNativeLibs(dependsOn: copyNativeLibs) {
+ ["debug", "release"].each { buildType ->
+ APP_ABIS.each { abi ->
+ def libs = []
+ libs.addAll(BOOST_SHARED_LIBS)
+ libs += "crystax"
+
+ libs.each { name ->
+ dependsOn tasks.create(name: "strip-native-library-${name}-${abi}-${buildType}", type: Exec) {
+ commandLine getStripExecutable(abi), "--strip-unneeded", "${getTargetLibDir(abi, buildType)}/lib${name}.so"
+ }
+ }
+
+ }
+ }
}
dependencies {
@@ -37,3 +102,152 @@ dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
}
+
+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 getCrystaxNdkDir() {
+ def ndkDir = getNdkDir()
+ if (!(new File(ndkDir, "sources/crystax").exists()))
+ throw new GradleException("""\
+ '${ndkDir}' is not a CrystaX NDK.
+ Edit ndk.dir in local.properties or set ANDROID_NDK_ROOT
+ environment variable pointing to CrystaX NDK""")
+
+ return ndkDir
+}
+
+def getFlavorName(abi) {
+ switch (abi) {
+ case "armeabi":
+ return "arm";
+ case "armeabi-v7a":
+ return "arm7"
+ case "arm64-v8a":
+ return "arm64"
+ default:
+ return abi.replaceAll('-', '_')
+ }
+}
+
+def getToolchainName(abi) {
+ switch (abi) {
+ case ~/^armeabi.*/:
+ return "arm-linux-androideabi"
+ case ~/^arm64.*/:
+ return "aarch64-linux-android"
+ case "mips":
+ return "mipsel-linux-android"
+ case "mips64":
+ return "mips64el-linux-android"
+ case ["x86", "x86_64"]:
+ return abi
+ default:
+ throw new GradleException("Unsupported ABI: '${abi}'")
+ }
+}
+
+def getToolchainPrefix(abi) {
+ switch (abi) {
+ case ~/^armeabi.*/:
+ return "arm-linux-androideabi"
+ case ~/^arm64.*/:
+ return "aarch64-linux-android"
+ case "mips":
+ return "mipsel-linux-android"
+ case "mips64":
+ return "mips64el-linux-android"
+ case "x86":
+ return "i686-linux-android"
+ case "x86_64":
+ return "x86_64-linux-android"
+ default:
+ throw new GradleException("Unsupported ABI: '${abi}'")
+ }
+}
+
+def getHostOS() {
+ if (OperatingSystem.current().isLinux())
+ return "linux"
+ if (OperatingSystem.current().isMacOsX())
+ return "darwin"
+ if (OperatingSystem.current().isWindows())
+ return "windows"
+ throw new GradleException("Unsupported host OS")
+}
+
+def getHostArch() {
+ def arch = System.getProperty("os.arch")
+ switch (arch) {
+ case ["x86_64", "amd64"]:
+ return "x86_64"
+ case ~/^i[3456]86/:
+ case "x86":
+ return "x86"
+ default:
+ throw new GradleException("Can't detect host's CPU architecture: '${arch}'")
+ }
+}
+
+def getHostTag() {
+ def tag = getHostOS()
+ def arch = getHostArch()
+ if (tag != "windows" || arch != "x86")
+ tag += "-${arch}"
+ return tag
+}
+
+def getStripExecutable(abi) {
+ def ndk = getCrystaxNdkDir()
+ def toolchainName = getToolchainName(abi)
+ def toolchainPrefix = getToolchainPrefix(abi)
+ def hostTag = getHostTag()
+ def strip = "${ndk}/toolchains/${toolchainName}-5/prebuilt/${hostTag}/bin/${toolchainPrefix}-strip"
+ if (OperatingSystem.current().isWindows())
+ strip = strip.replaceAll('/', '\\\\') + '.exe'
+ return strip
+}
+
+def getPrebuiltLibPaths(abi) {
+ def paths = []
+ paths += getBoostLibDir(abi)
+ paths += getLibCrystaxDir(abi)
+ return paths
+}
+
+def getTargetLibDir(abi, buildType) {
+ return "${buildDir}/intermediates/binaries/${buildType}/${getFlavorName(abi)}/lib/${abi}"
+}
+
+def getLibCrystaxDir(abi) {
+ return "${getCrystaxNdkDir()}/sources/crystax/libs/${abi}"
+}
+
+def getLibCrystax(abi) {
+ return "${getLibCrystaxDir(abi)}/libcrystax.so"
+}
+
+def getBoostDir() {
+ return "${getCrystaxNdkDir()}/sources/boost/1.59.0"
+}
+
+def getBoostIncDir() {
+ return "${getBoostDir()}/include"
+}
+
+def getBoostLibDir(abi) {
+ return "${getBoostDir()}/libs/${abi}/gnu-5"
+}
Структура каталога
Файловое дерево каталога TestBoost2/app должно теперь выглядеть так:
.
├── app.iml
├── build.gradle
├── libs
├── proguard-rules.pro
└── src
├── androidTest
│ └── java
│ └── net
│ └── crystax
│ └── examples
│ └── testboost2
│ └── ApplicationTest.java
├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── net
│ │ └── crystax
│ │ └── examples
│ │ └── testboost2
│ │ └── MainActivity.java
│ ├── jni
│ │ ├── gps.cpp
│ │ ├── gps.hpp
│ │ └── test.cpp
│ └── res
│ ├── drawable
│ ├── layout
│ │ ├── activity_main.xml
│ │ └── content_main.xml
│ ├── menu
│ │ └── menu_main.xml
│ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── values-v21
│ │ └── styles.xml
│ └── values-w820dp
│ └── dimens.xml
└── test
└── java
└── net
└── crystax
└── examples
└── testboost2
└── ExampleUnitTest.java
Конечный результат
Дело сделано! Теперь запустите сборку проекта как обычно (Build -> Make Module 'app') и стартуйте приложение
на устройстве или эмуляторе. Ниже приводится снимок экрана с устройства при запущенном приложении:
Пример
Для простоты мы залили готовый к использованию проект на
GitHub.
Скачивайте и наслаждайтесь!