Android NDK开发从0到1

本文记录了 NDK 早期配置方式,偏繁琐。现在应该使用 CMake 的方式进行构建,会更加快捷。本文没有讲述 CMake 的配置方式,因此还请读者自行搜索。

Android NDK 开发从0到1

本文的开发环境为 Windows,其他平台操作类似

其实说到 NDK 就不得不提 JNI ( Java Native Interface ) ,JNI 是专门用来与本地代码进行交互而提供的一个接口。通过 JNI 就可以调用 C/C++ 所编写的本地代码。

而 NDK ( Native Development Kit) 是 Android 所提供的一个工具集合,通过 NDK 就可以很方便的在 Android 中通过 JNI 来访问本地代码。

那么使用 NDK 有什么好处呢?简单总结几点如下,更多的欢迎补充

  • 可以使用 C/C++ 的开源库 ( 比如 OpenCV 等)
  • 安全性。因为 so 库很难进行反编译,所以在安全性上有一定的保证
  • 可移植。通过编译生成的 so 库可以在其他平台上使用

那么废话不多说,下面开始吧~

开发流程

首先先大致明确NDK开发从0到1的几个步骤:

  • 环境变量配置
  • 在 Java 类中编写 native 代码
  • 生成与 native 方法对应的头文件
  • 利用生成的头文件,编写对应的 C/C++ 代码
  • 生成 so 库
  • 使用 so 库

嗯,大致的步骤就是这几个,但是具体实现起来时有很多坑,那么接下来就按照这几个步骤开始讲解

环境变量配置

首先就是下载 NDK,那么这里有几个方法

ndk下载

在 AndroidDevTools 中下载

当NDK下载完过后,经过解压等一些列操作,就可以配置环境了

新建系统变量,将变量名( NDK_HOME )和变量值 (自己NDK文件路径)如下输入

因为我是用的第二种方式,所以 NDK 直接下在了 android-sdk 中,具体情况请根据自己的文件位置而定

设置NDK_HOME

然后添加到 path 中 %NDK_HOME%\ ,如果是添加在最后要记得在前面加个分号

将NDK_HOME添加进path

配置完成过后,在命令窗口输入 ndk-build

![ndk-build][ndk-build]

若能出现如上信息,则说明配置成功

最后就是对Android Studio 进行环境配置,路径根据自己的情况而定

as的ndk环境配置

在 Java 类中编写 native 代码

新建 Android 项目,现在先建一个 JniUtil 类,专门用来存放 native 方法

1
2
3
4
5
public class JniUtil {

public native int add(int a, int b);

}

这里将利用 NDK 计算两数之和

生成与 native 方法对应的头文件

在 Android NDK 开发中,C/C++ 中对应于 Java 方法的函数名应该叫什么是很有讲究的,大致是形式是

Java_包名_类名_方法名

所以 C/C++ 中的函数名不能随便取,必须按照规则来。因为这个函数名很繁琐,手动书写十分容易出错,所以这里需要利用 javah 的命令来生成对应于函数的头文件在头文件中会有对应的 C/C++ 函数名,所以直接复制函数名就可以编写自己的逻辑了

那么接下来就利用 javah 生成自己的头文件,命令格式如下

javah -classpath (搜索类目录) -d (输出目录) (类名)

这里需要强调一下的是这里的搜索类目录,搜索的是这个 Java 类的 .class 文件 ,所以在在执行生成头文件的命令时,需要使用快捷键 ctrl+F9 或者 Android Studio 顶部菜单栏 build -> Make Project,手动生成这个类的 class 文件

生成完 class 文件过后,就可以利用头文件生成对应的头文件了,命令如下:

javah -classpath app\build\intermediates\classes\debug -d ./app/src/main/jni com.innofang.ndkdemo.JniUtil

这里只需要将上面括号类的内容按照自己项目的情况进行修改就可以了。但是,这么长的命令,难道每次添加 native 方法都要自己手动输入生成头文件吗?

在这里可以利用一个小技巧,就是利用 Android Studio 的 External Tools 工具 ,一件生成头文件

File -> Settings 或者 Ctrl+Alt+S 打开设置界面,接着点击 Tools ,找到 External Tool

找到external_tools

点击 +新建一个 Exteranl Tools

输入如下信息

general header

在 Name 区域输入 Generate Header File

Program : $JDKPath$\bin\javah

Parameters : -classpath $OutputPath$ -d ./app/src/main/jni $FileClass$

Working directory : $ProjectFileDir$

其余的勾选项对照上图即可

然后点击 OK 就完成了

使用时,只需要右击java文件,找到 External Tool ,点击 Generate Header File 即可

找到 Generate Header File

切换到 Project 视图,就可以看到在 main 包下多了一个 jni 的文件,里面就是刚刚生成的头文件

生成的jni文件

Q: 出现类似 错误: 找不到 'com.innofang.ndkdemo.jnituil' 的类文件。 的情况

A:在生成头文件之前要先生成类文件,使用快捷键 ctrl+F9 或者 Android Studio 顶部菜单栏 build -> Make Project

利用生成的头文件,编写对应的 C/C++ 代码

点击进入头文件,查看头文件内部的内容

自动生成的头文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_innofang_ndkdemo_JniUtil */

#ifndef _Included_com_innofang_ndkdemo_JniUtil
#define _Included_com_innofang_ndkdemo_JniUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_innofang_ndkdemo_JniUtil
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_innofang_ndkdemo_JniUtil_add
(JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

可以看到,在头文件内部已经写好了对应函数的声明,这里有一点需要强调一下

1
2
3
#ifdef __cplusplus
extern "C" {
#endif

这里的宏定义指定了 extern "C" 内部的函数将采用 C 语言的命名风格来编译。如果你想使用 C++ 那么这里最好改成 extern "C++" { 。否则当采用 C++ 时,可能会因为 C 和 C++ 比编译过程中对函数的命名风格不同,导致编译错误。

接下来就是编写 C 代码 就将整个声明复制下来,在 jni 目录下,新建一个 C 文件并取名为取名为 jnituil.c ,这个名字可以随意,可以不和头文件名相同

建议根据头文件中规定的是采用 C 还是 C++ 来决定新建什么类型的文件( .c 或 .cpp ),虽然不会报错,但是牵扯到语言风格,规范一点总没错

1
2
3
4
5
6
7

#include "com_innofang_ndkdemo_JniUtil.h"

JNIEXPORT jint JNICALL Java_com_innofang_ndkdemo_JniUtil_add
(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}

因为函数功能是两数相加求和,所以这里直接返回两数之和即可,这一步还是比较简单的

简单介绍一下上面出现的几个陌生的东西,JNIEXPORT, JNICALL, JNIEnv 和 jobject,这些都是 JNI 标准中所定义的类型或者宏,含义如下:

  • JNIEnv * : 表示一个指向 JNI 环境的指针,可以通过它来访问 JNI 提供的接口方法
  • jobject : 表示 Java 对象中的 this
  • JNIEXPORT 和 JNICALL : 都是 JNI 中所定义的宏,可以在 jni.h 这个头文件中查找到 ( 上面的代码中虽然没有包含这个头文件,但是如果你细心的话会发现在之前生成的头文件中已经包含这个头文件了,所以这里就只包含自己生成的头文件。当然还需要哪些头文件需要根据具体情况而定)

这里涉及到 JNI 的知识,暂时不是本文章范围内,以后有时间再详细介绍

生成 so 库

这一步很关键,操作也比较繁琐,简单罗列一下操作步骤

  • 配置 Gradle
  • 编写 Android.mk 文件
  • 编写 Application.mk 文件

配置 Gradle

打开 build.gradle 文件 (记住这是在 Module:app 下的)

  1. defaultConfig 标签下,加入如下内容

    1
    2
    3
    ndk {
    moduleName "app"
    }

    这个 app 就是等会生成的 so 库的名字,也就是等会要使用的 so 库的名字

  2. 然后再在 android 标签下,添加如下内容

    1
    2
    3
    sourceSets.main {
    jni.srcDirs = ['libs']
    }

    表示生成的 so 库会在 src/main/libs 目录下

这两个步骤缺一不可

为了便于对照查看,下面贴出这部分 Gradle 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.innofang.ndkdemo"
minSdkVersion 21
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

ndk{
moduleName "app" // 这个 app 就是等会生成的 so 库的名字
}

}
buildTypes {
release {
...
}
}

sourceSets.main {
jni.srcDirs = ['libs'] // 表示生成的 so 库会在 src/main/libs 目录下
}

}

dependencies {
...
}

编写 Android.mk 文件

**在 jni 文件下 **,新建一个文件取名为 Android.mk

编辑内容如下

1
2
3
4
5
6
7
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := app
LOCAL_SRC_FILES := jniutil.c

include $(BUILD_SHARED_LIBRARY)

这里要关注的是 LOCAL_MODULELOCAL_SRC_FILES 这两个东西

  • LOCAL_MODULE 表示模块的名称。后面的 app 就是上一步骤配置的 moduleName
  • LOCAL_SRC_FILES 表示需要参与编译的原文件。后面的文件名就是刚才编写的 C/C++ 代码的文件

请根据具体情况来更改,这两项必须对应正确

编写 Application.mk 文件

同样 **在 jni 文件下 **,新建一个文件取名为 Application.mk

编辑内容如下

1
APP_ABI := all
  • APP_ABI : 表示 CPU 的架构平台的类型。all 表示编译所有 CPU 平台的 so 库。

目前常见的平台有 armeabi、x86 和 mips ,其中在移动设备中占据主要地位的是 armeabi ,所以有很多 apk 只包含 armeabi 类型的 so 库的原因。默认情况下 NDK 会编译产生各个 CPU 平台的 so 库,通过 APP_ABI 选项即可指定 so 库的 CPU 平台的类型。像上面写 all 表示编译所有 CPU 平台的 so 库, 若改成 armeabi 则只会编译 armeabi 平台下的 so 库,如果需要编译多个 so 库,可以用 , 分割,如:APP_ABI := armeabi, x86

请根据实际情况来修改,若编译所有 CPU 平台的 so 库,不可避免 apk 的体积也会随之变大

上面的三个步骤都仅仅只是生成 so 库的准备工作,下面才开始真正生成 so 库

找到 Android Studio 底部操作栏的 Terminal 并点击,在 Terminal 内分别输入如下命令

1
2
cd app/src/main/jni     # 将位置切换到 jni目录下
ndk-build

这里需要强调一点的是,如果目录名字不是 jni,那么 ndk-build 则无法编译成功。因为在上一步生成头文件的时候已经通过命令行自动生成了一个 jni 文件夹并在里面生成了头文件,所以这一步没有强调让创建 jni 文件夹。

如果终端信息出现如下内容则说明 so 库生成成功,否则请检查上面的步骤是否正确操作

so库生成成功

经过上面一系列步骤,你会发现 main 目录下多了两个文件夹 libs 和 obj,这两个文件下存的都是新生成的 so 库。至此,生成 so 库这个任务就算完成了。

但是发现每次生成 so 库时都要手动切换路径然后生成文件,虽然说没有之前生成头文件那么复杂,但是这种重复的操作还是有点麻烦,那么为了简化这个步骤,下面就要利用到上面提到过的 External Tool 了

还是打开设置界面,找到 Tools 这个目录,找到 External Tool,点击 + 新建一个 External Tool

general so

在 Name : 区域输入 Generate SO

Program : F:\android-sdk\ndk-bundle\ndk-build.cmd ( 这里请改成自己的 NDK 目录下的 ndk-build.cmd 的位置 )

Parameters : -C ./app/src/main/jni ( -C 后面接 project 路径 )

Working directory : $ProjectFileDir$

最后点击 OK 就完成了。使用时,右击 jni 文件 找到并点击 External Tool 下的 Generate SO 即可

找到 general so

结果跟手动生成的一样

使用 so 库

上面进行了很多操作,都是为了使用生成的 so 库

要使用 so 库,首先就是先修改 JniUtil.java 文件,添加一个静态块

1
2
3
4
5
6
7
8
9
public class JniUtil {

static {
System.loadLibrary("app");
}

public native int add (int a, int b);

}

然后在 main 目录下**新建一个文件夹,取名为 jniLibs **

将刚才生成的 libs 目录下的所有文件拷贝到这个 jniLibs 目录下!!!
将刚才生成的 libs 目录下的所有文件拷贝到这个 jniLibs 目录下!!!
将刚才生成的 libs 目录下的所有文件拷贝到这个 jniLibs 目录下!!!

重要的事情说三遍,如果缺失这一步骤将会出现找不到 so 库的错误

确定经过上面这步后,就可以正式使用 so 库了

回到我们熟悉的 MainActivity 中,为了使用 JniUtil 中的 add 方法,首先在布局文件中添加两个 EditText 和一个 Button,然后在 MainActivity 中编写如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* 含有本地方法的Java类 */
final JniUtil jniUtil = new JniUtil();
/* 控件初始化 */
final EditText numberA = (EditText) findViewById(R.id.number_a);
final EditText numberB = (EditText) findViewById(R.id.number_b);
/* Button 点击事件 */
findViewById(R.id.result).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String a = numberA.getText().toString();
String b = numberB.getText().toString();
/* 判空 */
if (!TextUtils.isEmpty(a) && !TextUtils.isEmpty(b)) {
/* 使用本地方法 */
int result = jniUtil.add(Integer.parseInt(a), Integer.parseInt(b));
Toast.makeText(MainActivity.this,
"" + result,
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this,
"cannot be empty",
Toast.LENGTH_SHORT).show();
}
}
});
}

在这里,获取两个 EditText 中的数字,然后调用 JniUtil 中的 add 方法,将返回数据 Toast 显示出来

成果

到此为止,Android NDK开发的第一步已经成功迈出!!!

总结

上面知识添加了一个 native 方法,如果根据又需要要添加多个 native 方法 ,那么步骤基本相似,这里将步骤重温一遍当作总结

  • 初次使用 NDK 首先配置环境
  • 在 Java 类中添加 native 本地方法
    • 若初次编写就新建一个 Java 类,若有则完全可以在同一个 Java 类中继续添加 native 方法
  • 生成对应的头文件
    • 手动输入 javah 命令或使用 External Tool 工具
  • 利用生成的头文件编写 C/C++ 代码
    • 在jni目录下新建C/CPP文件,复制头文件中的函数名进行编写以防出错。记住要包含对应的头文件 #include "*.h"
  • 生成 so 库
    • 保证配置好了 Gradle
    • 这里必须有对应的 Android.mk 和 Application.mk ,否则无法成功
  • 将生成的 so 库全部复制到 jniLibs 目录下( 若有文件就覆盖掉原来 )
    • 使用前保证已经添加了静态代码块,其中的 System.loadLibrary("");中的字符串是之前定义的 moduleName 也是 so 库的名字(去掉前面的 lib 和 后面的 .so ),这里务必对应正确
  • 使用 native 方法

本文源码见此

Author: Inno Fang
Link: http://innofang.github.io/2017/04/16/Android-NDK%E5%BC%80%E5%8F%91%E4%BB%8E0%E5%88%B01/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-ND 4.0 unless stating additionally.