本章将讲述Java与Native之间如何实现相互调用。我将围绕围绕如下三点来讲解。
一.native函数注册
当Java代码中执行Native的代码的时候,首先是通过一定的方法来找到这些native方法。这种方式就是native函数的注册,而注册native函数的具体方法不同,会导致系统在运行时采用不同的方式来寻找这些native方法。
native函数注册的注册一共有俩种方式,一种是动态注册,一种是静态注册,下面就依次来介绍一下静态注册和动态注册。
(1)静态注册
什么是静态注册?
先由Java得到本地方法的声明,然后再通过JNI实现该声明方法。
如何实现静态注册?
实现静态注册很简单,只需要根据函数名来遍历Java和JNI函数之间的关联,并且要求JNI层函数的名字必须遵循特定的格式。具体的实现很简单,首先在Java代码中声明native函数,然后通过javac或者javah相关命令来生成native函数的对应得头文件,然后在c/c++文件中引用这些头文件,最后在JNI代码中实现这些函数的具体业务逻辑即可。
好了,下面我们就根据以上原理来实现一个简单的静态注册。下面简单的看一个列子:
1. 在本地Java代码声明native函数
package com.bnd.multimedialearning.jni;
public class NDKTools {
//声明native函数
public static native String getStringFromNDK();
static {
System.loadLibrary("native-lib");
}
}
2. 通过javac/javah来生成native函数的对应得头文件
生成头文件的方式很简单,就是利用jdk自带的命令就可以生成,可以通过javah也可以通过javac,先看通过javah命令,再看javac。
- javah 生成头文件
首先进入java文件无目录下,我这里包名路径是com.bnd.multimedialearning.jni.NDKTools(请自行换成自己包名)
javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.bnd.multimedialearning.jni.NDKTools
- javac生成头文件
首先进入java文件无目录下,我这里包名路径是com.bnd.multimedialearning.jni.NDKTools(请自行换成自己包名)
javac -encoding utf8 -h . NDKTools.java
通过这俩个命令我们就可以获得相应的头文件,以及clsss文件,如下所示:
生成的.h文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_bnd_multimedialearning_jni_NDKTools */
#ifndef _Included_com_bnd_multimedialearning_jni_NDKTools
#define _Included_com_bnd_multimedialearning_jni_NDKTools
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_bnd_multimedialearning_jni_NDKTools
* Method: getStringFromNDK
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_bnd_multimedialearning_jni_NDKTools_getStringFromNDK
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
生成的.class文件:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.bnd.multimedialearning.jni;
public class NDKTools {
public NDKTools() {
}
public static native String getStringFromNDK();
static {
System.loadLibrary("native-lib");
}
}
我们重点来看一下.h文件。分析自后你会发现JNI方法名的规范就出来了。
jni的命令规范如下:
JNIEXPORT jstring JNICALL Java_com_bnd_multimedialearning_jni_NDKTools_getStringFromNDK
(JNIEnv *, jclass)
返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数
注意事项:
- 注意分隔符:
Java前缀与类名以及类名之间的包名和方法名之间使用"_"进行分割; - 注意静态:
如果在Java中声明的方法是"静态的",则native方法也是static。否则不是 - 如果你的JNI的native方法不是通过静态注册方式来实现的,则不需要符合上面的这些规范,可以格局自己习惯随意命名
3.在JNI代码中实现这些函数
在编写jni代码之前,我们需要将第二步中生成的头文件引入使用,然后在编写jni代码。
//引入头文件
#include "com_bnd_multimedialearning_jni_NDKTools.h"
#include <jni.h>
JNIEXPORT jstring JNICALL
//native具体实现函数
Java_com_bnd_multimedialearning_jni_NDKTools_getStringFromNDK(JNIEnv *env, jclass clazz) {
return (*env)->NewStringUTF(env,"Hello from C++,这是老张用传统方式实现jni的调用!");
}
注意:
先根据函数名找到对应的JNI函数。Java层在调用某个函数时,会从对应的JNI中寻找该函数,如果没有就会报错,如果存在就会建立一个关联关系,以后再调用时会直接使用这个函数,这部分的操作由虚拟机完成。
(2)动态注册
既然有了静态注册,为什么又要动态注册了,下面我们就来讲一下如何实现动态注册。
通过上面的介绍,我们知道,静态注册native方法的过程,就是Java层声明的nativ方法和JNI函数一一对应。这种关系就像是一一匹配映射的关系,但是也存在缺点,那就是只能一一对应,而且如果路径稍微错一个地方就会报错。那么有没有更好的方式让Java层的native方法和任意JNI函数连接起来。答案肯定是有的,那就是动态注册。也就是通过RegisterNatives
方法把C/C++中的方法自动映射到Java中的native方法,而无需遵循特定的方法命名格式。这样就避免了写错和手动一一对应的问题。
当我们使用System.loadLibarary()
方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad
函数兵调用该函数,这个函数的作用是告诉Dalvik
虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad
()函数,VM会默认该库使用最老的JNI 1.1版本。由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad
()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android中的Activity中的onCreate
()方法。该函数前面也有三个关键字分别是JNIEXPORT
,JNICALL
,jint
。其中JNIEXPORT
和JNICALL
是两个宏定义,用于指定该函数时JNI函数。jint
是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层。JNI_OnLoad函数调用案列如下:
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_FALSE;
}
jint size = sizeof(getMethods) / sizeof(JNINativeMethod);
registerNatives(env, JAVA_CLASS, getMethods, size);
LOGD("Methods: %d", size);
//指定jni的版本
return JNI_VERSION_1_6;
}
该函数会有两个参数,其中*vm为Java虚拟机实例,查看jni.h文件,你会发现JavaVM结构体定义了如下函数。
struct _JavaVM {
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
这些函数的返回值都是jint。
下面,我们举列子说明,如何动态注册。
1. 加载.so库文件
package com.bnd.multimedialearning.jni;
public class SampleJni {
public static native void printHello(long object);
static {
System.loadLibrary("SampleJni-lib");
}
}
- 在jni中实现
JNI_OnLoad
方法
jint JNI_OnLoad(JavaVM* vm, void* reserved)
本列子具体实现如下:
jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGD("JNI", "enter jni_onload");
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
//获取所有的函数的个数
jint size = sizeof(getMethods) / sizeof(JNINativeMethod);
LOGD("JNI", "jint size is '%s'\n", size);
jniRegisterNativeMethods(env, className, getMethods, size);
return JNI_VERSION_1_4;
}
- 通过
RegisterNatives
函数动态的注册native方法
动态注册是通过RegisterNatives
实现的,查看源码会发现如下所示:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
下面,我们就剖析一下RegisterNatives
这个函数。
参数说明:
名称 | 说明 |
---|---|
clazz | 对应得Java类,包括详细的包结构,这里包结构以前用的’.‘现在要换成英文的’/’ |
methods | 所在java类的函数名称 |
nMethods | native函数的个数 |
在第二步《 在jni中实现JNI_OnLoad
方法》中,我们有一个这样方法–jniRegisterNativeMethods
,其实这个方法就是我们自定义的注册方法,我们看看jniRegisterNativeMethods
方法内部是如何调用RegisterNatives
这个函数的。
//注册NativeMethods
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGD("JNI","Registering %s natives\n", className);
clazz = (env)->FindClass( className);
if (clazz == NULL) {
LOGE("JNI","Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, getMethods, numMethods) < 0) {
LOGE("JNI","RegisterNatives failed for '%s'\n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
这里你会发现,在第二步《 在jni中实现JNI_OnLoad
方法》中,我们通过 jint size = sizeof(getMethods) / sizeof(JNINativeMethod);
方法获得函数的个数,而java类是我们全局配置好了的,如下所示,他是包括了完整的包路径的:
static const char *className = "com/bnd/multimedialearning/jni/SampleJni";
在得到className后,我们 通过通用 clazz = (env)->FindClass( className);
就能获取clazz 参数,到此,动态注册的三个参数就获取成功了。下面我们就看一下完整的动态注册代码:
#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
#define LOG_TAG "NATIVE_LOG"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
static const char *className = "com/bnd/multimedialearning/jni/SampleJni";
//native对应得函数
static void printHello(JNIEnv *env, jobject, jlong handle) {
LOGD("JNI", "native function is print to hello!");
}
//获取所有的native函数数组
static JNINativeMethod getMethods[] = {
{"printHello", "(J)V", (void*)printHello},
};
//注册NativeMethods
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGD("JNI","Registering %s natives\n", className);
clazz = (env)->FindClass( className);
if (clazz == NULL) {
LOGE("JNI","Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, getMethods, numMethods) < 0) {
LOGE("JNI","RegisterNatives failed for '%s'\n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
//JNI_OnLoad动态注册:动态注册通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法
//注意:
//当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad函数兵调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。
// 由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android中的Activity中的onCreate()方法。该函数前面也有三个关键字分别是JNIEXPORT,JNICALL,jint。
// 其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,jint就是用于衔接Java层和JNI层。
jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGD("JNI", "enter jni_onload");
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
//获取所有的函数的个数
jint size = sizeof(getMethods) / sizeof(JNINativeMethod);
LOGD("JNI", "jint size is '%s'\n", size);
jniRegisterNativeMethods(env, className, getMethods, size);
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
好了,这样我们就实现了一个动态注册,结合上面的完整代码,我们来做一个简单流程分析。
首先看第一步中JNI_OnLoad
函数的实现。JNI_OnLoad主要就是两个代码块,一个是if语句判断,一个是jniRegisterNativeMethods函数的实现。
. if语句分析
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
这里调用了GetEnv函数是为了获取JNIEnv结构体指针,其实JNIEnv结构体指向了一个函数表,该函数表指向了对应的JNI函数,我们通过这些JNI函数实现JNI编程。
. jniRegisterNativeMethods函数分析
jniRegisterNativeMethods
函数是自己定义的函数,内部主要是调用了RegisterNatives
实现动态注册。这里面注意一个静态变量getMethods
,这个静态变量是一个JNINativeMethod
类型的数组。它代表的是一个native方法的数组,如果你在一个Java类中有一个native方法,这里它的size就是1,如果是两个native方法,它的size就是2…,以此类推,我这里定义的getMethods变量的实现如下所示:
//获取所有的native函数数组
static JNINativeMethod getMethods[] = {
{"printHello", "(J)V", (void*)printHello},
};
细心点你会发现,他是JNINativeMethod结构体,关于JNINativeMethod 结构体,这里我不在详细介绍,在上一篇《Android JNI(三)——JNI数据结构之JNINativeMethod》里我详细介绍了有关这个结构体相关的知识,不懂的可以前往查看。然后我们接着分析动态注册的实现。
首先通过clazz = (env)->FindClass( className)
;找到声明native方法的类。然后通过调用RegisterNatives
函数将注册函数的Java类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定。
上面提到JNINativeMethod
结构体的时候,我们看到一个参数signature
。什么是signature
,signature
就是签名,我们下面就来讲一下签名。
二.jni的签名signature
(1)为什么要使用签名?
在上一篇《Android JNI(三)——JNI数据结构之JNINativeMethod》一篇中,我们知道变量signature,用字符串是描述了Java中函数的参数和返回值,这个参数最为复杂。既然这么复杂,为什么又要搞这个签名了,其实这个和Java语法有很大的关系。为了适应和匹配Java语法,所以才搞出了签名这个东西。
大家都知道,Java是支持函数重载的,这就意味着,可以定义相同方法名,不同参数的方法,然后Java根据其不同的参数,找到其对应的实现的方法。这样是很好,所以说JNI肯定要支持的,那JNI要怎么支持这种情况了,如果仅仅是根据函数名,没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——“签名”,即将参数类型和返回值类型的组合。通过这个组合关系,就实现了签名,就唯一确定了一个方法。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。
(2) 如何查看类中的方法的签名?
查看类中的方法签名也很简单。可以使用javap
命令,如下所示:
javap -s -p Test.class
注意:
这里需要将.java文件转成.class文件,然后通过javap命令方能查看签名。
下面就看一下上面列子中CMakeNDKTools这个类的所有签名,代码如下:
package com.bnd.multimedialearning.jni;
public class CMakeNDKTools {
public static native String getStringFromNDK();
static {
System.loadLibrary("CMakeJni-lib");
}
}
下面我们执行一下javap -s -p 类名.class
命令,查看结果如下:
Compiled from "CMakeNDKTools.java"
public class com.bnd.multimedialearning.jni.CMakeNDKTools {
public com.bnd.multimedialearning.jni.CMakeNDKTools();
descriptor: ()V
public static native java.lang.String getStringFromNDK();
descriptor: ()Ljava/lang/String;
static {};
descriptor: ()V
}
你会看到上面有这些稀奇古怪的东西。比如:()V
; ()Ljava/lang/String
;其实这些就是签名以后东西,下面我们就研究下签名的格式。
(3) JNI如何规范函数的签名信息
jni的签名规范或者说签名格式如下:
(参数1类型标示;参数2类型标示;参数3类型标示…)返回值类型标示。
注意:
当参数为引用类型的时候,参数类型的标示的格式为"L+包名",其中包名的.(点)要换成"/",看我上面的例子就差不多,比如String就是Ljava/lang/String;Long就是Ljava/lang/Long;…,其实细心你会发现就是该数据类型或者说该对象所在的完整包结构加类名。只是在包结构前面要加上"L",不要为我为什么要加L,难道不能是其他的,答案当然是可以,只是类型不同,下面就列出各种类型的标示
类型标示 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
以上就是基本数据类型对应得标示,其实很好记,出了boolean 对应得是Z;J对应这long,其余类型均是基本数据类型首字母大写。
如果返回值是void,对应的签名是V。
重点来说一下数组以及Array这个特殊的类型.如下表所示:
类型标示 | Java类型 |
---|---|
[签名 | 数组 |
[i | int[] |
[Ljava/lang/Object | String[] |
三.Java和native代码的相互调用
在上面我们已经知道如何从JNI中调用Java类中的方法,其实在jni.h中已经定义了一系列函数来供我们调用。下面我们就以此举例说明:
(1) 获取Class对象
为了能够在C/C++中调用Java中的类,jni.h的头文件专门定义了jclass
类型表示Java中Class类。JNIEnv中有3个函数可以获取jclass。查看jni.h文件你会发现是如下三个函数:
jclass (*FindClass)(JNIEnv*, const char*);
jclass GetObjectClass(jobject obj);
jclass GetSuperclass(jclass clazz);
先来看看Findclass:
- Findclass
jclass (*FindClass)(JNIEnv*, const char*);
FindClass是通过类的名称(类的全名,这时候包名不是用’".“点号而是用”/"来区分的)来获取jclass。比如我们获取一个String。如下所示:
jclass jcl_string=env->FindClass("java/lang/String");
- GetObjectClass
jclass GetObjectClass(jobject obj);
通过对象实例来获取jclass,相当于Java中的getClass()函数。
- GetSuperclass
jclass getSuperClass(jclass obj);
通过jclass可以获取其父类的jclass对象.
(2) 获取属性方法
在Native本地代码中访问Java层的代码,常用的就是获取Java类的属性和方法。为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldID
和jmethodID
这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性的时候,首先就要在本地代码中取得代表该Java类的属性的jfieldID
,然后才能在本地代码中进行Java属性的操作,同样,在需要调用Java类的某个方法时,也是需要取得代表该方法的jmethodID
才能进行Java方法操作。
常见的调用Java层的方法如下,都是通过JNIEnv来进行操作的,具体的方法实现如下如下所示:
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
注意:
- GetFieldID/GetMethodID:获取某个属性/某个方法
- GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法
细心一点你会发现,他们都是有4个参数的,而且每个参数都是*JNIEnv *env
,jclass clazz
,const char *name
,const char *sig
。关于JNIEnv
,前面我们已经讲过了,这里我们就不详细讲解了,JNIEnv
代表一个JNI环境接口,jclass
上面也说了代表Java层中的"类"
,name
则代表方法名或者属性名。而char *sig
代表JNI中的一个特殊字段——签名
,这几个字段在上面都详细的介绍过了,这里不在重复介绍。
(3) 如何构造对象
构造一个对象的方法也有多个,大概包括了如下几个:
jobject AllocObject(JNIEnv *env, jclass clazz);
jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);
这里也不再一一详细介绍这几个函数的参数,以及不同,具体的可以参考我的上一篇博客《Android JNI学习(四)——JNI的常用方法的API》,里面我详细的介绍了开发中各类JNI的api,其中就包括了如上四种,今天这里不用过多语言一一讲解,就讲一下最常用的NewObject
系列的使用。
NewObject
函数如下:
jobject NewObject(jclass clazz, jmethodID methodID, ...)
我们知道Java类中可能有多个构造函数,当我们要指定调用某个构造函数的时候,会调用下面这个方法:
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);
也就是把指定的构造函数传入进去即可。下面我看看NewObject
的二个主要参数。
参数名称 | 参数说明 |
---|---|
clazz | 是需要创建的Java对象的Class对象 |
methodID | 传递对应得方法ID,想一想Java对象创建的时候,需要执行什么操作?就是执行构造函数。我们传入对应构造方法即可。 |
上面的代码也是可以进一步简化一下的,只不过会多了一个参数,如下所示:
jobject NewObjectA(JNIEnv *env, jclass clazz,
jmethodID methodID, jvalue *args);
你会发现。多的一个参数就是jvalue *args
,这个参数代表的是对应构造函数的所有参数的,我们可以将传递给构造函数的所有参数放在jvalues
类型的数组args
中,该数组紧跟着放在methodID
参数的后面。NewObject()
收到数组中的这些参数后,将把它们传给对应得调用的Java方法。
上面说到,jvalue *args
参数是个数组,如果参数不是数组怎么处理,jni.h同样也提供了一个方法,如下:
jobject NewObjectV(JNIEnv *env, jclass clazz,
jmethodID methodID, va_list args);
NewObjectV
和NewObjectA
不同在于,NewObjectV
将构造函数的所有参数放到在va_list
类型的参数args
中,该参数紧跟着放在methodID
参数的后面。
四.总结
关于Java与Native之间如何实现相互调用的知识点还是很多的,设计到了动态注册和静态注册,以及jni的签名。如果你是一个新手,我建议你采用静态注册,虽然相对麻烦,但是条理清晰,只有熟悉并掌握了基本的,才能学习更复杂一点的动态注册,因为动态注册设计到了签名相关的知识,还包括了JNINativeMethod数据结构体,以及常用的jni的api,所涉及的知识点还是很多的。
如果您对jni还不是很熟,或者刚刚入门,那么我建议你先看一下我以前几篇博客,在接下来的最后一篇,我将实战讲解jni,做一个总结系统性的归纳。