Android JNI 介绍

一:JNI 概述

        JNI全称为Java Native Interface,它能够使在Java虚拟机中运行的Java代码去操作用其他异于Java的编程语言(如C/C++所编写的程序和库。通俗地说,JNI是一种技术,通过这种技术可以做到如下两点:

   1. Java 程序中的函数可以调用Native 语言写的函数,Native一般指C/C++编写的函数。

   2. Native程序中的函数可以调用java层的函数,也就是说在C/C++程序中可以调用Java 函数。

       在Android 平台上,JNI就是一座将Native 世界和java世界之间通途的桥,并在大量使用,如surface、mediaPlayer 等等。看图1-1展示了Android 平台上JNI所处的位置:

                 

                   图1-1 Android 平台中JNI的示意图

    

由上图可知,JNIjavaNative空间紧密联系在一起了。在很多linux版本应用移植到Android 平台上也可能使用JNI,如一下视频播放器可以通过JNI无缝过渡到Android平台上来。


二:JNI 函数注册

         在介绍JNI函数注册前,先介绍两个JNI使用中重要的概念:JavaVMJNIEnvJavaVMJNIEnv 本质上都是指向函数表的指针的指针,在C++版本中,它们被定义成类,类里面包含一个指向函数表的指针。具体参见头文件定义dalvik\libnativehelper\include\nativehelper\jni.h。JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有线程都可以使用这个JavaVM

        JNI方法是Android应用程序与本地操作系统直接进行通信的一个手段,这就需要将JAVAC/C++函数注册到JNIEnv中,从下面代码段可以看出注册过程。


Java 程序:

package com.example.test.app;
public class MiddleWare {
    ... ...
   static {
		System.loadLibrary("testmw_jni");
		nativeinit();
	}
	
	... ...
	public native void openUrl(String urlStr);
	... ...
	public void browserUrl(String url) {
	  if(StringUtils.isEmpty(url)){
	    return;
	  }
	  openUrl(url);
	}
	......
}
C++ 程序:

static void _MiddlewareJNI_Native_Init(JNIEnv *env)
{
   
    if (pthread_key_create(&g_EnvKey, _JNI_ThreadDestructor))
        return ;
    pthread_setspecific(g_EnvKey, env);
    jclass clazz;
    if (!(clazz = env->FindClass(TEST_CLASS_PATH_NAME)))
        return ;
    g_InstanceID = env->GetFieldID(clazz, "mNativeContext", "I");
}


static void _MiddlewareJNI_openUrl(JNIEnv *env, jobject thiz, jstring jUrl)
{
   
    MiddlewareJNI* mv = (MiddlewareJNI*)env->GetIntField(thiz, g_InstanceID);
    if (mv) {
        const char* url = env->GetStringUTFChars(jUrl, 0);
        if (url) {
            mv->openUrl(url);
            env->ReleaseStringUTFChars(jUrl, url);
        }
    }
}

static JNINativeMethod sMethods[] =
{
    {"nativeinit",             "()V",                                      (void*)_MiddlewareJNI_Native_Init},
    {"openUrl",                "(Ljava/lang/String;)V",                   (void*)_MiddlewareJNI_openUrl}
};


jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    jclass clazz = NULL;
    JNIEnv* env = NULL;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4) != JNI_OK)
        return JNI_ERR;

    if (!(clazz = env->FindClass(TEST_CLASS_PATH_NAME)))
        return JNI_ERR;

    env->RegisterNatives(clazz, sMethods, sizeof(sMethods) / sizeof(sMethods[0]));
    g_JavaVM = vm;
    return JNI_VERSION_1_4;
}

        涉及到一些项目信息,为不体现具体项目内容,本文的例子都做过相关修改。以上代码段是测试实现的例子,Test应用实现成一个testmw_jni so文件中。因此,在该JNI方法能够被调用之前,我们首先要将它加载到当前应用程序进程来,这是通过调用System类的静态成员函数loadLibrary来实现的。libtestmw_jni .so文件被加载的时候,函数JNI_OnLoad就会被调用。在函数JNI_OnLoad中,参数vm描述的是当前进程中的Dalvik虚拟机,通过调用它的成员函数GetEnv就可以获得一个JNIEnv对象。有了这JNIEnv对象之后,我们就可以调用另外一个函数RegisterNatives来向当前进程中的Dalvik虚拟机注册JNI方法,如上面代码段描述的nativeinit注册过程,将java层的nativeinit() 函数与C/C++ 中的_MiddlewareJNI_Native_Init函数对应起来了。

        从上述代码段中注册过程可以看出,JAVA层的函数前带有Java 关键字native,它表示对应函数将由JNI层来实现。在JNI层中与之对应的函数都使用static函数。

2.1 JNI数据类型转换规则

         在java调用Native函数传递的参数是java数据类型,那么这些参数类型到了JNI会如何对应需要遵守JNI数据类型转换规则。Java 数据类型分为基本数据类型和引用数据类型两种,JNI层也是区分对待这二者的。详见表2-1 及表2-2

               表2-1,基本数据类型的转换关系表

JAVA

Native 类型

符号属性

字长

boolean

jboolean

无符号

8

byte

jbyte

无符号

8

char

jchar

无符号

16

short

jshort

有符号

16

int

jint

有符号

32

long

jlong

有符号

64

float

jfloat

有符号

32

double

jdouble

有符号

64

                     表2-2 Java 引用数据类型转换表

Java引用类型

Native 类型

Java引用类型

Native类型

All objects

jobject

char[]

jcharArray

java.lang.Class 实例

jclass

short[]

jshortArray

java.lang.String 实例

jstring

int[]

jintArray

Object[]

jobjectArray

long[]

jlongArray

boolean[]

jbooleanArray

float[]

jfloatArray

byte[]

jbyteArray

double[]

jdoubleArray

java.lang.Trowable实例

jthrowable

 

 

 

2.2 JNI命名规则与签名规则

        JNI实现方法与Java声明方法是不同的,例如:Java层声明的Native方法名是openUrl,而其对应的JNI实现方法的方法名却是_MiddlewareJNI_openUrl。可见,除了数据类型有对应关系外,方法名也有对应关系。JNI 接口指针是JNI实现方法的第一个参数,其类型是JNIEnv。第二个参数因本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对Java对象的引用,而静态本地方法的第二个参数是对其 Java 类的引用,其余的参数都对应于Java 方法的参数。JNI规范里提供了JNI实现方法的命名规则,方法名由以下几部分串接而成:
        Java_前缀
        全限定的类名
       下划线(_)分隔符
       增加第一参数JNIEnv* env
       增加第二个参数jobject
       其他参数按类型映射
       返回值按类型映射

 再看一段注册代码:

static JNINativeMethod sMethods[] =

{

    ......

     {"openUrl",   "(Ljava/lang/String;)V",    (void*)_MiddlewareJNI_openUrl},

    ......

}

其中"(Ljava/lang/String;)V"字符是签名信息,有参数类型和返回值共同组成。之所以要签名是因为Java支持函数重载,可以定义相同的函数,不同的参数。仅仅通过函数名没有办法找到具体函数,为了解决这一问题,JNI技术将参数类型和返回值类型组合成一个函数的签名信息,有了签名信息和函数名,很顺利能找到java中的函数了。

JNI规范定义的签名格式如下:

(参数1类型标示参数2类型标示... 参数n类型标示)返回值类型标示

常见的类型标示示意如表2-3java还存在其他签名的方式,如函数签名,这里不一一举例。

                    表2-3 类型标识示意图

 类型标示

Java类型

类型标示

Java类型

Z

boolean

F

float

B

byte

D

double

C

char

L/java/langaugeString

String

S

short

[I

int[]

I

int

[L/java/lang/object

Object[]

J

long

 

 

 

2.3. 正向和反向通信

    JNI是连通java层与C/C++层的桥梁,存在双向通信,正向java层调用C/C++ 层,反向C/C++调用java层。 Java 调用native 方法比较简单,framework中也有不少参考代码。 上述代码段也给出了示例,Java层调用native方法openUrl,JVM将会调用到native的_MiddlewareJNI_openUrl方法,其中在native method中,JNIEnv作为第一个参数传入,通过JNIEnv获取相应对象来处理相关实现。

    反向通信是很容易使用错误,JNI开发最常见的错误就是滥用了JNIEnv接口。需要强调的是JNIEnv是跟线程相关的,而JavaVM是进程相关要用的时候在不同线程中再通过JavaVM *jvm的方法来获取与当前线程相关的JNIEnv*。

     正如前面所述,不论进程中有多少线程,JavaVM进程只有一份,JNIEnv与JavaVM关系如下:

     1.调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体,这样就可以在后台线程中回调java函数。

     2.在后台线程退出前,需要调用JavaVM 的DetachCurrentThread 函数来释放对应的资源。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章