Android JNI学习(三)——Java与Native相互调用

  • Post author:
  • Post category:java


本系列文章如下:

  • 1、注册native函数
  • 2、JNI中的签名
  • 3、native代码反调用Java层代码

思维导图如下:

image.png

前面两篇文章简单的介绍了JNI,下面我们就进一步了解下一下JNI的调用原则,要想了解JNI的调用原则, 前面我们说了

JNI

中的

JNIEnv

以及Java类型和native中的类型映射关系。下面我们先来看注册native函数

一、注册native函数

当Java代码中执行Native的代码的时候,首先是通过一定的方法来找到这些native方法。而注册native函数的具体方法不同,会导致系统在运行时采用不同的方式来寻找这些native方法。

JNI有如下两种注册native方法的途径:

  • 静态注册:

    先由Java得到本地方法的声明,然后再通过JNI实现该声明方法
  • 动态注册:

    先通过JNI重载JNI_OnLoad()实现本地方法,然后直接在Java中调用本地方法。

(一)、静态注册native函数

根据函数名找到对应的JNI函数;Java层调用某个函数时,会从对应的JNI中寻找该函数,如果没有就会报错,如果存在就会建立一个关联关系,以后再调用时会直接使用这个函数,这部分的操作由虚拟机完成。

静态注册就是根据函数名来遍历Java和JNI函数之间的关联,而且要求JNI层函数的名字必须遵循特定的格式。具体的实现很简单,首先在Java代码中声明native函数,然后通过javah来生成native函数的具体形式,接下来在JNI代码中实现这些函数即可。

举例如下:

public class JniDemo1{ static { System.loadLibrary("samplelib_jni"); } private native void nativeMethod(); } 

接来下通过

javah

来产生jni代码,假设你的包名为

com.gebilaolitou.jnidemo

javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.gebilaolitou.jnidemo.JniDemo1

然后就会得到一个JNI的.h文件,里面包含这几个native函数的声明,观察一下文件名以及函数名。其实JNI方法名的规范就出来了:

返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数

:注意事项:

  • 注意分隔符:

    Java前缀与

    类名

    以及

    类名之间的包名

    和方法名之间使用”_”进行分割;
  • 注意静态:

    如果在Java中声明的方法是”静态的”,则native方法也是static。否则不是
  • 如果你的JNI的native方法不是通过

    静态注册

    方式来实现的,则不需要符合上面的这些规范,可以格局自己习惯随意命名

(二)、动态注册native函数

上面我们介绍了静态注册native方法的过程,就是Java层声明的nativ方法和JNI函数一一对应。以我来说,刚开始做JNI的前期,可能会遵守静态注册的流程:1、编写带有native方法的Java类,2、使用Javah命令生成.h头文件;3、编写代码实现头文件中的方法,这样的单调的标准流程,而且还要忍受这么”长”的函数名。那有没有更简单的方式呢?比如让Java层的native方法和任意JNI函数连接起来?答案是有的——动态注册,也就是通过

RegisterNatives

方法把C/C++中的方法映射到Java中的native方法,而无需遵循特定的方法命名格式。

当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个

JNI_OnLoad

函数兵调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于

Android

中的

Activity

中的

onCreate()

方法。该函数前面也有三个关键字分别是

JNIEXPORT



JNICALL



jint

。其中

JNIEXPORT



JNICALL

是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层,这块前面已经介绍过了,我这里就不唠叨了。

PS:与JNI_OnLoad()函数相对应的有JNI_OnUnload()函数,当虚拟机释放的该C库的时候,则会调用JNI_OnUnload()函数来进行善后清除工作。

该函数会有两个参数,其中

*jvm

为Java虚拟机实例,

JavaVM

结构体定义一下函数:

DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv

下面我们就举例说明

举例说明,首先是加载so库

public class JniDemo1{ static { System.loadLibrary("samplelib_jni"); } } 

在jni中的实现

jint JNI_OnLoad(JavaVM* vm, void* reserved) 

并且在这个函数里面去动态的注册native方法,完整的参考代码如下:

#include <jni.h>
#include "Log4Android.h" #include <stdio.h> #include <stdlib.h> using namespace std; #ifdef __cplusplus extern "C" { #endif static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2"; static void sayHello(JNIEnv *env, jobject, jlong handle) { LOGI("JNI", "native: say hello ###"); } static JNINativeMethod gJni_Methods_table[] = { {"sayHello", "(J)V", (void*)sayHello}, }; static int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { jclass clazz; LOGI("JNI","Registering %s natives\n", className); clazz = (env)->FindClass( className); if (clazz == NULL) { LOGE("JNI","Native registration unable to find class '%s'\n", className); return -1; } int result = 0; if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) { LOGE("JNI","RegisterNatives failed for '%s'\n", className); result = -1; } (env)->DeleteLocalRef(clazz); return result; } jint JNI_OnLoad(JavaVM* vm, void* reserved){ LOGI("JNI", "enter jni_onload"); JNIEnv* env = NULL; jint result = -1; if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { return result; } jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod)); return JNI_VERSION_1_4; } #ifdef __cplusplus } #endif 

我们一个个来说,首先看

JNI_OnLoad

函数的实现,里面代码很简单,主要就是两个代码块,一个是if语句,一个是jniRegisterNativeMethods函数的实现。那我们一个一个来分析。

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
    return result ;
}

这里调用了GetEnv函数时为了获取JNIEnv结构体指针,其实JNIEnv结构体指向了一个函数表,该函数表指向了对应的JNI函数,我们通过这些JNI函数实现JNI编程。

然后就调用了

jniRegisterNativeMethods

函数来实现注册,这里面注意一个静态变量

gJni_Methods_table

。它其实代表了一个

native方法的数组

,如果你在一个Java类中有一个native方法,这里它的size就是1,如果是两个native方法,它的size就是2,大家看下我这个

gJni_Methods_table

变量的实现

static JNINativeMethod gJni_Methods_table[] = {
    {"sayHello", "(J)V", (void*)sayHello}, }; 

我们看到他的类型是JNINativeMethod ,那我们就来研究下JNINativeMethod

JNI允许我们提供一个函数映射表,注册给Java虚拟机,这样JVM就可以用函数映射表来调用相应的函数。这样就可以不必通过函数名来查找需要调用的函数了。Java与JNI通过JNINativeMethod的结构来建立联系,它被定义在jni.h中,其结构内容如下:

typedef struct { 
    const char* name; const char* signature; void* fnPtr; } JNINativeMethod; 

这里面有3个变量,那我们就依次来讲解下:

  • 第一个变量

    name

    ,代表的是Java中的

    函数名
  • 第二个变量

    signature

    ,代表的是Java中的

    参数和返回值
  • 第三个变量

    fnPtr

    ,代表的是的

    指向C函数的函数指针

下面我们再来看下

jniRegisterNativeMethods

函数内部的实现

static int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { jclass clazz; LOGI("JNI","Registering %s natives\n", className); clazz = (env)->FindClass( className); if (clazz == NULL) { LOGE("JNI","Native registration unable to find class '%s'\n", className); return -1; } int result = 0; if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) { LOGE("JNI","RegisterNatives failed for '%s'\n", className); result = -1; } (env)->DeleteLocalRef(clazz); return result; } 

首先通过

clazz = (env)->FindClass( className);

找到声明native方法的类

然后通过调用

RegisterNatives函数

将注册函数的Java类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定。

上面在讲解

JNINativeMethod

结构体的时候,提到一个概念,就是”signature”即签名,这个是什么东西?我们下面就来讲解下。

二、JNI中的签名

(一)、为什么JNI中突然多出了一个概念叫”签名”?

因为Java是支持函数重载的,也就是说,可以定义相同方法名,但是不同参数的方法,然后Java根据其不同的参数,找到其对应的实现的方法。这样是很好,所以说JNI肯定要支持的,那JNI要怎么支持那,如果仅仅是根据函数名,没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——”签名”,即将参数类型和返回值类型的组合。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。

(二)、如果查看类中的方法的签名

可以使用

javap

命令:

javap -s -p MainActivity.class

Compiled from "MainActivity.java"
public class com.example.hellojni.MainActivity extends android.app.Activity { static {}; Signature: ()V public com.example.hellojni.MainActivity(); Signature: ()V protected void onCreate(android.os.Bundle); Signature: (Landroid/os/Bundle;)V public boolean onCreateOptionsMenu(android.view.Menu); Signature: (Landroid/view/Menu;)Z public native java.lang.String stringFromJNI(); //native 方法 Signature: ()Ljava/lang/String; //签名 public native int max(int, int); //native 方法 Signature: (II)I //签名 } 

我们看到上面有

()V



(Landroid/os/Bundle;)V



(Landroid/view/Menu;)Z



(II)I

我们一脸懵逼,这是什么鬼,所以我们要来研究下签名的格式

(三)、JNI规范定义的函数签名信息

具体格式如下:

(参数1类型标示;参数2类型标示;参数3类型标示…)返回值类型标示

当参数为引用类型的时候,参数类型的标示的根式为”L包名”,其中包名的

.

(点)要换成”/”,看我上面的例子就差不多,比如

String

就是

Ljava/lang/String



Menu



Landroid/view/Menu

类型标示 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double

如果是基本类类型,其签名如下:

类型标示 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double

这个 其实很好记的,除了boolean和long,其他都是首字母大写。

如果返回值是void,对应的签名是

V



这里重点说1个特殊的类型,一个是数组及Array

类型标示 Java类型
[签名 数组
[i int[]
[Ljava/lang/Object String[]

三、native代码反调用Java层代码

上面讲解了如何从JNI中调用Java类中的方法,其实在jni.h中已经定义了一系列函数来实现这一目的,下面我们就以此举例说明:

(一)、获取

Class

对象

为了能够在C/C++中调用Java中的类,

jni.h

的头文件专门定义了jclass类型表示Java中Class类。JNIEnv中有3个函数可以获取jclass。

  • jclass FindClass(const char* clsName):

    通过类的名称(类的全名,这时候包名不是用'”.”点号而是用”/”来区分的)来获取jclass。比如:
jclass jcl_string=env->FindClass("java/lang/String");

来获取Java中的String对象的class对象

  • jclass GetObjectClass(jobject obj):

    通过对象实例来获取jclass,相当于Java中的getClass()函数
  • jclass getSuperClass(jclass obj):

    通过jclass可以获取其父类的jclass对象

(二)、获取属性方法

在Native本地代码中访问Java层的代码,一个常用的常见的场景就是获取Java类的属性和方法。所以为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性的时候,首先就要现在本地代码中取得代表该Java类的属性的jfieldID,然后才能在本地代码中进行Java属性的操作,同样,在需要调用Java类的某个方法时,也是需要取得代表该方法的jmethodID才能进行Java方法操作。

常见的调用Java层的方法如下:

一般是使用JNIEnv来进行操作

  • GetFieldID/GetMethodID:获取某个属性/某个方法
  • GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法

方法的具体实现如下:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 

大家发现什么规律没有?对了,我们发现他们都是4个入参,而且每个入参的都是

*JNIEnv *env



jclass clazz



const char *name



const char *sig

。关于

JNIEnv

,前面我们已经讲过了,这里我们就不详细讲解了,

JNIEnv

代表一个JNI环境接口,

jclass

上面也说了代表Java层中的”类”,

name

则代表方法名或者属性名。那最后一个

char *sig

代表什么?它其实代表了JNI中的一个

特殊字段

——签名,上面已经讲解过了。我们这里就不在冗余了。

(三)、构造一个对象

常用的JNI中创建对象的方法如下:

jobject NewObject(jclass clazz, jmethodID methodID, ...)

比如有我们知道Java类中可能有多个构造函数,当我们要指定调用某个构造函数的时候,会调用下面这个方法

jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);

即把指定的构造函数传入进去即可。

现在我们来看下他上面的二个主要参数

  • clazz:是需要创建的Java对象的Class对象
  • methodID:是传递一个方法ID,想一想Java对象创建的时候,需要执行什么操作?就是执行构造函数。

有人会说这要走两行代码,有没有一行代码的,是有的,如下:

jobject NewObjectA(JNIEnv *env, jclass clazz, 
jmethodID methodID, jvalue *args);

这里多了一个参数,即

jvalue *args

,这里是

args

代表的是对应构造函数的所有参数的,我们可以应将传递给构造函数的所有参数放在jvalues类型的数组args中,该数组紧跟着放在methodID参数的后面。NewObject()收到数组中的这些参数后,将把它们传给编程任索要调用的Java方法。

上面说到,参数是个数组,如果参数不是数组怎么处理,jni.h同样也提供了一个方法,如下:

jobject NewObjectV(JNIEnv *env, jclass clazz, 
jmethodID methodID, va_list args);

这个方法和上面不同在于,这里将构造函数的所有参数放到在va_list类型的参数args中,该参数紧跟着放在methodID参数的后面。

上一篇文章

Android JNI学习(二)——实战JNI之“hello world”


下一篇文章

Android JNI学习(四)——JNI的常用方法的中文API

作者:隔壁老李头

链接:https://www.jianshu.com/p/b71aeb4ed13d

来源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

转载于:https://www.cnblogs.com/Free-Thinker/p/10601042.html