NDK/JNI | 面向纯小白的 JNI 调用实现

329 阅读4分钟

注意!是面向纯纯纯小白的!

这是一篇小白笔记,是实现 JNI 的最精简的步骤(能省的步骤就尽可能省,复杂的我也不会)。

JNI(Java Native Interface),简单说就是允许运行于 JVM 的 Java 程序调用本地代码(C/C++ 甚至汇编语言的代码)。

那废话不多说了。

这里实现一个很简单的需求,点击按钮,在 Java 端调用 C 端的方法,C 端返回一个字符串给 Java 端,并显示在界面上,如下图:

Animation.gif

分以下几个步骤实现:

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 块内添加如下代码:

externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }

Application.mk 代码如下:

APP_ABI := all

很简单,就一行代码。用于指定生成哪些平台的 so 文件,这里设置成 all,会生成下图中 4 个文件夹,对应 4 个不同平台:

image.png

我之前有试着将 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']
        }
    }

不添加的话,会报如下错误,目前尚不知为何。

image.png

最后,我将生成的 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 显示出来。


参考:

blog.csdn.net/wzhseu/arti…

《Android 开发艺术探索》第 14 章


欢迎关注我的公众号呀~(MCLzone)