使用Android Studio构建基于NDK和Boost C++库的应用程序
2015.01.29 11:40

上一篇, 我们介绍了如何构建一个简单的使用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, stlabiFilter; 在这里我们不能指定额外的依赖关系(如Boost库). 我们也不能指定一些路径供链接器去搜索库文件的所在位置, 以及许多其他的设置.

这是因为Gradle插件(Android Studio用来编译工程)忽略jni文件夹的Application.mkAndroid.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

Java部分完成了; 接下来看native部分.

Native部分

创建native代码存放的文件夹路径:

在下一个窗口使用默认的'main' source set, 点击"Finish"按钮:

源码

在刚刚创建的文件夹中创建以下文件(app/src/main/jni):

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

编译脚本

现在我们需要修改编译脚本, 以便可以像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

以下是我们修改过的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

Our contributors: