1. 概述

在上一章节JNI—NDK开发流程(ndk-build与CMake)中讲述了NDK的开发流程,但是还遗留两个问题:

C/C++ 与 Java如何进行通信的?
如何阅读Android Native 源码?

今天来解决第二个问题C/C++与Java 如何进行通信的?

2. 数据类型与描述符

2.1. 数据类型

非常多博文讲述了JNI的数据类型与JAVA语言数据类型的映射关系,但是为什么JNI还需要定义一套本地数据类型呢。JNI定义的数据类型起到了衔接C/C++ 与JAVA的作用。 C/C++ 与JAVA属于两种不同的语言,运行在不同的平台上,也就是说C/C++ 是没有办法识别JAVA中的类型的,比如C/C++ 的整型int与Java整形int无法直接互换直接使用的。那么JNI内部再定义一套数据类型映射C/C++与JAVA之间的数据类型。

java 怎么跟C消息交互 java c++交互_jni.h


看上篇文章(文末有例子的链接)的例子:

JAVA方法:

public native String setString(String text);

JNI 方法:

JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){
    char* jnistr = (char *) env->GetStringUTFChars(str, NULL);
    strcat(jnistr,": I am JNI");
    return env -> NewStringUTF(jnistr);
}

JAVA Native方法的String类型在JNI变成jstring类型,而C/C++ 也无法识别jstring,需要通过env提供的函数指针转换成C/C++ 可使用数据类型char。可见JNI定义的数据类型是C/C++ 与JAVA的衔接媒介。 再来看看JNI数据类型与JAVA数据类型的映射关系表:

基本数据类型:

Java数据类型

JNI数据类型

描述

boolean

jboolean

C/C++无符号8为整数

byte

jbyte

C/C++有符号8位整数

char

jchar

C/C++无符号16位整数

short

jshort

C/C++有符号16位整数

int

jint

C/C++有符号32位整数

long

jlong

C/C++有符号64位整数

float

jfloat

C/C++32位浮点数

double

jdouble

C/C++64位浮点数

引用数据类型:

Java引用数据类型

JNI应用类型

描述

Object

jobject

可以表示任何Java的对象,或者没有JNI对应类型的Java对象

String

jstring

Java的String字符串类型的对象

Class

jclass

Java的Class类型对象(静态方法的强制参数)

Object[]

jobjectArray

Java任何对象的数组表示形式

boolean[]

jbooleanArray

Java基本类型boolean的数组表示形式

byte[]

jbyteArray

Java基本类型byte的数组表示形式

char[]

jcharArray

Java基本类型char的数组表示形式

short[]

jshortArray

Java基本类型short的数组表示形式

int[]

jintArray

Java基本类型int的数组表示形式

long[]

jlongArray

Java基本类型long的数组表示形式

float[]

jfloatArray

Java基本类型float的数组表示形式

double[]

jdoubleArray

Java基本类型double的数组表示形式

void

void

2.2. 描述符

JNI中还有一个概念,描述符,JVM虚拟机中使用描述符来存储数据类型,然后利用该描述符可以定位、调用相关class的参数和方法等。说通俗一点,描述符是JAVA数据类型在JVM中的符号表示,比如java整型 int 在JVM中的表示为 I。 描述符常常用于JNI中C/C++ 代码访问JAVA类或方法,后面第4节注册方式讲述JNI中C/C++如何获取JAVA方法和类。

基本数据类型描述符:

描述符

JAVA类型

Z

boolean

B

byte

C

char

S

short

I

int

J

long

F

float

D

double

V

void

应用数据类型描述符:

L+类全限定名称+;

例如:String:Ljava/lang/String;

方法描述符:

(<形参描述符>)+返回类型描述符

例如:
void test() : ()V
String getString(String key) : (Ljava/lang/String;)Ljava/lang/String;

3. jni.h——粘合剂

C/C++ 与JAVA的交互函数指针env以及上面讲述的JNI数据类型都是在jni.h中定义。jni.h 定义了C/C++ 与JAVA可以交互的能力的头文件。 并适配了C 与 C++的差异。
上面JNI与JAVA基本类型的映射关系不要记忆,在jni.h文件中查询可获知。一起看看这个文件:
Android P源码目录 : android\libnativehelper\include_jni\jni.h 或
在Demo中cpp文件右击进入查看jni.h文件

#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;


#else /* not __cplusplus */

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;

#endif /* not __cplusplus */

JNIEnv结构体:

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif


struct JNINativeInterface {
    .....
    jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);
    jbyteArray    (*NewByteArray)(JNIEnv*, jsize);
    jcharArray    (*NewCharArray)(JNIEnv*, jsize);
    jshortArray   (*NewShortArray)(JNIEnv*, jsize);
    jintArray     (*NewIntArray)(JNIEnv*, jsize);
    .....
}

JNIEnv结构体定义了一些系列函数指针,通过该函数指针列表C/C++ 可与JAVA进行交互,并且C 与 C++的调用方式有点不一样(一个是指针的指针,一个指针本身)。至于每个函数指针的具体实现就不讲述了,其中涉及到JVM虚拟机的实现细节了。

4. 注册方式

第2和第3节都讲述了JNI中一些遵循的规则,但是JAVA中调用Native方法是如何找到对应的JNI的方法。其实JVM映射对应的JNI方法主要由两种方式:静态注册和动态注册。

4.1. 静态注册

静态注册通过方法名建立起Java方法与JNI方法的映射关系。
静态注册的JNI方法中JNIEXPORT和JNICALL表明该函数是JNI函数,链接时通过方法名链接到对应的JAVA方法。比如Demo中JNI方法Java_com_wuzl_jnitest_HelloWorldJNI_setString对应JAVA的com.wuzl.jnitest.HelloWorldJNI.setString方法。其中Java中“.”都被替换成下划线“_”
JNI 方法:

JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){
    char* jnistr = (char *) env->GetStringUTFChars(str, NULL);
    strcat(jnistr,": I am JNI");
    return env -> NewStringUTF(jnistr);
}

但是发现有个弊端,那就是静态注册中JNI方法过长并且复杂,导致代码可读性降低并在编码的过程中容易出错,并且无法定义方法名。那么动态注册恰好解决了静态注册的弊端并可以自定义方法名。

4.2. 动态注册

在编写JNI方法时,利用结构体JNINativeMethod记录JNI与JAVA方法的映射关系,然后实现JNI_OnLoad方法,在该方法中调用JNI的函数指针注册到JVM虚拟机。 结构体JNINativeMethod和JNI_OnLoad方法在jni.h中被定义了, JNINativeMethod为JAVA方法和JNI函数的映射关系表。

typedef struct {
    const char* name; /*java 方法名*/
    const char* signature; /*java 方法描述符*/
    void*       fnPtr; /*Native 函数指正*/
} JNINativeMethod;

.....
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
.....
  • 创建JNI函数
    JAVA方法映射的JNI函数。
jstring sayHello(JNIEnv *env, jobject obj, jstring str){
        char *name = (char *) env->GetStringUTFChars(str, NULL);

        strcat(name,":say hello ~~~~~");
        return env -> NewStringUTF(name);
    }
  • 编写JAVA Native方法
public native String sayHi(String text);
  • C/C++源文件中实现JNI_OnLoad方法
    通过System.loadLibarary()加载so库时,JAVA虚拟机会判断是否实现了该函数,若有则执行该函数,类似JAVA代码中的初始化函数。
    其中JavaVM *vm可以理解为JAVA 虚拟机指针,一个进程只有一个JavaVM对象,但可有多个JNIEnv对象。然后通过JNIEnv和JAVA代码相互通信。
  • 获取JNIEnv结构体指针
    通过JavaVM指针获取JNIEnv结构体指针。
  • 构建JNINativeMethod数组
    JNINativeMethod数组为JAVA方法和JNI方法的映射关系表。
  • 获取jclass对象
    通过类的全路径获取jclass对象,主要使用了JNIEnv结构体的函数FindClass。
  • 注册到虚拟机
    通过RegisterNative函数指针注册到JVM虚拟机。
jint JNI_OnLoad(JavaVM *vm, void *reserved) {

        JNIEnv *env = NULL;

        /*1. 通过JavaVM 虚拟机获取JNIEnv结构体指针,最终需要通过JNIEnv与Java虚拟机打交道和Java方法进行通信*/
        if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
            return JNI_EVERSION;
        }

        /*2. 构建JNINativeMethod数组(JAVA方法和JNI方法的映射关系表)*/
        static JNINativeMethod methods_table[] = {
                {"sayHi", "(Ljava/lang/String;)Ljava/lang/String;", (void *) sayHello},
        };

        /*3. 通过类路径查找jclass对象*/
        jclass clazz;
        clazz = (env)->FindClass("com/wuzl/jnitest/HelloWorldJNI");

        if (clazz == NULL) {
            return JNI_ERR;
        }

        /*4. 调用RegisterNative注册到JVM虚拟机,形成映射,参数三为注册native方法的数量*/
        if ((env)->RegisterNatives(clazz, methods_table, sizeof(methods_table)/sizeof(methods_table[0])) < 0 ) {
            return JNI_ERR;
        }

        (env)->DeleteLocalRef(clazz);

        return JNI_VERSION_1_4;
    }

5. 总结

C/C++通过JNIEnv与JAVA进行打交道,以及jni.h粘合剂的作用,同时JNI通过静态注册和动态注册构建JAVA与JNI方法的映射关系。在Android 源码分析过程中,会发现大量动态注册的手法,为后面如何阅读Android Native源码打下了基础。


Demo:https://github.com/PlepleLiang/JNIDemo

参考:
Android JNI编程—JNI基础Android NDK开发:JNI实战篇