理解 JNI 技术

  • 2017-05-17
  • 18,060
  • 2

JNI 即Java native interface,,是一种技术,提供了丰富的接口,可以在Java层调用native代码,也可以在native层调用Java代码,native代码一般是指C/C++程序。JNI就像是一座桥,连通着Java与native。阅读Android源代码的时候可以发现其用了大量的JNI技术,如果要深入学习Android了解JNI技术是必不可少的。在追求性能,对安全性要求高或者计算密集型的模块,使用JNI技术是一种不错的选择。

本篇文章会介绍JNI技术,下篇文章将以”使用OpenCV处理图像”做为一个例子来加深对JNI技术的理解。

在Java中有个native关键字来声明该方法为native方法,调用它最终将会调用native代码,在调用该方法之前必须先调用System.loadLibrary来加载一个动态链接库,在Linux系统中动态链接库的格式一般为so,Windows系统中为dll。如果我这样加载一个动态链接库System.loadLibrary(“native_lib”),那么系统会自动根据不同的平台拓展成真实的动态库文件名,Linux系统上会拓展成libnative_lib.so,而在Windows平台上则会拓展成native_lib.dll,在Android环境下,会查找以下文件/data/app/${packagename}/lib/${cpu平台}/libnative_lib.so,如果找不到这个文件便会报无法链接的错误。如果这一步加载没问题,那我们调用以native声明的Java方法是如何寻找到native代码的呢?

比如这样一个类:


package me.pqpo;
public class NativeUtils {
static {
System.loadLibrary("native_lib");
}
public static void nativeInit();
}

如果调用NativeUtils.nativeInit();最终会调用到:


#include <jni.h>
#include <iostream>

using std::cout;
using std::endl;
extern "C"
JNIEXPORT void JNICALL
Java_me_pqpo_NativeUtils_nativeInit(JNIEnv *env, jclass type) {
cout << "nativeInit" << endl;
}

正如你所见,是按照一定规则的方法名称查找到的,Java_ + 包名(点换成下划线)+ _ + 类名 + _ + 方法名,为了方便,可以直接使用javah命令生成,初次调用native函数时,虚拟机会建立起这个映射关系。这便是静态注册。静态注册有一些弊端,比如方法名太长,建立映射关系时会影响运行效率。为解决这些问题,JNI还提供了动态注册。
下面是一个典型的动态注册过程:


static const char* const kClass = "me/pqpo/NativeUtils";

static JNINativeMethod gMethods[] = {

        {
                "nativeInit",
                "()V",
                (void*)nativeInit
        },

};

static void nativeInit(JNIEnv *env, jclass type) {
}

extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_FALSE;
    }
    jclass jclazz= env->FindClass(kClass);
    if(env -> RegisterNatives(jclazz, gMethods, sizeof(gMethods)/ sizeof(gMethods[0])) < 0) {
        return JNI_FALSE;
    }
    return JNI_VERSION_1_4;
}

当加载动态链接库之后,虚拟机会率先调用JNI_OnLoad方法,所以在这里可以做一下初始化工作,包括动态注册函数。
vm指向一个JavaVM结构体,并且是全局唯一的,代表着虚拟机的相关信息,通过它获取env,指向的是JNIEnv结构体,它非常重要,后面还会讲到,
然后获取需要注册类,类名中的点改成”/”


jclass jclazz= env->FindClass(kClassEvaluateUtil);

最后就可以注册该类的native函数了:


env -> RegisterNatives(jclazz, gMethods, sizeof(gMethods)/ sizeof(gMethods[0]))

回过头看gMethods,它是一个JNINativeMethod数组,里面包含了所有需要注册的函数


static JNINativeMethod gMethods[] = {

        {
                "nativeInit",
                "()V",
                (void*)nativeInit
        },

};

本例就注册了一个函数,第一个参数是Java层的函数名,第二个参数是方法签名信息,括号里面是参数,后面是返回值,第三个参数是c++函数指针。
这样就将Java层的函数与native函数关联起来了,其中第二个参数方法签名对应表如下:

类型标示 Java类型 类型标示 Java类型
Z boolean F float
B byte D double
C char Ljava/lang/String; String
S short [I int[]
I int [Ljava/lang/object; Object[]
J long

比如有个Java函数声明为:


public void native String getString(int num, String content);

那么对应的JNINativeMethod结构体函数签名应该为:(ILjava/lang/String)Ljava/lang/String;

解决了函数注册的问题,那么Java与native又是如何进行数据交互的呢?

以上面getString函数为例,在native层的函数声明必须符合规定的参数:


static jstring getNativeString(JNIEnv *env, jobject instance, jint num, jstring content);

第一个参数是线程相关的JNIEnv结构体,我们需要通过它来进行数据类型转换,第二个参数是Java层调用的对象(this),它是一个jobject对象,如果是静态方法,该参数会替换为jclass type,指的是调用的类
后面的参数和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.Throwable实例

jthrowable

我们拿到的是Java层的类型参数,怎么转换层C/C++可用的数据类型呢?
以上面函数的参数jstring content为例,可以通过下面方法获取native可用的字符串类型:


const char *contentStr = env->GetStringUTFChars(content, NULL);

之后别忘记释放:


env->ReleaseStringUTFChars(content, contentStr);

关于数据类型转换这里就不多说了,具体可以查看JNIEnv的api。
最后在说一下native层调用Java代码,获取及设置Java类参数,涉及到的概念包括MethodId,FieldId。
主要就是通过方法名,参数名获取id,然后通过这个id去操作相应的方法或参数。
id可以通过下面方法得到:


jfieldID Get[Static]FieldID(jclass clazz,const char*name, const char *sig);
jmethodID Get[Static]MethodID(jclass clazz, const char*name,const char *sig);

第三个参数为签名,和之前注册时填写的签名一致,不再累述。
JNIEnv提供了一系列CallMethod来调用Java方法,包括static方法:


NativeType Call[Staitc]Method(JNIEnv *env,jobject[jclass] obj,jmethodID methodID, ...)

对于参数,提供了get,set方法获取和设置Java对象的参数:


NativeType Get[Static]Field(JNIEnv *env,jobject obj,jfieldID fieldID)
void Set[Static]Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

JNIEvn结构体中还有包括将Java层数据转换成native层数组的方法,也是一系列方法,比如:


jbyte* GetByteArrayElements(jbyteArray array, jboolean* isCopy);

还有相应的释放方法:


void ReleaseByteArrayElements(jbyteArray array, jbyte* elems, jnit mode);

主要方法都在JNIEnv结构体中,读者可以自行查看。
下篇文章将以”使用OpenCV处理图像”做为一个例子来加深对JNI技术的理解。

>> 转载请注明来源:理解 JNI 技术

评论

  • wqycsu回复

    感觉文章中有两个错误:
    1、查找so不是在/data/app/${packagename}/lib/${cpu平台}/libnative_lib.so该路径下找不到就报错的,系统会兼容其它cpu平台的so,这篇文章也有详细介绍:http://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653577702&idx=1&sn=1288c77cd8fc2db68dc92cf18d675ace&scene=4#wechat_redirect
    2、表格中floatArray应该是jfloatArray吧

    • pqpo回复

      感谢提出!
      第二个为笔误已经修改。
      第一个问题应该是说的是 http://47.98.205.211/2017/05/31/system-loadlibrary/ 这篇文章吧。
      这篇文章中有这么个观点:先查找 /data/app/${packagename}/lib/${cpu平台} 目录,再查找/vendor/lib:/system/lib 这两个目录。
      这里是有点问题,系统如果找不到对应 cpu 平台的 so 文件会找兼容的 so 文件,具体路径可以查看日志里 nativeLibraryDirectories 的值。
      我写的那篇文章主要讲了 so 在 Android 平台类比 Linux 平台的加载过程,安装拷贝 so 还没深入研究过,你推荐的那篇文章写得更细。

回复给 pqpo 点击这里取消回复。