上一篇博客讲了Java代码如何调用本地C代码,这一篇则主要讲C代码如何访问Java的变量和方法,那么我们继续从JDK源码入手,从源码中学习,学会JNI真正的使用方式和使用场景,而不是想当然的写几个简单的demo,那样是没有意义的,知道API和会用API并不是一回事,源码的JNI使用方式是高效的经得起考验的,如果我们仅仅只是看了几个demo,查了一下文档就去写JNI,那样很可能就是在给自己埋坑。
好了,进入主题,看到我们今天的主角,使用Java的zip压缩与解压,之所以从这里入手,是因为此处的代码逻辑比较简单,且正真实现压缩与解压的是本地C库,所以非常适合学习JNI,以及Java与C的大数据量交互。
打开openjdk的Java源码/jdk/src/share/classes/java/util/zip/Deflater.java
可以看到,在该类的开头就做了一个介绍,并贴心的给出了一段demo,实际上这个demo太过于简单,真实的代码中并不是这样用,我们这里就以此demo做个小测试,简单改造一下
try {
// Encode a String into bytes
String inputString = "Beautiful is better than ugly.Explicit is better than implicit.";
byte[] input = inputString.getBytes();
System.out.println("原始长度:"+input.length);
// Compress the bytes
byte[] output = new byte[100];
Deflater compresser = new Deflater();
compresser.setInput(input);
compresser.finish();
int compressedDataLength = compresser.deflate(output);
compresser.end();
System.out.println("压缩之后:"+compressedDataLength);
// Decompress the bytes
Inflater decompresser = new Inflater();
decompresser.setInput(output, 0, compressedDataLength);
byte[] result = new byte[100];
int resultLength = decompresser.inflate(result);
decompresser.end();
System.out.println("解压长度:"+resultLength);
// Decode the bytes into a String
String outputString = new String(result, 0, resultLength);
System.out.println(outputString);
} catch (Exception ex) {
ex.printStackTrace();
}
这一段程序主要做了两件事,先压缩字节,再解压恢复,这里压缩使用Deflater 类,而解压则用到Inflater 类,从名字也很好理解。大家可以运行上述代码,看看一行字符串,压缩之后减少了多少字节。
首先将上述程序分开来看,先看压缩部分,主要干事的代码如下
Deflater compresser = new Deflater();
compresser.setInput(input);
compresser.finish();
int compressedDataLength = compresser.deflate(output);
compresser.end();
源码其实不是很多,这里仅摘取相关部分
private final ZStreamRef zsRef;
private byte[] buf = new byte[0];
private int off, len;
private int level, strategy;
private boolean setParams;
private boolean finish, finished;
...
static {
/* Zip library is loaded from System.initializeSystemClass */
initIDs();
}
/**
* Sets input data for compression. This should be called whenever
* needsInput() returns true indicating that more input data is required.
* @param b the input data bytes
* @param off the start offset of the data
* @param len the length of the data
* @see Deflater#needsInput
*/
public void setInput(byte[] b, int off, int len) {
if (b== null) {
throw new NullPointerException();
}
if (off < 0 || len < 0 || off > b.length - len) {
throw new ArrayIndexOutOfBoundsException();
}
synchronized (zsRef) {
this.buf = b;
this.off = off;
this.len = len;
}
}
/**
* Sets input data for compression. This should be called whenever
* needsInput() returns true indicating that more input data is required.
* @param b the input data bytes
* @see Deflater#needsInput
*/
public void setInput(byte[] b) {
setInput(b, 0, b.length);
}
...
/**
* When called, indicates that compression should end with the current
* contents of the input buffer.
*/
public void finish() {
synchronized (zsRef) {
finish = true;
}
}
...
/**
* Compression flush mode used to achieve best compression result.
*
* @see Deflater#deflate(byte[], int, int, int)
* @since 1.7
*/
public static final int NO_FLUSH = 0;
public int deflate(byte[] b) {
return deflate(b, 0, b.length, NO_FLUSH);
}
public int deflate(byte[] b, int off, int len, int flush) {
if (b == null) {
throw new NullPointerException();
}
if (off < 0 || len < 0 || off > b.length - len) {
throw new ArrayIndexOutOfBoundsException();
}
synchronized (zsRef) {
ensureOpen();
if (flush == NO_FLUSH || flush == SYNC_FLUSH ||
flush == FULL_FLUSH)
return deflateBytes(zsRef.address(), b, off, len, flush);
throw new IllegalArgumentException();
}
}
...
private static native void initIDs();
private native static long init(int level, int strategy, boolean nowrap);
private native static void setDictionary(long addr, byte[] b, int off, int len);
private native int deflateBytes(long addr, byte[] b, int off, int len,
int flush);
private native static int getAdler(long addr);
private native static long getBytesRead(long addr);
private native static long getBytesWritten(long addr);
private native static void reset(long addr);
private native static void end(long addr);
简单浏览一下代码就知道,前几行代码其实都是一些准备工作,真正做压缩的是deflate方法,而对应的其实是native修饰的deflateBytes方法。
我们打开/jdk/src/share/native/java/util/zip/Deflater.c 源码 ,按照Java层的代码调用顺序看
static {
initIDs();
}
本地实现
JNIEXPORT void JNICALL
Java_java_util_zip_Deflater_initIDs(JNIEnv *env, jclass cls)
{
levelID = (*env)->GetFieldID(env, cls, "level", "I");
strategyID = (*env)->GetFieldID(env, cls, "strategy", "I");
setParamsID = (*env)->GetFieldID(env, cls, "setParams", "Z");
finishID = (*env)->GetFieldID(env, cls, "finish", "Z");
finishedID = (*env)->GetFieldID(env, cls, "finished", "Z");
bufID = (*env)->GetFieldID(env, cls, "buf", "[B");
offID = (*env)->GetFieldID(env, cls, "off", "I");
lenID = (*env)->GetFieldID(env, cls, "len", "I");
}
这个初始化方法是干什么的呢?我们从最直观的角度就能判断,这个方法应该是在获取类的成员变量,因为这个方法里出现了Deflater类的成员变量level、strategy、finish
等等,再看到函数名GetFieldID,如果非常熟悉Java反射机制的话,那么对于Java的Field域是再熟悉不过了,这一段代码就跟Java反射获取类成员是高度相似的,唯一的不同是,这里获取的Field的ID
通过阅读文档,我们可以知道,本地方法访问Java实例变量,有两个步骤。 首先,调用
GetFieldID
函数获取字段的ID,第二步,在以上示例代码中则是调用GetObjectClass
函数来访问实例变量。
看到函数原型
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
前三个参数都很好理解,由于Java中initIDs方法是static方法,所有这里第二个参数传的是jclass类型,主要是最后一个参数不好理解,该参数实际上是一个Field描述符,或者理解为一种类型签名,实际上它是一个字符串,8种基本类型和引用类型都有对应的类型签名,先看一下八种基本类型,最后再做一下总结
Z boolean
B byte
C char
S short
I int
J long
F float
D double
其中带"["的表示数组,如示例中bufID = (*env)->GetFieldID(env, cls, "buf", "[B");
回到Deflater.c 源码,在函数的外层定义了这些全局静态变量,由此很好理解在Java静态代码块所做的事情,实际上就是在类加载的时候,将类成员的FieldId
保存到本地代码的全局静态变量中,这样做的目的就是提高效率,以免本地代码访问类成员时频繁获取FieldId
static jfieldID levelID;
static jfieldID strategyID;
static jfieldID setParamsID;
static jfieldID finishID;
static jfieldID finishedID;
static jfieldID bufID, offID, lenID;
在Deflater类的构造方法中也做了一些事情,主要是对native层的zlib库的一些准备,关于zlib库如何使用就不在本文讨论范围了,实际上也就是函数的调用,看一下zlib库的示例很好理解,接下来看到关键代码
JNIEXPORT jint JNICALL
Java_java_util_zip_Deflater_deflateBytes(JNIEnv *env, jobject this, jlong addr,
jarray b, jint off, jint len, jint flush)
{
z_stream *strm = jlong_to_ptr(addr);
/* 通过之前加载的FieldId 获取类成员变量*/
jarray this_buf = (*env)->GetObjectField(env, this, bufID);
jint this_off = (*env)->GetIntField(env, this, offID);
jint this_len = (*env)->GetIntField(env, this, lenID);
jbyte *in_buf;
jbyte *out_buf;
int res;
if ((*env)->GetBooleanField(env, this, setParamsID)) {
int level = (*env)->GetIntField(env, this, levelID);
int strategy = (*env)->GetIntField(env, this, strategyID);
/* 获取待压缩的字节数组*/
in_buf = (*env)->GetPrimitiveArrayCritical(env, this_buf, 0);
if (in_buf == NULL) {
// Throw OOME only when length is not zero
if (this_len != 0)
JNU_ThrowOutOfMemoryError(env, 0);
return 0;
}
/* 获取压缩后输出的字节数组*/
out_buf = (*env)->GetPrimitiveArrayCritical(env, b, 0);
if (out_buf == NULL) {
(*env)->ReleasePrimitiveArrayCritical(env, this_buf, in_buf, 0);
if (len != 0)
JNU_ThrowOutOfMemoryError(env, 0);
return 0;
}
/* 调用zlib库做压缩之前,需要做的一些准备工作*/
strm->next_in = (Bytef *) (in_buf + this_off);
strm->next_out = (Bytef *) (out_buf + off);
strm->avail_in = this_len;
strm->avail_out = len;
/* C库中,真正做压缩的方法,调用完成后返回一个结果值,用以判断是否成功*/
res = deflateParams(strm, level, strategy);
/* 释放资源*/
(*env)->ReleasePrimitiveArrayCritical(env, b, out_buf, 0);
(*env)->ReleasePrimitiveArrayCritical(env, this_buf, in_buf, 0);
switch (res) {
case Z_OK:
(*env)->SetBooleanField(env, this, setParamsID, JNI_FALSE);
this_off += this_len - strm->avail_in;
(*env)->SetIntField(env, this, offID, this_off);
(*env)->SetIntField(env, this, lenID, strm->avail_in);
return len - strm->avail_out;
case Z_BUF_ERROR:
(*env)->SetBooleanField(env, this, setParamsID, JNI_FALSE);
return 0;
default:
JNU_ThrowInternalError(env, strm->msg);
return 0;
}
} else {
/* 此处省略另一种策略下的压缩逻辑,实际上这个else中的逻辑与if中的类似,只是具体调用的方法不同*/
}
}
从这段源码中,可以学会大数据量数组的传递,以及在本地方法中访问Java变量并修改,特别关键的一点是可以学习标准的JNI编写套路,这里Java层的数组并不是直接通过参数传递到本地代码中的,而是先将字节数组的引用保存到成员变量中,在本地代码中去获取这个类成员变量,从而获取到该数组的指针,前面的博客已经详细说明过,GetPrimitiveArrayCritical
并不一定获取到原始数组的指针,也可能是副本的指针,所以一定要调用ReleasePrimitiveArrayCritical
进行缓冲区释放,回写数组。
从这个源码示例进行引申,将JNI官方文档中,对于Java实例变量与静态变量做一个归纳总结
函数原型
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
函数原型
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
函数原型
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID,NativeType value);
除了实例变量,还有用static修饰的静态变量,看看静态变量如何访问
函数原型
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
函数原型
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz,jfieldID fieldID);
函数原型
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
由于Java的JNI接口被设计出来,主要是用于提升Java性能,复用已有的C/C++库,所以在JDK中没有很好的示例学习在本地代码中创建Java对象和反调Java方法,然而这样的情况在Android NDK开发中则大为不同,JNI成为了本地运行环境和Java层的重要桥梁,因而JNI在Android平台被发扬光大了。在后续的Android NDK相关博客中再从源码角度学习,这里先从官方文档示例入手学习
首先我们回顾一下Java反射机制,在我的Java入门基础博客里曾有写过
所谓反射,简单说就是通过一个class文件对象来使用该class文件中的构造方法,成员变量,成员方法。获取Class类对象通常有三种方法
//1.Object类中的getClass()方法
Person p = new Person();
Class c1 = p.getClass();
//2.通过数据类型的一个静态的class属性
Class c2= Person.class;
//3.通过Class类的一个静态方法forName()
Class c3 = Class.forName("com.test.Person");
在Java中,通常得到了Class对象就可以反射构造方法从而创建一个该类的实例对象,看到文档给出的示例
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jmethodID cid;
jcharArray elemArr;
jstring result;
/*获取jclass对象*/
stringClass = (*env)->FindClass(env, "java/lang/String");
if (stringClass == NULL) {
return NULL;
}
/*获取 String(char[]) 构造函数的MethodID*/
cid = (*env)->GetMethodID(env, stringClass,"" , "([C)V");
if (cid == NULL) {
return NULL;
}
/*创建一个char[]数组作为String类的内容 */
elemArr = (*env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL;
}
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/*创建一个java.lang.String对象 */
result = (*env)->NewObject(env, stringClass, cid, elemArr);
/* 释放本地引用*/
(*env)->DeleteLocalRef(env, elemArr);
(*env)->DeleteLocalRef(env, stringClass);
return result;
}
从这个示例中可以看出,本地代码创建对象的步骤和Java反射其实是一致的
我们通过查询文档,学习示例中的相关方法
FindClass
函数类似Java中的forName
方法,传入完整包名类名,注意中间使用"/"分隔符,该方法加载的是CLASSPATH路径下的类函数原型
jclass FindClass(JNIEnv *env, const char *name);
除了该函数外,还有另一个函数也可以用来或许jclass对象,这一点和Java的getClass()
方法也是高度相似的,都是通过一个实例对象获取自身的class对象
jclass GetObjectClass(JNIEnv *env, jobject obj);
函数原型
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
该函数的使用与GetFieldID
是一样的,最后两个参数传入方法名和方法签名,需要注意的一点是,如果获取的是构造方法,那么方法名统一使用"
,这里还有一个知识点就是方法签名的使用,我们在最后做一个总结
函数原型
jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID,...);
可以看到这个函数后面是不定参,传入的正是Java构造方法中需要的参数。处理NewObject
函数用来创建对象,还有另外两个函数可用,只是传参的形式稍有不同,一个是传入jvalue
数组,另一个则传入va_list
类型
jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
接下再看看如何调用Java成员方法,先看的官方示例
class InstanceMethodCall {
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
}
}
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallVoidMethod(env, obj, mid);
}
与访问成员变量步骤相同,也是两步,首先获得Java方法的MethodID
,最后调用JNI函数执行该方法。此示例中,调用的是CallVoidMethod
函数,它表示执行一个无返回值的Java方法。我们知道Java方法返回值除了Void类型,还可以是8种基本类型和引用类型,下面看到Call函数的原型
<NativeType> Call<Type>Method(JNIEnv *env,jobject obj, jmethodID methodID, ...);
除了实例方法,Java还有静态方法,静态方法的调用步骤与实例方法也是相同的
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
<NativeType> CallStatic<Type>Method(JNIEnv *env, jclass clazz,jmethodIDmethodID, ...);
在本地代码调用Java方法中,最后还有一个点需要注意,即在父类中定义但在子类中已被覆盖的实例方法。在Java中我们直接通过关键字
super.
来调用,在JNI中也有单独处理,而调用步骤和上面讲的也是相同的,都是先获取MethodID,只是执行Java方法的函数是不同的
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...);
在前面无论是访问Java成员变量还是调用Java方法,都涉及到一个描述符的东西,也就是签名,它是一个字符串,有固定的写法规则
可以看到,引用类型必须以L
开头,并带上完整包名类名,其包名中的分隔符使用/
,而数组则必须以[
开头,如果是二维数组,则用[[
,三维数组用[[[
,最后还有需要特别注意的一点,签名的后面一定要以;
结尾
方法签名
与签名的类型前不同的是,方法签名是有返回值的,其固定格式如下
(形参列表)返回值类型
当我们手写方法签名的时候很容易出错,JDK是有提供工具自动生成签名的,就和javah工具一样,生成签名的工具是javap命令,如需生成InstanceMethodCall类的签名,执行如下命令javap -s -p InstanceMethodCall
在上一篇博客我们谈了基本类型数组的使用,这一篇的最后就补充一下关于字符串和对象类型数组的使用
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than 127 characters */
scanf("%s", buf);
return (*env)->NewStringUTF(env, buf);
}
实际上在C语言中是没有字符串类型的,字符串就是一个字符数组,因此对于字符串的操作,和数组是非常类似的,但是为了和Java的类型对接起来,JNI对Java的字符串提供了专有的函数操作,我们学习了前面的基本类型数组的操作,对于字符串的操作函数就非常好理解了。
从官方示例中可以看出来,这段代码进行了两种操作,1.获取Java层传下来的字符串,并转换为本地代码中的字符数组;2.在本地代码中创建一个Java字符串对象返回。
const jbyte * GetStringUTFChars(JNIEnv *env,jstring string, jboolean *isCopy);
如果有可能,该函数会返回一个指向Java字符串的UTF-8类型字符数组的指针,最后一个参数,我们在之前讲数组操作时已经讲过,注意传值方式,传入的是一个指针。它我们之前探讨的数组操作一样,也有可能会返回一个副本的指针,因此在最后一定要调用对应的Release函数释放副本,回写改变后的值到Java原始字符串中。
void ReleaseStringUTFChars(JNIEnv *env,jstring string, const char *utf);
与我们之前创建的字符串对象不同,这里没有去获取MethodID然后调用构造方法去创建,而是使用NewStringUTF
函数,从UTF-8字符数组构去造一个新的String对象
jstring NewStringUTF(JNIEnv *env, const char *bytes);
除了这些函数之外,还有一些字符串操作的函数,这些函数基本上与数组操作函数完全对应
JNI Function | Description | Since |
---|---|---|
GetStringChars ReleaseStringChars |
以Unicode格式获取或释放指向字符串内容的指针。 可能会返回字符串的副本。 | JDK1.1 |
GetStringUTFChars ReleaseStringUTFChars |
获取或释放指向UTF-8格式的字符串内容的指针。可能返回字符串的副本。 | JDK1.1 |
GetStringLength | 返回Unicode字符串中的字符长度。 | JDK1.1 |
GetStringUTFLength | 获取 UTF-8 编码字符串的长度,也可以通过标准 C 函数 strlen 获取 | JDK1.1 |
NewString | 通过给定的Unicode C字符串创建一个Java String字符串 | JDK1.1 |
NewStringUTF | 通过给定的UTF-8编码C字符串创建一个Java String字符串 | JDK1.1 |
GetStringCritical ReleaseStringCritical |
获取Unicode格式的字符串内容的指针。 可能会返回字符串的副本。 与我们之前数组操作一样,在Get / ReleaseStringCritical调用之间不得进行阻塞操作 | JDK1.2 |
GetStringRegion SetStringRegion |
以Unicode格式将字符串的内容复制到预分配的C缓冲区或从预分配的C缓冲区中回写修改的内容到原始字符串中。 | JDK1.2 |
GetStringUTFRegion SetStringUTFRegion |
以UTF-8 格式将字符串的内容复制到预分配的C缓冲区或从预分配的C缓冲区中回写修改的内容到原始字符串中。 | JDK1.2 |
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
示例
JNIEXPORT jobjectArray JNICALL Java_com_test_modifyStringArr(JNIEnv * env, jclass jc,jobjectArray strArray )
{
jstring jstr;
jsize len = (*env)->GetArrayLength(env, strArray);
char **pstr = (char **) malloc(len*sizeof(char *));
int i=0;
for (i=0 ; i< len ;i++){
jstr = (*env)->GetObjectArrayElement(env, strArray, i);
*pstr[i] = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
}
jclass stringClass = (*env)->FindClass(env, "java/lang/String");
jobjectArray strArr = (*env)->NewObjectArray(env, len , stringClass, NULL);
if( strArr == NULL){
return NULL;
}
for (i=0 ; i< len ;i++){
jstring js = (*env)->NewStringUTF(env,*pstr[i])
(*env)->SetObjectArrayElement(env, strArr , i, iarr,js);
}
free(pstr);
return strArr ;
}
String(byte[] data, String charsetName)