Android通过JNI和原生代码(C++)通信(持续更新到写完整为止)

“初中”

如果你做android原生开发的话,就会用到android和C++交互,也就是JNI开发。而每次新增接口最麻烦的部分,也是JNI这一层。需要做各种java/c++的类型转换,以及回调的反射等处理。接下来,详细记录一下android如何通过jni和C++代码通信,以及过程中可能出现的问题,方便自己,也方便其他人参考。这就是写这篇文章的“初中“啦。

JNI简介

JNI(Java Native Interface),是Java程序设计语言功能最强的特征,它允许Java类的某些方法原生实现,同时让它们和普通Java方法一样调用。原生方法也可以创建、使用Java对象。

Hello World

按照惯例,先从HelloWorld(官方Demo)开始。

Java native接口声明:

	package com.example.hellojni;
	public class HelloJni{
    	static{
        	System.loadLibrary("hello-jni");
    	}
    	...

    	public native String stringFromJNI();
    }

对应的原生实现:

    #include <string.h>
    #include <jni.h>
    ...

    Jstring
    Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env, jobject thiz)
    {
        return (*env)->NewStringUTF(env, "Hello from JNI !");
    }

添加原生实现的步骤:

  1. 声明Java的native方法声明
  2. 编写native对应的原生实现
  3. 将原生实现编译成动态库
  4. 在Java代码中加载该库

如此之后,就可以像使用java方法一样调用原生方法了。但是,我们发现java接口对应的JNI接口好像很复杂的样子,这个是怎么对应的,该如何写出这个JNI接口?
其实,我们仔细观察,还是很有规律的:

返回类型 Java_全限定类名(.换成_)_方法名(JNIEnv*,jobject)

其中,前两个参数是固定的,所有方法都有的,这两个参数我们后面再讲。

由此可以看出,想要写出Java native方法对应的原生接口声明,是一件非常麻烦且容易岀错的苦差事。而且,原生接口使用下划线分割包名,那么问题来了:我java方法本身就有下划线,肿么办?

其实不用担心,这些问题官方爸爸都替我们想了。
我们只需要写好Java的native方法,使用javah命令就可以直接生成对应的JNI接口声明。

javah -classpath/bin/ com.example.hellojni.HelloJni

java类的定义:

package com.example.hellojni;

public class HelloJni {
	public static native String stringFromJNI(); 
	public static native String string_from_jni();
}

运行javah命令,可在bin目录下生成com_example_hellojni_HelloJni.h头文件,其中包含了HelloJni中所有native方法对应的JNI方法声明,如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_hellojni_HelloJni */

#ifndef _Included_com_example_hellojni_HelloJni
#define _Included_com_example_hellojni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_hellojni_HelloJni
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI
  (JNIEnv *, jclass);

/*
 * Class:     com_example_hellojni_HelloJni
 * Method:    string_from_jni
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_string_1from_1jni
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

如此,是不是很方便!方法包含下划线的话,只需要在原来的下划线后面跟一个字符’1"即可。

光有这些,我们应该还有很多问题:Java跟C++之间数据类型如何对应和转换?我想从原生实现回调结果怎么办?岀参该怎么实现?

接下来,我们一一介绍。

java/jni/c++类型对照

先来看看java、jni、c++之间的类型对照:

1、基本数据类型

基本数据类型可以直接和C++的相应基本数据类型映射,如下表所示。JNI使用类型定义使得这种映射对开发人员透明,也就是JNI中的类型是定义在jni.h头文件中的宏,C++开发可以直接看到。

表1.基本数据类型对照表

JavaJniC++size
booleanjbooleanunsigned char无符号8位
bytejbytesigned char有符号8位
charjbyteunsigned short无符号16位
shortjshortshort有符号16位
intjintint有符号32位
longjlonglong long有符号64位
floatjfloatfloat32位
doublejdoubledouble64位

2、引用类型

与基本类型不同,引用类型对原生方法是不透明的,它们的内部数据结构并不直接向原生代码公开。引用类型映射表如下。

表2.引用类型对照表

Java原生类型
java.lang.Classjclass
java.lang.Throwablejthrowable
java.lang.Stringjstring
Other objectsjobject
boolean[]jbooleanArray
byte[]jbyteArray
char[]jcharArray
short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray
java.lang.Object[]jobjectArray
Other arraysjarray

从表中可以看到,除了几个特殊的类,其他类对应的原生类型都是jobject,也就是上面说的不透明。但是不透明的话,我们如何获取该对象的字段和方法呢?这个就涉及到如何将JNI类型转换为C++类型了,我们继续往下看。

Java/JNI/C++类型转换

接下来,看看这些类型之间该如何转换。

1、基本类型转换

基本类型boolean,byte,char,int,shrot,long,float,double和C++中类型是一一对应的,无需转换,可直接赋值,见表1。

2、数组

以byte数组为例:

	//参数说明:
	java --> byte[] in;
    jni -->  jbyteArray jIn;
    JNIEnv* env;

	//类型转换
	jbyte *srcBytes = env->GetByteArrayElements(jIn, 0);
	//use srcBytes as char* in C++
	
	//不要忘记释放哦
	env->ReleaseByteArrayElements(jIn, srcBytes, 0);

通过GetByteArrayElements函数获取到字节数组jIn的内存指针,有了这个指针,C++就可以操作和使用该内存了。用完别忘记释放哦!

其他数组都类似,使用函数对GetXXXArrayElementsReleaseXXXArrayElements即可。

3、对象类型

对象类型的转换比较麻烦,不过套路是一样的,一通可百通。

先来看个最常用的String类型的转换吧

	//参数说明:
	java --> String str;
	jni --> jstring jstr;
	JNIEnv* env;

	//类型转换,先jstring-->char*,随后就可正常使用了
	char* szStr = (char*)env->GetStringUTFChars(jstr, 0);
	
	//使用完成不要忘记释放哦
	env->ReleaseStringUTFChars(jstr,szStr);

接下来再看一个自定义类型的转换
本示例将java类com.example.Temp转换成C++结构体Temp,其中
java类的定义:

	com.example;
	public class Temp{
		public int a;
		public boolean b;
	}

c++结构体定义:

	typedef _temp{
		int a;
		bool b;
	}Temp;

类型转换:

	//参数说明:
	java --> Temp temp;
	jni --> jObject jTemp;
	JNIEnv* env;
	Temp p;
	
	//首先,获取java类
	jclass tempCls = env->FindClass("com/example/Temp");
	
	//然后,获取字段ID
	jfieldID aaId  = env->GetFieldID(tempCls, "a", "I");
	jfieldID bbId  = env->GetFieldID(tempCls, "b", "Z");
	
	//最后,获取字段值
	jint aa =  env->GetIntField(jTemp, aaId);
	p.a = aa;
	
	jboolean bb =  env->GetBooleanField(jTemp, bbId);
	p.b = bb;

套路总结:

  • 通过java类的包名,获取jclass。
    jclass cls = env->FindClass(“pkgName”);
  • 通过字段名和字段签名,获取字段ID。
    fieldID = env->GetFieldID(cls, “memberName”,“memberSign”);
  • 通过对象和字段ID,获取字段值。
    XXX a = env->GetXXXField(obj,fieldID);

签名

这里可以看到,在获取类字段ID、方法ID时,都分别需要字段类型签名和方法签名。这里重点讲一下签名问题,这是很多新手容易忽视和出错的地方。签名映射可以通过下表获取:

表3.Java类型签名映射表

java类型签名
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
fully-qualified-classLfully-qualified-class;
type[][type
method type(arg-type)ret-type

在写代码时,由于该签名参数是字符串,没有语法检查,所以如果不小心写错的话,不容易排查。如果只是JNI函数参数的话,因为都是单个的,一般不会岀错(如果你硬是出错了,那我只能说:你不是一般人!)。但如果是回调的话,其中方法的签名比较长,对象数组和基本类型混合的话,就很容易岀错了。因此涉及到签名的,请格外小心,写完代码后做好review。

今天就先写到这里啦!

未完
待续...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值