这是一篇小白笔记,是实现 JNI 的最精简的步骤(能省的步骤就尽可能省,复杂的我也不会)。
JNI(Java Native Interface),简单说就是允许运行于 JVM 的 Java 程序调用本地代码(C/C++ 甚至汇编语言的代码)。
那废话不多说了。
这里实现一个很简单的需求,点击按钮,在 Java 端调用 C 端的方法,C 端返回一个字符串给 Java 端,并显示在界面上,如下图:
分以下几个步骤实现:
1. 新建 Android 项目
2. 声明 native 方法
3. 实现 native 方法
4. 使用 ndk-build 命令生成 so 文件
5. Java 端调用 native 方法
怎么样?看起来是不是很清爽?
下面一步一步来。
1. 新建 Android 项目没啥好说的,就是正常步骤来就行。
2. 声明 Native 方法
public class JniTest {
static {
// 加载 jni so库, 这个 jni-test 就是最终编译产出的 so 的名字,
// 也可以起其他的名字,但必须要和最终的so库名相同。
// (最终产生的 so 文件前面会自动加上 lib)
System.loadLibrary("jni-test");
}
public native String getStrFromNative();
}
我这里是单独新建了一个类 JniTest,JniTest 类有一个名为 getStrFromNative 的方法,其返回值为 String 类型。注意到,该类内部还有一个 static 块儿,这是在调用 native 方法之前所必需的,因为你必须得把 so 文件加载进来才能调用它的方法呀!
3. 实现 native 方法
在 main 目录下新建一个 jni 文件夹,jni 文件夹下新建三个文件:Android.mk、Application.mk、test.c(最后这个文件名随意,只要跟 Android.mk 文件里对应就行)
Android.mk 代码如下:
LOCAL_PATH := $(call my-dir)
// 设置工作目录, my-dir 会返回 Android.mk 所在的目录
include $(CLEAR_VARS)
LOCAL_MODULE := jni-test
// 设置模块的名称,即编译出的 so 文件名
// 注意要和 上面类 JniTest 中加载的 jni-test 相同
LOCAL_SRC_FILES := test.c
// 指定参与编译的 C/C++ 源文件名
include $(BUILD_SHARED_LIBRARY)
第六行和上面 JniTest 类中第七行对应(都是 jni-test)
第十行和第三个文件的文件名对应(得一样)
然后在 app 的 build.gradle 文件的 android 块内添加如下代码:
第六行和上面 JniTest 类中第七行对应(都是 jni-test)
第十行和第三个文件的文件名对应(得一样)
然后在 app 的 build.gradle 文件的 android 块内添加如下代码:
Application.mk 代码如下:
APP_ABI := all
很简单,就一行代码。用于指定生成哪些平台的 so 文件,这里设置成 all,会生成下图中 4 个文件夹,对应 4 个不同平台:
我之前有试着将 all 改成 armeabi ,报了个莫名其妙的错,没能解决,目前还不知道原因,后面再找找吧。
最后是 test.c 代码:
#include<jni.h>
jstring Java_com_example_jnitest2_JniTest_getStrFromNative(JNIEnv *env,jobject thiz){
return (*env)->NewStringUTF(env,"我是来自 Native 的字符串");
}
就返回一个字符串。注意这里方法的返回值 jstring 对应 Java 里的 String,还有就是方法名字 Java_完整包名_类名_方法名。还有就是这个 env 指针,它简单讲就是指向 JNI 环境的指针,可以通过它来访问 JNI 提供的接口方法。
4. 使用 ndk-build 命令生成 so 文件
首先得在自己的电脑上安装 NDK,并配置好环境变量,这里不再赘述,下载安装配置就行。
然后,在命令行窗口 cd 到 jni 的父目录下(这里是 ......\app\src\main),然后执行 ndk-build 命令导出 so 文件。
然后会在 jni 的父目录下生成两个文件夹(libs-包含各个平台对应的 so 文件,obj-临时中间文件,obj是可删除的)
然后别忘记,到 app 的build.gradle 的 android 块中添加代码:
sourceSets{
main{
jniLibs.srcDirs=['libs']
}
}
不添加的话,会报如下错误,目前尚不知为何。
最后,我将生成的 libs 目录 改成了 jniLibs(除了直接改成 jniLibs,还可以在 libs 同级目录下新建一个 jniLibs 文件夹,将包含各个平台 so 文件的文件夹拷贝进去)。
我解释一下为什么直接将 libs 改成了 jniLibs。
如果新建一个 libs 文件夹在别的地方,再把生成的 libs 文件夹下的 so 文件拷贝过去也是可以的(就是需要改一下上面代码中第三行,改到对应路径就行),但是有一个问题就是,当我改变 native 方法后,必须重新执行 ndk-build 命令,重新导出 so 文件(这里我想改变返回的字符串),这就很麻烦,而如果我直接将执行 ndk-build 命令生成的 libs 文件夹改为 jniLibs,就不必重新导出 so 文件,我也不知道为什么。
5. Java 端调用 native 方法
这就很简单啦,就更调用普通方法一样的。
String str = new JniTest().getStrFromNative();
tv.setText(str);
我这里是将从得到的 Native 得到的字符串用一个 TextView 显示出来。