JNI编程原理

1. JNI简介

JNI - Java Native Interface,它是Java调用Native语言的一种特性。通过JNI可以在Java代码中调用C/C++语言的代码,同样也可以在C/C++代码中调用Java代码,这样可以发挥各语言的特性。特别是算法基本都是c/c++实现,这样确保运行的性能,而业务逻辑与交互则在Java层实现。JNI相当于担任了一个桥梁的角色,它将JVM与Native模块联系起来,从而实现了Java代码与Native代码的互访。

假定已有一份c++的人脸检测代码,现在我们想将它移植到手机上,从而实现Android版的人脸应用。这里就涉及JNI的知识,需将Java层视频或图片数据传递到native层,完成人脸检测后再将结果返给到Java层做相应的业务逻辑。要实现这个功能,大概要经历如下几步:

  1. 首先我们在java层定义好native方法,对应NativeMethod中的detect();
  2. 在JNI层,定义好与java对应的native方法 Jave_com_example_facedemo_NativeMethod_detect(),同时在该方法内再进一步调用c++实现的人脸检测方法;
  3. 在c/c++层,对应具体的人脸检测代码实现;
  4. 通过NDK将native代码编译成FaceDetection.so库文件;
  5. 完成android开发后整体打包成FaceDemo.apk可执行文件。

运行时,通过system.loadLibrary("FaceDetect")加载我们编译好的libFaceDetect.so库,通过detect方法将Java层的视频数据传递给Jni层,再调用c++的人脸检测模块。在这个过程中,Java层的detect函数到底是怎么和JNI层的detect函数建立关联的呢?这就涉及到动态注册和静态注册的概念,后续所有介绍都是基于静态注册的方式来实现的。

在JNI调用相关方法之前,需要对Java中以native关键字修饰的方法进行注册。通过注册,将指定的native方法和so中对应的方法绑定起来,这样就可以调用相应的so层的函数了。默认情况下,都会使用静态注册的方式,具有开发简单的优点,缺点是JNI函数名比较长。背后的运行原理大概为:

1)根据函数名将Java代码中的native方法与so中的JNI方法一一对应,当Java层调用so层的函数时,如果发现其上有JNIEXPORT和JNICALL两个宏定义申明时,就会将so层函数链接到对应的native方法上。

2)而native方法和so方法对应的规则是:以字符串 "Java" 为前缀,并且用 "_" 下划线将包名、类名以及native方法名链接起来就是对应的JNI函数名了。

在该例中,包名为com.example.facedemo,类名" 为NativeMethod,函数名为detect,则对应JNI的函数名为Java_com_example_facedemo_NativeMethod_detect。在JNI的detect函数中,除了图像数据和宽高这3个参数外,我们注意到其前面多了两个参数:JNIEnv *和jobject,它们是必需的。前者是一个JNIEnv 结构体的指针,这个结构体中定义了很多JNI的接口函数指针,使开发者可以使用JNI所定义的接口功能;后者指代的是调用这个JNI函数的Java对象,有点类似于C++中的this 指针。在上述两个参数之后,才是与Java层函数声明依次对应的参数。这时又引出下面将要介绍的内容,Java层和JNI层之间的数据定义是有差异的,那他们之间是如何对应的呢?

2. 数据类型

从上面人脸检测例子中可以看到,我们需要从Java层把视频数据当参数传递给c++层,同时底层完成人脸检测后需要把结果返回到Java层,需要清楚的一个问题是:Java编程语言中的数据类型与C/C++等本地编程语言中的数据类型是如何实现映射的。

JNI中定义了一组对应于Java编程语言中类型的C/C++类型。在Java编程语言中存在着两种类型:基本数据类型,如int、float和char,以及引用类型,如类、实例和数组。基本数据类型的映射是直接的,Java编程语言中的int类型映射为C/C++的jint(定义在jni.h中,为32位有符号整型数),Java编程语言中的float类型映射为C/C++的jfloat(定义在jni.h中,为32位浮点类型数),如下表所示。

Java类型

Native类型

说明

boolean

jboolean

unsiged 8 bits

byte

jbyte

signed 8 bits

char

jchar

unsigned 16 bits

short

jshort

signed 16 bits

int

jint

signed 32 bits

long

jlong

signed 64 bits

float

jfloat

32 bits

double

jdouble

64 bits

void

void

JNI传递对象给本地方法作为不透明引用,不透明引用指的是引用Java虚拟机内部数据类型的C指针类型。本地代码可以通过JNIEnv接口指针指向的适当的函数来操作底层对象。例如,java.lang.String对应于JNI类型jstring,jstring引用的确切位置和本地代码是不相关的。JNI定义了一组在概念上是jobject的子类型的引用类型,这些子类型对应于Java编程语言中经常使用的引用类型。例如:jstring表示字符串,jobjectArray表示对象数据。

3. 访问数据

了解了Java与JNI之间数据类型的映射关系后,我们看下Jni层如何访问Java层传递的数据。对于基本的数据类型,比如int,float等,可以实现直接的访问;但对于String,数组等引用类型,就需要利用一系列函数来实现。

3.1 访问字符串

jstring类型在Java虚拟机代表着字符串,但不同于常规的C字符串(char *)。不能将jstring作为常规C字符串来使用,比如运行下面的代码不会得到期望的结果,可能会导致Java虚拟机崩溃。

JNIEXPORT jstring JNICALL Java_com_example_jnidemo_NativeMethod_nativeString
        (JNIEnv *env,jobject thisz, jstring s)
{
	/* ERROR: incorrect use of jstring as a char* pointer */ 
	printf("%s", s); 
	...
}

我们通过下面的例子来说明如何通过JNI获取字符串内容,同时创建1个新的字符串返回。

JNIEXPORT jstring JNICALL Java_com_example_jnidemo_NativeMethod_nativeString
        (JNIEnv *env,jobject thisz, jstring s)
{
	const char *str = env->GetStringUTFChars(s,0);
	if(str == NULL)
		return NULL;
    
	int len = env->GetStringUTFLength(s); //length of the string ,not include'\0'
    //doing somthing about str
    env->ReleaseStringUTFChars(s,str);
  
    jstring strout = env->NewStringUTF("abc"); //create new string
    return strout;
}
  1.  转换为本地字符串:通过调用JNI方法GetStringUTFChars来读取字符串的内容,它将通常在Java虚拟机中实现为Unicode序列的jstring引用转换为UTF-8格式的C式字符串。一定要检查GetStringUTFChars的返回值,这是因为Java虚拟机内部需要申请内存来容纳UTF-8字符串,内存的申请是有可能会失败的。如果内存申请失败,那么GetStringUTFChars将会返回NULL并且会抛出OutOfMemoryError异常。
  2. 释放本地字符串资源:使用完通过GetStringUTFChars获取的UTF-8字符串后,需要调用ReleaseStringUTFChars,表明本地代码不再需要GetStringUTFChars返回的UTF-8字符串了,调用ReleaseStringUTFChars就能够释放掉UTF-8字符串占用的内存。如果不调用该函数,则会产生内存泄漏,积累到一定程度最终导致内存耗尽。

  3. 创建新字符串:通过调用JNI函数NewStringUTF,你可以在本地代码中创建一个新的java.lang.String实例。NewStringUTF方法使用一个UTF-8格式的C式字符串作为参数并生成一个java.lang.String实例对象。如果虚拟机没办法申请足够的内存来构造java.lang.String实例的话,NewStringUTF会抛出一个OutOfMemoryError异常并返回NULL。

这里列出常见的几种JNI字符串函数:

JNI函数

描述

GetStringChars\ReleaseStringChars

获取或释放指向Unicode格式的字符串内容指针

GetStringUTFChars\ReleaseStringUFTChars

获取或释放指向UTF-8格式的字符串内容指针

GetStringLength

返回字符串中Unicode字符的数量

GetStringUTFLength

返回以UTF-8格式的字符串所需字节数,不包含尾数'\0'

NewString

创建Unicode格式的java.lang.String实例

NewStringUTF

创建UTF-8格式的java.lang.String实例

3.2 访问数组

与3.1提到的 jstring 一样,jarray也不是C语言数据,不能直接访问jarray引用来完成相关操作。比如我们要对int数组的所有元素求和,利用下面的C代码是非法的。

/* This program is illegal! */ 
JNIEXPORT jint JNICALL Java_com_example_jnidemo_NativeMethod_sumArray
        (JNIEnv *env, jobject obj, jintArray arr)
{
    int i, sum = 0;
    for (i = 0; i < 10; i++)  
        sum += arr[i];
    return sum;
}

要想正确的访问jintArray数组元素,下面给出了正确的使用方法。

JNIEXPORT jint JNICALL Java_com_example_jnidemo_NativeMethod_sumArray
        (JNIEnv *env, jobject obj, jintArray arr)
{
    jint buf[10]; 
    jint i, sum = 0;
    env->GetIntArrayRegion(arr, 0, 10, buf); 
    for (i = 0; i < 10; i++)  
        sum += buf[i];    
    return sum; 
}

GetIntArrayRegion函数来复制整型数组中的所有元素到C缓冲区中。第二个参数是需要复制的元素的起始索引,第三个参数表示需要复制的元素的总数。完成元素的复制后,我们就能够在本地代码中访问他们了。在这个例子中,我们已知数组的长度为10,因此不会引发越界问题。实际项目中我们需要知道传入数组的真实长度,这时候可以使用env->GetArrayLength(arr)来获取。

除了利用GetIntArrayRegion复制数据,另一种访问数据的方式是利用GetIntArrayElement函数

JNIEXPORT jint JNICALL Java_com_example_jnidemo_NativeMethod_sumArray
        (JNIEnv *env, jobject obj, jintArray arr)
{     
    jint *pdata = env->GetIntArrayElements(arr,0);
    if (pdata == NULL) {
        return 0; 
    }    
    
    int len = env->GetArrayLength(arr);    
    int sum = 0;
    for (i=0; i<len; i++) {
        sum += pdata[i];
    }
    
    env->ReleaseIntArrayElements(arr, pdata, 0);    
    return sum;
}

与jstring操作一样,需要调用ReleaseIntArrayElements来释放内存。

这里列出常见的几种JNI数组函数:

JNI函数

描述

Get<Type>ArrayRegion

复制type类型数组到c缓存区

Set<Type>ArrayRegion

利用c缓存区数据修改type类型数组元素

Get<Type>ArrayElements\

Release<Type>ArrayElements

获取\释放指向type类型的数组指针

GetArrayLength

返回数组中元素的个数

New<Type>Array

创建给定长度的数组

<Type>表示的是基本类型,比如 int,boolean,short等。

3.3访问对象数组

JNI提供了一对单独的函数来访问对象数组,GetObjectArrayElement返回给定索引处的元素,而SetObjectArrayElement更新给定索引处的元素。上面提到数组也是引用对象类型,下面我们利用一个本地生成二维数组的例子来说明对象数组的访问。

JNIEXPORT jobjectArray JNICALL Java_com_example_jnidemo_NativeMethod_init2DArray
        (JNIEnv *env,jobject thisz,jint dim)
{
	jobjectArray retArr;

	jclass intArrCls = env->FindClass("[I");
	if(intArrCls == NULL)
		return NULL;

	retArr = env->NewObjectArray(dim,intArrCls,0);
	if(retArr == NULL)
		return NULL;

	for(int i=0; i<dim; i++)
	{
		int tmp[256]; //make sure larger than dim

		jintArray iarr = env->NewIntArray(dim);
		if(iarr == NULL)
			return NULL;

		for(int j=0; j<dim; j++)
			tmp[j] = i+j;

		env->SetIntArrayRegion(iarr,0,dim,tmp);
		env->SetObjectArrayElement(retArr,i,iarr);
		env->DeleteLocalRef(iarr);
	}

	return retArr;
}

initInt2DArray方法首先调用JNI函数FindClass来获取一个对二维int类型数组的元素类的引用。FindClass的参数 "[I" 是一个对应于Java编程语言中int[]类型的JNI类描述符。如果类型查询失败,FindClass会返回NULL并抛出异常(例如由于缺少类文件或者内存不足的情况)。

下一步,NewObjectArray函数分配一个数组,其元素类型由intArrCls类应用决定。NewObjectArray仅仅分配第一个维度,我们仍然需要填写构成第二个维度的数组元素。Java虚拟机中没有特殊的数据类型来表示多维数组。一个二维数组其实就是一个数组。

创建第二维数组的代码是简单易懂的。NewIntArray分配独立的数组元素,SetIntArrayRegion将tmp缓冲区的内容复制到新分配的一维数组中。完成SetObjectArrayElement调用后,第i个一维数组的第j个元素的值为i+j。在循环结尾调用DeleteLocalRef释放iarr,避免内存占用。

4. 成员变量交互

在实际工程中,除了Java层传递参数给JNI层,底层处理完的结果也需要返回到Java层。比如下面的例子中,NativeMethod类定义了name、bSucess、width、coord四个成员变量,调用accessField方法后JNI层对这些变量的值进行访问和修改,之后再在Java层利用这些变量来处理后续的业务逻辑。

class NativeMethod{
      private String name="java_abc";
      private boolean bSucess = False;
      private int  width = 100;
      private int[]  coord;

      private native void accessField();
        
      static {
           system.loadLibrary("NativeMethod");
     }
}

通过下面的JNI代码,我们获取变量的值,并在底层实现修改:

name: JNI获取Jave层的变量并打印出来,并在底层将其修改为"native_abc"

bSucess: JNI层获取它的值,并取反

width: JNI先获取初始值,新的值为初始值的2倍

coord: 在JNI层给它赋值为[1,2,3,4]的四维数组

JNIEXPORT void JNICALL Java_com_example_jnidemo_NativeMethod_accessField(JNIEnv *env,jobject thisz)
{
	/* Get a reference to obj’s class */
	jclass cls = env->GetObjectClass(thisz);
	if(cls == NULL)
		return;

	jfieldID fid_width = env->GetFieldID(cls,"width","I");
	if(fid_width != NULL)
	{
		jint jw = env->GetIntField(thisz,fid_width);
		env->SetIntField(thisz,fid_width,jw*2);
	}

	jfieldID fid_coord = env->GetFieldID(cls,"coord","[I");
	if(fid_coord != NULL)
	{
		jintArray arr = env->NewIntArray(4);
		if(arr != NULL)
		{
			int tmp[4]={1,2,3,4};
			env->SetIntArrayRegion(arr,0,4,tmp);
			env->SetObjectField(thisz,fid_coord,arr);
			env->DeleteLocalRef(arr);
		}
	}

	/**/
	jfieldID fid_sucess = env->GetFieldID(cls, "bSucess","Z");
	if(fid_sucess != NULL)
	{
		jboolean jbln = env->GetBooleanField(thisz,fid_sucess);
		if(jbln == false)
			env->SetBooleanField(thisz,fid_sucess,true);
		else
			env->SetBooleanField(thisz,fid_sucess,false);
	}

	/* Look for the instance field ID in cls */
	jfieldID fid_name = (env)->GetFieldID(cls, "name", "Ljava/lang/String;");
	if(fid_name != NULL)
	{
		/* Read the instance field*/
		jstring jstr = (jstring)env->GetObjectField(thisz, fid_name);
		if(jstr!=NULL)
		{
			const char *str = env->GetStringUTFChars(jstr, NULL);
			if (str != NULL)
			{
				env->ReleaseStringUTFChars(jstr, str);

				/* Create a new string and overwrite the instance field */
				jstr = env->NewStringUTF("native_abc");
				if (jstr != NULL)
					env->SetObjectField(thisz,fid_name,jstr);
			}
		}
	}
}

上述代码较长,可能看起来有点晦涩难度。总结下来,要实现对变量的访问与修改,包括如下4步:

1. 获取类

JNI提供了两个接口,第一个传递jobject来获取class

jclass cls = env->GetObjectClass(thisz);

第二个是通过传递类名来获取

jclass objCls = env->FindClass("com/example/jnidemo/circleData");

2. 获取变量ID

jFieldID fid = env>GetFieldID("类对象", "参数名", "参数的字段描述符"); 比如下面获取变量width对应的ID,当没找到时返回NULL。最好做个判断,如果没找到而后续又使用会导致程序奔溃。

jfieldID fid_width = env->GetFieldID(cls,"width","I");
if(fid_width == NULL)
    return;

下表给出了Java类型所对应的字段描述符细节

Java类型

字段描述符

备注

int

I

int的首字母,大写

float

F

float的首字母,大写

double

D

double的首字母,大写

short

S

short的首字母,大写

long

L

long的首字母,大写

char

C

char的首字母,大写

byte

B

byte的首字母,大写

boolean

Z

因B被byte占用,使用Z

object

L+/分隔完整类名

如 String对应Ljava/lang/String

array

[+类型描述符

如 int[]对应 [I

3. 获取变量的值

在知道了变量ID之后,那我们就可以获取他们的值。对于String和Array这类对象类型,使用GetObjectField来访问,第二个参数为该变量的ID。

jstring jstr = (jstring)env->GetObjectField(thisz, fid_name);

对于常见的数据类型,采用Get<Type>Field来访问,Type为基本数据类型,第二个参数为变量ID。

jint jw = env->GetIntField(thisz,fid_width);

4. 修改变量的值

除了获取变量的值,也能对它的值进行修改。对于String和Array这类对象类型,使用SetObjectField来修改,第二个参数为该变量的ID,第三个参数为新的值。

int tmp[4]={1,2,3,4};
env->SetIntArrayRegion(arr,0,4,tmp);
env->SetObjectField(thisz,fid_coord,arr);

对于常见的数据类型,采用Set<Type>Field来修改,Type为基本数据类型,第二个参数为变量ID,第三个参数为新的值。

jint jw = env->GetIntField(thisz,fid_width);
env->SetIntField(thisz,fid_width,jw*2);

备注:

如果成员变量定义为静态的,如 static String name,那么应该采用带有static的函数:GetStaticFieldID,GetStaticIntField,SetStaticIntField。

5. JNI局部引用

在阅读上面代码过程中,我们会发现出现了一个没介绍过的函数DeleteLocalRef,这和本节将要介绍的“局部引用”内容有关。局部引用也称本地引用,通常是在JNI 函数内部创建的 ,比如通过 NewLocalRef 和各种 JNI 接口(FindClass、NewObject、GetObjectClass和NewCharArray等)来创建,它们在 JNI 函数返回后无效。一般情况下,JVM会去自动释放局部引用,但下面一些情况下必须手动调用 DeleteLocalRef() 去释放,避免造成奔溃或不必要的内存占用。

1. 创建大量JNI局部引用

for (i = 0; i < len; i++) {
    jstring jstr = env->GetObjectArrayElement(arr, i);
    ... /* 使用jstr */
    env->DeleteLocalRef(jstr); // 使用完成之后马上释放
}

JNI局部引用表的数量是有限的,比如512个;当创建大量的局部引用,如果没即时释放,会导致JNI局部引用表的溢出,所以在不需要局部引用时就立即调用DeleteLocalRef手动删除。比如,在上面的代码中,本地代码遍历一个特别大的字符串数组(len很大),每遍历一个元素,都会创建一个局部引用 "jstr",当使用完这个元素的局部引用时,就应该马上手动释放它。

2. 创建大的对象引用

因为局部引用会阻止所引用的对象被GC回收所以需要即时手动释放不再需要的资源。

JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
    jintArray arr = (env)->NewIntArray(w*h); /* arr引用的是一个大的数组 */
    ...                                      /* 在这里完成赋值操作,后续不再使用 */
    (*env)->DeleteLocalRef(env, arr);        /* 及时删除,并释放相应的资源*/
    ...                                      /* 在里有个比较耗时的计算过程 */
    return;                                  /* 函数返回之前所有引用都已经释放 */
}

比如上面代码,在本地函数中刚开始需要访问一个大对象,w*h大小的intArray用于图像数据的处理,一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止GC回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就应该把引用给释放了,以免不必要的资源浪费。

到这里,我们把后续所需要的JNI编程基础知识都介绍完了。需要指出的是这只是JNI开发的冰山一角,要了解更多的内容,可以搜索有关《JNI/NDK开发指南》的文章,做更全面深入的学习。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值