在 上一篇, 我们介绍了如何构建一个简单的使用Boost C++库的Android可执行程序.
这个例子很好的说明了工作流程和内部原理. 但是从实用角度出发,
我们需要了解如何构建一个可以提交Google Play商店的完整Android应用程序.
本篇将举例说明.
官方支持的标准实现方式是通过 Android Studio 来创建这样的应用程序.
不幸的是, Android Studio对native应用程序的支持不像Java应用程序那么好. NDK的支持目前非常有限.
因此开发者使用Android Studio用于构建应用开发的Gradle脚本, 唯一支持的NDK应用是只包含一个模块(最终共享库文件),
从位于jni文件夹的源码进行构建, 没有任何依赖, 没有子模块(例如一组静态库和共享库等),等等.
没法做定制(除了一些非常有限的设置选项).
build.gradle
defaultConfig {
...
ndk {
moduleName "my-module-name"
cFlags "-std=c++11 -fexceptions"
ldLibs "log"
stl "gnustl_shared"
abiFilter "armeabi-v7a"
}
}
}
NDK build只能设置moduleName, cFlags, ldLibs, stl 和 abiFilter;
在这里我们不能指定额外的依赖关系(如Boost库).
我们也不能指定一些路径供链接器去搜索库文件的所在位置, 以及许多其他的设置.
这是因为Gradle插件(Android Studio用来编译工程)忽略jni文件夹的Application.mk 和 Android.mk文件.
相反它会在运行时生成它自己的Android.mk,使用构建脚本中的设置.
实际上, 在Android Studio中构建功能齐全的NDK应用程序的唯一方式是完全禁用它有限的NDK支持,
手动调用$NDK/ndk-build命令. 这里我们将描述逐步怎么做.
我们将从零开始使用Android Studio创建一个简单的Android应用程序, 然后添加native部分.
我们假设你已经安装了Android Studio和设置好了Android SDK; 我们还假设你已经下载并解压
CrystaX NDK 到您的计算机上某处.
Java部分
首先, 打开Android Studio, 新建一个Android工程:
选择"Android 4.0.3" target:
选择blank 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
添加以下代码到MainActivity.onCreate():
TextView field = (TextView)findViewById(R.id.text);
field.setText(getGPSCoordinates(getFilesDir().getAbsolutePath()));
添加native方法的声明到MainActivity类:
private native String getGPSCoordinates(String rootPath);
同样别忘记添加加载native库文件到static初始化部分:
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部分完成了; 接下来看native部分.
Native部分
创建native代码存放的文件夹路径:
在下一个窗口使用默认的'main' source set, 点击"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();
}
}
编译脚本
现在我们需要修改编译脚本, 以便可以像Java部分一样编译native部分.
首先打开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
}
以下是我们修改过的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') 并运行在设备上. 这里是一张运行时的截图: