JNI是Java Native Interface的缩写,JNI是一种机制,有了它就可以在java程序中调用其他native代码,或者使native代码调用java层的代码。也 就是说,有了JNI我们可以使Android项目中,java层与native层各自发挥所长并相互配合。

JNI相对与native层来说是一个接口,java层的程序想访问native层,必须通过JNI,反过来也一样。JNI的学习,在我看来,主要可分为以下几个方面

Java基本数据类型和C语言中基本数据类型的对应关系

JAVA中调用C语言的函数

初始化加载native的动态链接库

在Java层编写函数的声明

在C/C++层编写函数的实现

编写native的成员函数

编写native的静态函数

C/C++层调用Java的函数和变量

Java中基本数据结构的对应关系

JNI为Java和C中常见的数据结构提供了常见的类型对应。他们的对应关系如下表所示

一个普通标题 一个普通标题 一个普通标题
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本

1. 如何告诉VM(虚拟机)java层需要调用native层的哪些libs?

我们知道java程序是运行在VM上的,而Native层的libs则不然。所以为了让java层能访问native层的libs,必须得告诉VM要使用哪些native层的libs。下面看一段代码

public class MediaPlayer    
{    
  ...    

  static {    
    System.loadLibrary("media_jni");    
    native_init();    
  }    
  ...    
  private native final void native_init();    
  ...    
}  

可以看到上面的代码中,在MediaPlayer类中有一段static块包围起来的代码, 其中System.loadLibrary("media_jni")就是告诉VM去加载libmedia_jni.so这个动态库,那么这个动态库什么时候被加载呢?因为static语句块的原因,所以在MediaPlayer第一次实例化的时候就会被加载了。这段代码中,我们还看到了一个函数 native_init(),该函数被修饰为native型,就是告诉VM该函数由native层来实现。

2. 如何做到java层到native层的映射。

所谓Java 层到native层的映射就是说JVM在调用 void native_init() 这个函数时,该怎么去找相应的动态链接库里面的函数。因为void native_init () 函数并没有在java文件中定义。

这里有2种办法

1) 一种是我们在C 文件中按照一定的命名规则来定义函数。

假设上面的MediaPlayer类在android.media包内。
我们的native层的代码可如下定义

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

#ifndef _Included_MediaPlayer
#define _Included_MediaPlayer
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class:   MediaPlayer
* Method:  init media player
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_android_media_MediaPlayer_native_init
 (JNIEnv *, jobject);
  
#ifdef __cplusplus
}
#endif
#endif

其中jobject参数是JVM的MediaPlayer调用native_init时的对象,JNIEnv* 是一个全局环境结构。

这个命名规则,想必很容易看懂,两个修饰符JNIEXPORT JNICALL,应该分别用来兼容so和dll的导出函数的,JNICALL是用来兼容stdcall 等这些不同的函数调用方式的。

函数名字以Java开头,后面跟着的是声明这个函数的类的全路径,包括包名,最后是函数名。

可以发现,这个函数名实在是很丑陋,难以阅读。

2) 我们可以进行动态注册

我们来看看Android源码里的MediaPlayer的native是怎么写的。

当VM执行到System.loadLibrary()的时候就会去执行native libs中的JNI_OnLoad(JavaVM* vm, void* reserved)函数,因为JNI_OnLoad函数是从java层进入native层第一个调用的方法,所以可以在JNI_OnLoad函数中完成一些native层组件的初始化工作,同时更加重要的是,通常在JNI_jint JNI_OnLoad(JavaVM* vm, void* reserved)函数中会注册java层的native方法。下面看一段代码:

jint JNI_OnLoad(JavaVM* vm, void* reserved)  
{  
    JNIEnv* env = NULL;  
    jint result = -1;  
    //判断一下JNI的版本   
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {  
        LOGE("ERROR: GetEnv failed\n");  
        goto bail;  
    }  
    assert(env != NULL);  

    if (register_android_media_MediaPlayer(env) < 0) {  
        LOGE("ERROR: MediaPlayer native registration failed\n");  
        goto bail;  
    }  

    if (register_android_media_MediaRecorder(env) < 0) {  
        LOGE("ERROR: MediaRecorder native registration failed\n");  
        goto bail;  
    }  

    if (register_android_media_MediaScanner(env) < 0) {  
        LOGE("ERROR: MediaScanner native registration failed\n");  
        goto bail;  


    if (register_android_media_MediaMetadataRetriever(env) < 0) {  
        LOGE("ERROR: MediaMetadataRetriever native registration failed\n");  
        goto bail;  
    }  

    if (register_android_media_AmrInputStream(env) < 0) {  
        LOGE("ERROR: AmrInputStream native registration failed\n");  
        goto bail;  
    }  

    if (register_android_media_ResampleInputStream(env) < 0) {  
        LOGE("ERROR: ResampleInputStream native registration failed\n");  
        goto bail;  
    }  

    if (register_android_media_MediaProfiles(env) < 0) {  
        LOGE("ERROR: MediaProfiles native registration failed");  
        goto bail;  
    }  

    /* success -- return valid version number */  
    result = JNI_VERSION_1_4;  

bail:  
    return result;  
}  

上面这段代码的 JNI_OnLoad(JavaVM* vm, void* reserved) 函数实现与 libmedia_jni.so 库中。上面的代码中调用了一些形如 register_android_media_MediaPlayer(env) 的函数,这些函数的作用是注册 native method 。我们来看看函数 register_android_media_MediaPlayer(env) 的实现。

// This function only registers the native methods  
static int register_android_media_MediaPlayer(JNIEnv *env)  
{  
    return AndroidRuntime::registerNativeMethods(env,  
                "android/media/MediaPlayer", gMethods, NELEM(gMethods));  
}
/* 
 * Register native methods using JNI. 
 */  
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,  
    const char* className, const JNINativeMethod* gMethods, int numMethods)  
{  
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);  
}  

最终jniRegisterNativeMethods函数完成java标准的native函数的映射工作。下面我们来具体的看看上面这个函数中各个参数的意义。

a. JNIEnv *env

关于JNIEnv我在google上找到了这些信息:

JNI defines two key data structures, "JavaVM" and "JNIEnv". Both of these are essentially pointers to pointers to function tables. (In the C++ version, they're classes with a pointer to a function table and a member function for each JNI function that indirects through the table.) The JavaVM provides the "invocation interface" functions,which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process,but Android only allows one.

The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.

The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.If a piece of code has no other way to get its JNIEnv, you should share the JavaVM, and use GetEnv to discover the thread's JNIEnv. (Assuming it has one; see AttachCurrentThread below.)

翻译下来是

JNI定义了2个关键的数据结构,分别是 JavaVMJNIEnv。 这两个结构里都是存放着必要的指针指向JNI的功能函数表。(在C++版本中,他们是一个类的指针,这个类带有一个指向函数表的指针成员变量 和 一个成员函数。这个成员函数为这些指针成员变量间接的调用JNI函数表。)Java虚拟机提供“invocation interface”函数,供你创建和销毁 JavaVM 变量。 理论上来说,每个进程可以同时拥有多个 JavaVM 结构体,但是 Android Darvik 虚拟机仅允许一个。

JNIEnv 提供了绝大部分的 JNI 函数。Native 函数的第一个参数都是 JNIEnv 的指针变量。

JNIEnv 是一个线程局部变量。因此,你不能在线程之间共享 JNIEnv 变量。如果一个代码没有办法获得 JNIEnv,而且它又需要时,你应该共享JavaVM变量,并且使用 JavaVM 的 GetEnv 函数来获得该段代码的执行线程的JNIEnv变量。如果该线程没有的话,则调用 JavaVM的 AttachCurrentThread函数。

这里需要注意一点的是,JNIEnv是一个线程的局部变量。JNIEnv是存在与多线程环境下的,因为 VM 通常是多线程(Multi-threading)的执行环境。每一个执行线程在呼叫JNI_OnLoad()时,所传递进来的 JNIEnv 的值都是不同的。为了配合这种多线程的环境,C/C++组件开发者在编写本地函数时,可由 JNIEnv 的值之不同而避免执行线程的资源冲突问题, 才能确保所写的本地函数能安全地在 Android 的多线程的 VM 里安全地执行。基于这个理由,JVM的JAVA代码在调用C/C++组件的函数时,都会将 JNIEnv的值传递给它。

b. char* className

这个没什么好说的,java空间中类名,其中包含了包名。

c. JNINativeMethod* gMethods

传递进去的是一个JNINativeMethod类型的指针gMethods,gMethods指向一个JNINativeMethod数组,我们先看看JNINativeMethod这个结构体。

typedef struct {  
  const char* name; /*Java 中函数的名字*/  
  const char* signature; /*描述了函数的参数和返回值*/  
  void* fnPtr; /*函数指针,指向 C 函数*/  
} JNINativeMethod;  

再来看看gMethods数组

static JNINativeMethod gMethods[] = {  
  {"setDataSource", "(Ljava/lang/String;)V",  (void *)android_media_MediaPlayer_setDataSource},  
  ...  
  {"setAuxEffectSendLevel", "(F)V", (void *)android_media_MediaPlayer_setAuxEffectSendLevel},  
  {"attachAuxEffect", "(I)V", (void *)android_media_MediaPlayer_attachAuxEffect},  
  {"getOrganDBIndex", "(II)I", (void *)android_media_MediaPlayer_getOrganDBIndex},  
};

在JNINativeMethod的结构体中,有一个描述函数的参数和返回值的签名字段,它是java中对应函数的签名信息,由参数类型和返回值类型共同组成。这个函数签名信息的作用是什么呢?

由于java支持函数重载,也就是说,可以定义同名但不同参数的函数。然而仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术中就将参数 类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能顺利的找到java中的函数了。
JNI规范定义的函数签名信息格式如下:

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

如:

"()V"  
"(II)V"  
"(Ljava/lang/String;Ljava/lang/String)V"; 

实际上这些字符是与函数的参数类型一一对应的。

  • "()" 中的字符表示参数,后面的则代表返回值。例如”()V” 就表示 void Func();
  • "(II)V" 表示 void Func(int, int);

值得注意的一点是,当参数类型是引用数据类型时,其格式是“L包名;”其中包名中的“.” 换成“/”,所以在上面的例子中(Ljava/lang/String;Ljava/lang/String;)V 表示 void Func(String,String);

如果 JAVA 函数位于一个内部类,则用$作为类名间的分隔符。
例如 “(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z”

下表是JAVA自带数据类型的对应关系

AVA类型 本地类型 对应的描述字符串 描述
boolean jboolean Z C/C++8位整型
byte jbyte B C/C++带符号8bit integer
char jchar C C/C++ 无符号16位整型
short jshort S C/C++ 带符号的16位整型
int jint I C/C++ 带符号的32位整型
long jlong J C/C++ 带符号的64位整型
float jfloat F C/C++ 32位浮点数
double jdouble D C/C++ 64位浮点数
Object jobject 任何Java对象
Class jclass Class 对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray [Z 布尔型数组
byte[] jbyteArray [B byte型数组
char[] jcharArray [C 短整型数组
short[] jshortArray [S 整型数组
int[] jintArray [I 长整型数组
long[] jlongArray [J 浮点型数组
float[] jfloatArray [F 双精度浮点数数组
double[] jdoubleArray [D
void   void V

3. Native函数调用JAVA函数

java层和JNI层应该是可以互相交互,我们通过java层中的native函数可以进入到JNI层,那么JNI层的代码能不能操作java层中函数呢?当然可以,通过JNIEnv。

先来看看两个函数原型

jfieldID GetFieldID(jclass clazz,const char *name,const char *sig );  
jmethodID GetMethodID(jclass clazz,const char *name,const char *sig);

结合前面的知识来看,JNIEnv是一个与线程相关的代表JNI环境的结构体。JNIEnv实际上提供了一些JNI系统函数。通过这些系统函数可以调用java层中的函数或者操作jobect。下面我看一段函数

class MyMediaScannerClient : public MediaScannerClient  
{  
public:  
    MyMediaScannerClient(JNIEnv *env, jobject client)  
        :   mEnv(env),  
            mClient(env->NewGlobalRef(client)),  
            mScanFileMethodID(0),  
            mHandleStringTagMethodID(0),  
            mSetMimeTypeMethodID(0)  
    {  
        jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");  
        if (mediaScannerClientInterface == NULL) {  
            fprintf(stderr, "android/media/MediaScannerClient not found\n");  
        }  
        else {  
            mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile", "(Ljava/lang/String;JJ)V");  
            mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag", "(Ljava/lang/String;Ljava/lang/String;)V");  
            mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface, "setMimeType", "(Ljava/lang/String;)V");  
            mAddNoMediaFolderMethodID = env->GetMethodID(mediaScannerClientInterface, "addNoMediaFolder", "(Ljava/lang/String;)V");  
        }  
    }  
...  

// returns true if it succeeded, false if an exception occured in the Java code  
    virtual bool scanFile(const char* path, long long lastModified, long long fileSize)  
    {  
        jstring pathStr;  
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;  

        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);  

        mEnv->DeleteLocalRef(pathStr);  
        return (!mEnv->ExceptionCheck());  
    }  
...
};
class MyMediaScannerClient : public MediaScannerClient  
{  
public:  
    MyMediaScannerClient(JNIEnv *env, jobject client)  
        :   mEnv(env),  
            mClient(env->NewGlobalRef(client)),  
            mScanFileMethodID(0),  
            mHandleStringTagMethodID(0),  
            mSetMimeTypeMethodID(0)  
    {  
        jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");  
        if (mediaScannerClientInterface == NULL) {  
            fprintf(stderr, "android/media/MediaScannerClient not found\n");  
        }  
        else {  
            mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile", "(Ljava/lang/String;JJ)V");  
            mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag", "(Ljava/lang/String;Ljava/lang/String;)V");  
            mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface, "setMimeType", "(Ljava/lang/String;)V");  
            mAddNoMediaFolderMethodID = env->GetMethodID(mediaScannerClientInterface, "addNoMediaFolder", "(Ljava/lang/String;)V");  
        }  
    }  
...  

// returns true if it succeeded, false if an exception occured in the Java code  
    virtual bool scanFile(const char* path, long long lastModified, long long fileSize)  
    {  
        jstring pathStr;  
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;  

        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);  

        mEnv->DeleteLocalRef(pathStr);  
        return (!mEnv->ExceptionCheck());  
    }  
...
};

可以看到上面的代码中,先找到java层中MediaScannerClinet类在JNI层中对应的jclass实例(通过FindClass)。然后拿到MediaScannerclient类中所需要用到函数的函数函数id(通过GetMethodID)。接着通过JNIEnv调用CallXXXMethod函数并且把对应的jobject,jMethodID还有对应的参数传递进去,这样的通过CallXXXMethod就完成了JNI层向java层的调用。这里要注意一点的是这里JNI层中调用的方法实际上是java中对象的成员函数,如果要调用static函数可以使用CallStaticXXXMethod。这种机制有利于native层回调java代码完成相应操作。

4. C++中修改Java代码中的变量

上面讲述了如下在JNI层中去调用java层的代码,那么理所当然的应该可以在JNI层中访问或者修改java层中某对象的成员变量的值。我们通过JNIEnv中的GetFieldID()函数来得到java中对象的某个域的id。看下面的具体代码


int register_android_backup_BackupHelperDispatcher(JNIEnv* env) { jclass clazz; clazz = env->FindClass("java/io/FileDescriptor"); LOG_FATAL_IF(clazz == NULL, "Unable to find class java.io.FileDescriptor"); s_descriptorField = env->GetFieldID(clazz, "descriptor", "I"); LOG_FATAL_IF(s_descriptorField == NULL, "Unable to find descriptor field in java.io.FileDescriptor"); clazz = env->FindClass("android/app/backup/BackupHelperDispatcher$Header"); LOG_FATAL_IF(clazz == NULL, "Unable to find class android.app.backup.BackupHelperDispatcher.Header"); s_chunkSizeField = env->GetFieldID(clazz, "chunkSize", "I"); LOG_FATAL_IF(s_chunkSizeField == NULL, "Unable to find chunkSize field in android.app.backup.BackupHelperDispatcher.Header"); s_keyPrefixField = env->GetFieldID(clazz, "keyPrefix", "Ljava/lang/String;"); LOG_FATAL_IF(s_keyPrefixField == NULL, "Unable to find keyPrefix field in android.app.backup.BackupHelperDispatcher.Header"); return AndroidRuntime::registerNativeMethods(env, "android/app/backup/BackupHelperDispatcher", g_methods, NELEM(g_methods)); }

获得jfieldID之后呢,我们就可以在JNI层之间来访问和操作java层的field的值了,方法如下

NativeType Get<type>Field(JNIEnv *env, jobject object, jfieldID fieldID);  

void Set<type>Field(JNIEnv *env, jobject object, jfieldID fieldID, NativeType value);

现在我们看到有了JNIEnv,我们可以很轻松的操作jobject所代表的java层中的实际的对象了。

5. jstring的介绍

之所以要把jstring单独拿出来说正是由于它的特殊性。java中String类型也是一个引用类型,但是JNI中并没有用jobject来与之对应,JNI中单独创建了一个jstring类型来表示java中的String类型。JAVA中的字符串是UTF-16的格式,而C++中一般是宽字符或者UTF-8编码甚至局部的本地编码。 显然java中的String不能和C++中的String等同起 来,那么怎么操作jstring呢?方法很多下面看几个简单的方法

  1. 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。只有这样才能让一个C++中String在JNI中使用。

  2. 调用JNIEnv的GetStringChars函数(将得到一个Unicode字符串)和GetStringUTFChars函数(将得到一个UTF-8字符串),他们可以将java String对象转换诚本地字符串。下面我们来看段示例代码。

    virtual bool scanFile(const char* path, long long lastModified, long long fileSize)  
        {  
            jstring pathStr;  
            //将char*数组字符串转换诚jstring类型  
            if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;  

            mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);  

            mEnv->DeleteLocalRef(pathStr);  
            return (!mEnv->ExceptionCheck());  
        }  

          ....  
          ....  
    while (env->CallBooleanMethod(iter, hasNext)) {  
                jobject entry = env->CallObjectMethod(iter, next);  
                jstring key = (jstring) env->CallObjectMethod(entry, getKey);  
                jstring value = (jstring) env->CallObjectMethod(entry, getValue);  

                const char* keyStr = env->GetStringUTFChars(key, NULL);  
                ...  
                ...  

GetStringUTFChars()函数将jstring类型转换成一个UTF-8本地字符串,另外如果代码中调用了上面的几个函数,则在做完相关工 作后,要调用ReleaseStringChars函数或者ReleaseStringUTFChars函数来释放资源。看下面的代码

...  
jstring key = (jstring) env->CallObjectMethod(entry, getKey);  
jstring value = (jstring) env->CallObjectMethod(entry, getValue);  

const char* keyStr = env->GetStringUTFChars(key, NULL);  
if (!keyStr) {  // Out of memory  
    jniThrowException(  
            env, "java/lang/RuntimeException", "Out of memory");  
    return;  
}  

const char* valueStr = env->GetStringUTFChars(value, NULL);  
if (!valueStr) {  // Out of memory  
    jniThrowException(  
            env, "java/lang/RuntimeException", "Out of memory");  
    return;  
}  

headersVector.add(String8(keyStr), String8(valueStr));  

env->DeleteLocalRef(entry);  
env->ReleaseStringUTFChars(key, keyStr);  
env->DeleteLocalRef(key);  
...  
...  

可以看到GetStringUTFChars与下面的ReleaseStringUTFChars对应.

总结

JNI是JVM确立的一套JAVA和C的交互机制。它提供了一些头文件和库,供C开发者使用。它也提供了一些工具供C开发者包装C代码。
在使用JNI中,主要有以下几点需要了解

  • JAVA的native函数怎么使用C函数来绑定和注册

  • C/C++函数中怎么调用JAVA层的函数或者类

  • JAVA的字符串编码和C++字符串编码的转换