做android系统开发,免不了用到JNI,JNI是java层与native层的交互方式之一,也是系统中用的最多的,譬如binder机制,有native的binder实现也有java层的binder类封装通过JNI调用native层;又譬如audio系统,java层有audiotrack,audiorecode等,而native层也有audiotrack,audiorecode等;譬如surface系统,java层有surface,而native层也有surface;mediaplayer更是如此等等。把JNI称为android系统中将java世界连接native世界的纽带也不为过,但用JNI也会有一些优缺点,在这里就不详细说明了,一旦使用JNI,JAVA程序就丧失了JAVA平台的两个优点:
1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。这个是针对android与其他平台而言。
2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。
一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JAVA和C之间的耦合性。
下面我将JNI中常用的几点归纳总结并用几个简单的例子来说明jni的用法。
一、JNI库的加载
java要调用native层函数,就要通过JNI层的动态库来实现。通常的做法是在类的static中调用System.loadLibrary方法。
public class testService extends Service {
private static String TAG = "testService";
/*加载jni库*/
static {
System.loadLibrary("test_jni");
}
/*声明jni库中的函数*/
private native final void test_class_init() throws IllegalStateException;
private native final int test_fun1() throws IllegalStateException;
private native final inttest_fun2() throws IllegalStateException;
以上只截取了部分代码,代码是一个service中创建的时候加载这个test_jni库,而so的位置就要具体应用具体分析了,如果是预制应用(system/app,system/pri_app,vendor/app),so一般放在system/lib或system/lib64下。如果是普通应用,随apk打进包里,就会在安装的时候放到/data/data/*****/lib下。 这里要注意System.loadLibrary后的库名称省略了lib,真实的so名称应该为libtest_jni.so。
二、JNI的注册
1.静态方法(不常用)
先编写java程序,然后生成class文件。使用java的工具javah(javah -o output 包名.类名)生成c/c++的头文件(包名_类名.h)。之后创建.h对应的源文件(包名_类名.cpp),实现对应的native方法。
2.动态方法
动态注册基本都是一个固定的注册方式,不过方法可以随时添加删除(java层的函数声明也要做相应修改),并且函数名字不会像静态那样非常的冗长,一下我就提供一个模版参考。
static void android_test_class_init(JNIEnv* env, jobject thiz)
{
if (!gBonovoSensorObj)
gBonovoSensorObj = env->NewGlobalRef(thiz);
jclass interfaceClass = env->GetObjectClass(gBonovoSensorObj);
method_report = env->GetMethodID(interfaceClass, "sensorCallback", "(IDDDDD)V");
}
static jint anroid_test_fun1(JNIEnv* env,jobject thiz, jint para)
{
return(JNI_TRUE);
}
static jint anroid_test_fun2(JNIEnv* env,jobject thiz, jint para)
{
return(JNI_TRUE);
}
static const char *classPathName = "包名/类名";
static JNINativeMethod sMethods[] = {
{"test_class_init","()V",(void *)android_test_class_init},
{"test_fun1", "(I)I", (void *)anroid_test_fun1},
{"test_fun2", "(I)I", (void *)anroid_test_fun2},
};
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* methods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, methods, numMethods)
LOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* Register native methods for all classes we know about.
*
* returns JNI_TRUE on success.
*/
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env, classPathName,
sMethods, sizeof(sMethods) / sizeof(sMethods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* This is called by the VM when the shared library is first loaded.
*/
typedef union {
JNIEnv* env;
void* venv;
} UnionJNIEnvToVoid;
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
UnionJNIEnvToVoid uenv;
uenv.venv = NULL;
jint result = -1;
JNIEnv* env = NULL;
gJavaVM = vm;
LOGI("JNI_OnLoad");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) {
LOGE("ERROR: GetEnv failed");
goto bail;
}
env = uenv.env;
if (registerNatives(env) != JNI_TRUE) {
LOGE("ERROR: registerNatives failed");
goto bail;
}
result = JNI_VERSION_1_6;
bail:
return result;
}
蓝色标注的部分基本可以不用动,只要修改红色字体部分即可。下面详细说明一下
static const char *classPathName = "包名/类名"为加载jni库的类全名,sMethods数组是java层函数与jni函数对接的关键,其中用类型签名做了重载机制,之后会介绍到类型签名。当java层调用System.loadLibrary加载JNI动态库后,会查找该库中的JNI_OnLoad函数,这里会取虚拟机环境建议保存为全局gJavaVM为以后调用做准备,注册native函数到java的sMethods。
三、Java与JNI的数据类型转换
1.基本数据类型转换
2.引用数据类型转换
字符串的处理
GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8格式的C字符串。从GetStringUTFChars中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉JVM这个UTF-8字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。
通过JNI函数NewStringUTF在本地方法中创建一个新的java.lang.String字符串对象。这个新创建的字符串对象拥有一个与给定的UTF-8编码的C类型字符串内容相同的Unicode编码字符串。
基本数组和对象数组的处理
如果你想在一个预先分配的C缓冲区和内存之间交换数据,应该使用Get/SetArrayRegion系列函数。这些函数会进行越界检查,在需要的时候会有可能抛出ArrayIndexOutOfBoundsException异常。
JNI提供了一个函数对来访问对象数组。GetObjectArrayElement返回数组中指定位置的元素,而SetObjectArrayElement修改数组中指定位置的元素。与基本类型的数组不同的是,你不能一次得到所有的对象元素或者一次复制多个对象元素。字符串和数组都是引用类型,你要使用Get/SetObjectArrayElement来访问字符串数组或者数组的数组。
四、JNIEnv与JavaVM
JNIEnv的类型是一个指针,指向存储全部JNI函数指针的结构,该指针只在创建它的线程有效,不能跨线程传递。JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,这个对象是线程共享的。
JNIEnv与JavaVM一般什么时候用呢? 因为JNIEnv在线程中起效,并且每一个jni函数的第一个参数都是JNIEnv,所以java调用jni的时候,直接使用JNIEnv指针就可以了。那么要问了,JavaVM有什么用? 当native调用java的时候,这个时候native中没有JNIEnv指针,那用什么呢。当然就是JavaVM,因为JavaVM在进程中是唯一的,我们用JavaVM获取一个JNIEnv指针就好了。gJavaVM->AttachCurrentThread(&env, NULL); 还记得之前JNI_OnLoad函数中保存了全局的gJavaVM吧,这时候就有用武之地了。
jfieldID和jmethodID分别表示java类中的成员变量和成员函数,可以透过JNIEnv指针来访问jobject。
jfieldIDGetFieldID(jclass clazz, const char *name, const char *sig);
jmethodIDGetMethodID(jclass clazz, const char *name, const char *sig);
jclass表示java类,name表示成员变量和成员函数的名称,sig表示函数跟变量的签名信息(之后会介绍)。
譬如method的例子
static void android_bluetooth_class_init(JNIEnv* env, jobject thiz)
{
if (!gBonovoBluetoothObj)
gBonovoBluetoothObj = env->NewGlobalRef(thiz);
jclass interfaceClass = env->GetObjectClass(gBonovoBluetoothObj);
method_report =env->GetMethodID(interfaceClass, "bluetoothCallback", "(Ljava/lang/String;Ljava/lang/String;I)V");
}
在初始化函数中拿到java类的函数指针,保存到类型为jmethodID的method_report中,之后回调java的时候,就可以直接用env->CallVoidMethod(gBonovoBluetoothObj,method_report, name, js, report);
譬如field的例子
javaAudioRecordFields.nativeRecorderInJavaObj =env->GetFieldID(audioRecordClass,JAVA_NATIVERECORDERINJAVAOBJ_FIELD_NAME, "J");
在此函数中拿到java类的函数指针,保存到类型为jfieldID的javaAudioRecordFields.nativeRecorderInJavaObj中,之后回调java的时候,就可以直接用(AudioRecord*)env->GetLongField(thiz, javaAudioRecordFields.nativeRecorderInJavaObj);和env->SetLongField(thiz, javaAudioRecordFields.nativeRecorderInJavaObj, (jlong)ar.get());设置java类的成员变量。
五、JNI类型签名
因为java支持函数的重载,也就是说,可以定义同名不同参数的函数。这样只靠函数名是无法找到具体函数的,所以JNI中将参数类型和返回值类型的组合作为函数的签名信息,有了签名信息和函数名,自然就可以找到java对应的native函数了。
类型标识
举几个小例子
{"jniconnectHFP", "(Ljava/lang/String;)I", (void *)android_bluetooth_connectHFP},----------对应的java函数---------private native finalintjniconnectHFP(String addr)throws IllegalStateException;这个是注册sMethods方法时候的。
method_report = env->GetMethodID(interfaceClass, "bluetoothCallback", "(Ljava/lang/String;Ljava/lang/String;I)V");----------对应java函数-------publicvoidbluetoothCallback(String name, String number, int report)这个是取java方法时候的调用。
六、JNI异常处理
一旦JNI发生错误,必须依赖于JVM来处理异常。通过调用Throw或者ThrowNew来向JVM抛出一个异常。一个未被处理的异常会记录在当前线程中。和JAVA中的异常不同,本地代码中的异常不会立即中断当前的程序执行。
本地代码中没有标准的异常处理机制,因此,JNI程序最好在每一步可能会产生异常的操作后面都检查和处理异常。JNI程序员处理异常通常有两种方式:
1、本地方法可以选择立即返回。让代码中抛出的异常向调用者抛出。
2、本地代码可以通过调用ExceptionClear清理异常并运行自己的异常处理代码。
以上只是简单的说明了一下jni的调用,jni的使用其实有很多复杂的情形,只是我们一般无法遇到,所以基本的流程就够用大概70%的场景了,如果遇到特殊场景,就要具体事情具体分析,参考资料也很多,譬如JNI_接口规范(75页),JNI编程指南(78页),这两个资料基本就能覆盖90%的场景了。如有需要资料的请给我留言,谢谢。
领取专属 10元无门槛券
私享最新 技术干货