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.so
2.1.2 CMake工具
直接新建一个Native C++项目
然后会自动生成一个demo,直接运行就可以了..
可以看到多了个CMakeLists.txt
, 看一下build.gradle
文件
1 | apply plugin: 'com.android.application' |
出现了两次externalNativeBuild
第一次: 填写CMake参数, 详情参考CMake|Android NDK
第二次: 指明CMakeLists.txt
路径
看看这个CMakeLists.txt
到底是什么鬼
1 | cmake_minimum_required(VERSION 3.4.1) |
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()
函数