1 Java如何通过JNI调用本地(C/C++)方法?
????????主要分为4步:? ? ? ?? ????????(1)在java中利用native关键字定义native方法,表示这个方法是映射到JNI层的,调用时实际上是调用的JNI层函数。 ????????(2)根据JNI标准用C/C++语言建立与java方法对应的JNI层函数,并建立映射关系,实现java层与JNI层的相互调用。 ? ? ? ? (3)实现JNI层与本地层的相互调用逻辑,并将本地层与JNI层源文件及库编译成so库。 ????????(4)利用Java中System类的public static void loadLibrary(String libname)方法加载so库,成功加载即可实现java调用本地方法。 ? ? ? ? 下面我们通过例子详细说明下。
2 理论结合实践
????????这里通过一个简单的例子,详细说明上述步骤。
(1)在java层创建native方法
? ? ? ? 通过Android studio创建一个新工程,选择Native C++,工程会自动为你配置好NDK、CMakeLists(Android studio默认使用的本地代码编译工具是Cmake)和JNI示例,如果你选的是普通工程,也可以自己编写CMakeLists,并在app的build.gradle中手动配置NDK、CMakeLists和JNI。我们定义一个LearnJNI类,专门用于定义java native方法,并在其中定义了实例native方法doPlus()和类native方法doMinus(),代码如下:
public class LearnJNI {
......
public native int doPlus(int a,int b);
public static native int doMinus(int a, int b);
}
(2)创建本地方法与Java方法的映射
? ? ? ? 创建映射的方法有两种,分别称为?静态注册 与 动态注册。
1)静态注册
a.概念:按照JNI指定命名规则,通过方法名字建立JNI本地方法与java native方法的映射。 b.本地方法命名规则为: ? ? ? ? ? ? ? ? ? ? ??Java_包名_类名_方法名?(注意:包名单词之间也用 _ 下划线分隔) c.示例:在main\cpp\目录下创建learn_jni.cpp文件,将该cpp文件包含到CMakeLists.txt的源码变量中(这样能让该cpp文件拥有本地环境,可调用NDK的本地库),在该文件中直接定义(其实不用写头文件)与上述java native方法对应的本地方法为:
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doPlus(JNIEnv *env, jobject thiz, jint a, jint b) {
return a+b;
}
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doMinus(JNIEnv *env, jclass clazz, jint a, jint b) {
return a-b;
}
????????其中本地方法名就是按照静态注册的规则写的,上述就完成了java native方法在JNI层的静态注册。
d.说明: ????????●?extern “C” 是指定该方法用C语言的编译方式编译,而不是C++的编译方式,因为由于C++支持函数重载,用C++编译器编译出的函数名字会变为_函数名_形参1类型_形参2类型_...的方式,例如:函数void foo(int x, int y),被C编译器编译后在符号库中的名字为_foo,被C++编译器编译后在符号库中的名字为_foo_int_int,名字变了,这会导致java native找不到对应的本地方法,所以extern“C”必须加。 ? ? ? ? ●?每个JNI本地方法必须有两个形参,一个是JNIEnv* 类型形参,一个是jobject/jclass类型形参,JNIEnv前面说过用于调用JNI预定义方法,线程唯一,同一个进程可以有多个;如果java native方法是实例方法,那么第二个参数类型就是jobject,表示调用该方法的java实例,如果java native方法是类方法,那么第二个参数类型就是jclass,表示该方法的类。 ????????● 如果记不住命名规则,AS可帮助我们快捷静态注册,在JNI本地方法所在的C++文件中,输入java native名字就会有补全,选在下面的两个回车,即可生成。
或者在java native方法所在的类中,光标移动到native方法,alt+enter,选择create...,即可补全。
或者执行javah命令,先生成java native方法对应的JNI静态注册本地方法声明,然后再定义,也可以,这个方法这里不介绍了,网上比较多。 ? ? ? ? 通过AS自动生成的JNI静态注册本地方法如下:
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_doPlus(JNIEnv *env, jobject thiz, jint a, jint b) {
return a+b;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_doMinus(JNIEnv *env, jclass clazz, jint a, jint b) {
return a-b;
}
会多了JNIEXPORT及JNICALL是JNI的两个宏,上一章介绍过,前者说明该本地方法是可见的,能被java调用,可以不加,不加默认是对外可见;JNICALL只用于说明这是一个jni方法,用于区别普通本地方法的标识,可以不加。
2)动态注册
a.概念:手动建立JNI本地方法与java native方法的映射关系并注册给JNI。 b.示例:
//1 首先定义JNI本地函数,名字随便取
extern "C"
jint do_plus(JNIEnv *env, jobject thiz, jint a, jint b) {
return a+b;
}
extern "C"
jint do_minus(JNIEnv *env, jclass clazz, jint a, jint b) {
return a-b;
}
//2 构建映射结构体数组,一个元素就是一个JNI本地方法与java native方法的映射
JNINativeMethod methodMap[] = {
{"doPlus","(II)I",(void *) do_plus},
{"doMinus","(II)I",(void *) do_minus}
};
//3 定义动态注册方法,在其中调用JNI的RegisterNatives()方法
int registNativeMethod(JNIEnv *env) {
//获取java native方法所在类的class实例
jclass class_LearnJNI = env->FindClass("com/boe/jnilearn/LearnJNI");
//将映射关系注册给JNI,成功返回JNI_OK
int result = env->RegisterNatives(class_LearnJNI, methodMap,
sizeof(methodMap) / sizeof(methodMap[0]));
if(result != JNI_OK){
result = -1;
}
return result;
}
//4 定义jni.h中声明的JNI_OnLoad函数,并在该函数中调用注册逻辑。
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
int result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
if (registNativeMethod(env) == JNI_OK) {
result = JNI_VERSION_1_6;
}
return result;
}
}
c.说明: ????????●?JNINativeMethod?结构体用于存储java native方法与JNI本地方法的映射关系,其包括? 三个成员,分别为:java native方法名字;java native方法签名;JNI本地方法指针。使用时,JNI本地方法指针前需要加上(void *),是固定的,不要修改。 ????????●?RegisterNatives() 方法的参数分别为:java native方法所在类的class实例;映射关系数组;映射关系个数。返回值为注册结果,成功返回JNI_OK。 ????????● JNI_OnLoad() :调用System.loadLibrary()加载so库时,如果so库中定义了该方法,会回调它,在该函数中获取JavaVM实例,从而获取JNIEnv实例,在其中调用注册逻辑,完成动态注册。该方法返回值为so库需要的JNI版本,可选值为JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6,如果返回的JNI版本虚拟机不能识别,则无法加载so库;如果so库中不定义该方法。则so库默认使用JNI 1.1版本。
3)对比
| 静态注册 | 动态注册 | 优点 | 代码少,使用简单 AS可以自动完成静态注册 | 代码多,使用稍显复杂 只能自己手动完成 | 缺点 | JNI本地方法名字太长且固定 方法查找效率低 | JNI本地方法名字随便取 方法查找效率高 |
(3)JNI层调用本地层函数,并编译so
? ? ? ? 我们创建两个本地层方法供JNI方法调用,实现JNI层和本地层的交互,并将JNI层和本地层编译成so,供java加载。在cpp目录下创建prebuilt目录,并在其中创建include、lib、source三个目录,include用于放置本地层头文件,source用于放置本地层源码,lib用于放置本地库(暂时不用),如下图:
my_math.h内容为:
int minus();
int plus(int a,int b);
my_math.cpp内容为:
#include <my_math.h>
int minus(int a, int b){
return a - b;
}
int plus(int a,int b){
return a + b;
}
在JNI层调用本地层函数,learn_jni.cpp即为JNI层具体实现,文件内容为:
#include <jni.h>
#include <string>
//引用本地层头文件
#include <my_math.h>
//静态注册
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doPlus(JNIEnv *env, jobject thiz, jint a, jint b) {
//JNI层调用本地层函数
return minus(a,b);
}
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doMinus(JNIEnv *env, jclass clazz, jint a, jint b) {
//JNI层调用本地层函数
return plus(a,b);
}
CMakeLists.txt文件内容为:
#指定cmake版本
cmake_minimum_required(VERSION 3.4.1)
#指定头文件路径
include_directories(./prebuilt/include)
#指定源文件
file(GLOB src_list
./*.cpp
./prebuilt/source/*.cpp)
#编译成动态库,名字为 native-lib
add_library(native-lib SHARED ${src_list})
#查找log系统库绝对路径
find_library(log-lib log)
#依赖log库
target_link_libraries(native-lib ${log-lib})
编译一下工程生成apk,双击apk,会发现gradle不仅帮我们编译好了so,还帮我们将so打到了apk中,不用我们手动将so放到jniLibs下了:
编译一下工程,生成so,在如下目录:
?(4)在java中加载so并调用
? ? ? ? 在LearnJni.java中添加加载so代码:
public class LearnJNI {
static {
//加载so名字,参数为libxxx.so的名字 xxx
System.loadLibrary("native-lib");
}
public native int doPlus(int a,int b);
public static native int doMinus(int a, int b);
}
? ? ? ? ?加载so库的逻辑一般放在静态代码块中,通过System.loadLibrary()方法加载,该方法的参数为so库名字。值得注意的是,当System.loadLibrary()方法刚开始加载so时,就会立即回调so中的JNI_OnLoad()函数,可在该函数中动态注册、持久化JavaVM实例等,JNI_OnLoad函数会返回so想用的JNI版本,若JNI版本没有被java虚拟机识别,则虚拟机不会加载so。在MainActivity中调用java native方法完成java层对本地层的调用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e("yy","" + LearnJNI.doMinus(1,2));
}
}
? ? ? ? ?至此已完成java方法通过JNI调用本地函数的开发,运行下工程即可。细心的朋友会发现,整个过程中我们并没有手动为工程添加so依赖,即将so放到jniLibs中,这是因为gradle已经为我们自动将so添加到了apk中,双击apk,你就看到了:
? ? ? ? 至于为啥gradle会为我们自动添加了so依赖,是因在build.gradle中添加了externalNativeBuild:
apply plugin: 'com.android.application'
android {
......
defaultConfig {
......
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
......
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
ndkVersion '21.4.7075529'
}
dependencies {
......
}
? ? ? ? ?好了分享就到这里,如有问题请告知,希望大家点个赞支持一下!!!
|