android入门-JNI

做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%的场景了。如有需要资料的请给我留言,谢谢。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180421G0OPXS00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券