How to use JNI
1 JNI
JNI (Java Native Interface),提供了若干 API 实现了 Java 和其他语言 (主要是 C/C++) 的通信。简单来说就是 Java 想要调用 C/C++ 的接口必须遵循 JNI 的规范
牵涉到一个很重要的头文件jni.h,这个头文件的位置:\jdk1.8.0_211\include\jni.h,如果是 Linux,或许是/usr/lib/jvm/java-8-openjdk-amd64/include/jni.h。这个头文件定义了数据类型,JNIInterface (定义了很多函数指针,就是一些接口)
那么为什么要有 JNI 这个东西,Java 虽说是与平台无关的,但是支撑它的虚拟机不是啊,那玩意还得用 native 语言来完成,这不就是一个理由了
1.1 hello world
- 创建一个
JniHello.java, 并声明一个 native 函数sayHello()
1 | public class JniHello { |
- 生产 C/C++ 头文件
1 | # 先编译 生成class文件 |

- 编写对应的 C/C++ 函数,实现
JniHello.h中声明的方法
1 | // JniHello.cpp 这里c和cpp文件是有区别的 |
- 生成目标文件
1 | # 一定要带上这两个目录 且看好是/而不是\,毕竟win容易出这个错 |

- 生成 C/C++ 共享库
1 | # 库文件名格式(name可以随便改): <name> + .dll |
总之执行完后,目录下会多出一个JniHello.dll文件
- Java 文件中加载共享库并调用函数
1 | public class JniHello { |
- 编译运行 Java 文件

以上就是在 Windows 下,纯手撸 java 调 c 的过程了,前提是要配置好 gcc 或其他 C/C++ 编译器
1.2 JNI 原理
计算机系统中,每种语言都有一个执行环境 (Runtime) 用于解释执行语言中的语句,不同种的语言一般是不能存在同一种环境的。人鬼殊途
1.2.1 JavaVM
Java 执行环境是 JVM,其实是主机环境中的一个进程,每个 JVM 在本地环境都有一个 JavaVM 结构体,该结构体在创建 JVM 时返回
1 | JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); |
JavaVM 是 JVM 在 JNI 层的代表,JNI 全局仅仅有一个 JavaVM 结构体,其中封装了一些函数指针 (函数表结构),JavaVM 中封装的这些函数指针主要是针对 JVM 操作接口。另外,在 C 和 CPP 中 JavaVM 中定义有所不同
1 | /* |
在 CPP 中对 JavaVM 进行了一次封装,少一个参数,所以推荐使用 CPP
1.2.2 JNIEnv
在jni.h中还声明一样东西
1 | /* |
JNIEnv 是当前 Java 线程执行环境,一个 JVM 对应一个 JavaVM 结构体,而一个 JVM 可以创建多个 Java 线程,每一个 Java 线程对应一个 JNIEnv 结构,他们保存在线程本地存储 TLS 中。因此,不同线程的 JNIEnv 是不同的,不能共用。JNIEnv 也是一个函数表,在本地代码中通过 JNIEnv 的函数表来操作 Java 数据或调用 Java 方法。
从这里也可看出来,c 和 cpp 在使用 env 时的区别了
2 Android JNI
Android 中使用 JNI 需要 NDK (Native Develop Kit),先体验一把
2.1 Java call C/CPP
2.1.1 传统方式
创建
HelloWorld.java并点击一下 Build1
2
3
4
5
6
7
8package io.github.sidneygod.jni_1;
public class HelloWorld {
static {
System.loadLibrary("HelloWorld");
}
public static native String sayHello(String str);
}生成 c/cpp 头文件
可以直接找到对应的
HelloWorld.class文件,然后使用javah -jni HelloWorld也可以这样,在 IDE 的 Terminal 中,进入
app/src/main/java/,输入对应的指令javah -jni io.github.sidneygod.jni_1.HelloWorld
- 在 main 下创建 jni 目录,编写对应的 c/cpp 文件
这里不在赘述 c/cpp 文件,记得把刚刚的头文件也给拷过来,比较烦人的是,不能用 cout.. 所以改成了 ndk 的 log

- 编写
Android.mk文件
在 jni 目录创建
Android.mk文件1
2
3
4
5
6
7LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := HelloWorld
LOCAL_SRC_FILES := io_github_sidneygod_jni_1_HelloWorld.cpp
# 为了能打印log
LOCAL_LDLIBS := -lm -llog
include $(BUILD_SHARED_LIBRARY)- 修改 app 下的
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24apply plugin: 'com.android.application'
android {
...
defaultConfig {
...
ndk {
// 这个moduleName和mk文件的LOCAL_MODULE一致
moduleName "HelloWorld"
// 打印日志需要的
ldLibs "log", "z", "m"
// 指定生成对应版本的库文件 不加这句话默认生成全部
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86"
}
}
buildTypes {
...
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
}改完了需要 sync 一下
- 编译运行

和预想中的一毛一样.. 生成的库文件就在
build/intermediates/ndkBuild/debug/obj/local/x86/libHelloWorld.so2.1.2 CMake 工具
直接新建一个 Native C++ 项目

然后会自动生成一个 demo,直接运行就可以了..

可以看到多了个
CMakeLists.txt, 看一下build.gradle文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}出现了两次
externalNativeBuild第一次: 填写 CMake 参数, 详情参考 CMake|Android NDK
第二次: 指明
CMakeLists.txt路径看看这个
CMakeLists.txt到底是什么鬼1
2
3
4
5
6
7
8
9
10
11
12
13
14cmake_minimum_required(VERSION 3.4.1)
...
add_library(
native-lib
SHARED
native-lib.cpp )
...
find_library(
log-lib
log )
...
target_link_libraries(
native-lib
${log-lib} )
cmake_minimum_required(): 指定 CMake 最小版本add_library: 创建一个静态或者动态库native-lib: 是库的名字SHARED: 是库的类别 动态还是静态native-lib.cpp: 库原文件路径
find_library(): 找到一个预编译的库, 将之作为变量存起来log-lib: 设置路径变量名称log: 制定 NDK 库的名字
target_link_libraries(): 指定 CMake 链接到的目标库native-lib: 指定的目标库${log-lib}: 将目标库链接到 NDK 中的日志库
注意: 与 Instant Run 不兼容
2.2 C/Cpp call Java
2.2.1 函数签名
首先看一眼jni.h中对 JNINativeMethod 的定义
1 | typedef struct { |
函数名, 函数指针都好理解, 多出来一个函数签名, 这是因为 Java 允许方法重载, 光靠名字是分别不出来是哪个方法的
查看一个类中函数的签名
1 | # 一定要写对class文件的目录 |
然后就可以看到一坨..
1 | Compiled from "JniHello.java" |
第一个()V:默认构造函数的签名
第二个(Ljava/lang/String;)Ljava/lang/String;:是那个 native 函数的签名
第三个([Ljava/lang/String;)V:是 main 函数的签名
第四个()V:是哪个静态代码块的
JNI 规范定义的函数签名信息 (注意这个 “;” 不加是识别不出来的..)
1 | (参数1类型标识;参数2类型标识;...;)返回值类型标识; |
- 当类型是引用类型时: L+ 包名 + 类名,
Ljava/lang/String - 当类型是基本类型时: 除了 boolean 是 Z, long 是 J, 其他全是基本类型首字母大写, V 就是 void
- 数组
- 一般数组: [签名
int[]: [iString[]: [LJava/lang/Object
2.2.2 实例
把那个啥上面传统方式的改一下
先在HelloWorld.java中加两个函数
1 | public class HelloWorld { |
再去完善 cpp 文件新增的函数 nHello
1 | JNIEXPORT void JNICALL Java_io_github_sidneygod_jni_11_HelloWorld_nHello |
其他基本上没什么变化,在 click2 中调用nHello(),就会触发调用 HelloWorld 的hello()函数
