在 Android 开发过程中,有时为了提升算法效率,往往采用 JNI 的方式调用 C/C++ 程序,这里主要总结了 Java 调用 Native 和 Native 调用 Java 的交互方式和实现原理。
Java 调用 Native
加载动态库
Android 虚拟机为每个进程分配一个 JavaVM 实例,在 Java 层中通过如下方式加载动态库:
1 | static{ |
在 JNI 中以下函数会被调用:
1 | jint JNI_OnLoad(JavaVM* vm, void* reserved){ |
这里可以直接获取 JavaVM 的实例 vm,然后调用 GetEnv()
获取当前线程对应的 JNI 指针 JNIEnv。JNIEnv 是一个与线程相关的,代表 JNI 环境的结构体,可以通过它调用 Java 方法和操作 jobject 对象(jobject 对象代表的是传入 JNI 层的 Java 对象)。
动态注册本地函数
在 JNI_OnLoad 中会进行初始化工作,比如,先查找对应路径下的类对象,再向 JVM 注册类对象中使用的本地方法,以此形成一个映射表:
1 | jint JNI_OnLoad(JavaVM* vm, void* reserved){ |
调用 JNI 提供的 RegisterNatives 函数,将本地的函数映射表注册到 JVM 中,这样 JVM 就可以根据函数映射表来调用相应的本地函数。
我们知道 Java 调用 JNI 还有一种方式,就是按照 JNI 规范的命名规则定义 JNI 函数:
1 | JNIEXPORT JNI返回类型 JNICALL java_完整包名_类名_方法名(); |
这种静态注册的方式没有实现 JNI_OnLoad()
函数,初次调用时需要根据函数名在 JNI 层中搜索对应的本地函数,然后调用 ResolveNativeMethod
建立对应关系,这个过程比较耗时,因此程序运行效率相对动态注册而言较低。
Native 调用 Java
寻找类对象
调用 FindClass 函数,传入一个 Class 描述符(Engine 类的描述符是 com/example/Engine),JVM 会从 classpath 路径下搜索该类,并返回 jclass 类型。
1 | jclass clazz = (*env)->FindClass(env, classPathName); |
获取方法 ID
使用GetMethodID
获取类的方法 ID,返回类型是 jmethodID,静态方法需要使用GetStaticMethodID
获取。因为 JVM 会为每个注册的 Java 方法设定一个 ID,所以这里获取方法 ID 后可以进一步获取对应的方法。
1 | jmethodID g_getPath = getStaticMethodIDCheck(env, g_Engine_class, "getPathStatic", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); |
调用对象方法
传入类对象,对象方法的 jmethodID,通过 callMethod 或 callStaticMethod 调用 ID 对应的 Java 方法,JVM 针对所有数据类型的返回值都定义了相关的函数。例如调用类的静态方法,CallStaticIntMethod、CallStaticFloatMethod、CallStaticObjectMethod 分别对应调用返回是 int,float,Object 类型的方法,对于返回的类型不是基本类型,而是指向对象的引用类型时,统一使用 CallStaticObjectMethod 调用。
1 | JNIEnv* env; |
如上所述,JNIEnv
是与线程相关的,使用它调用 Java 方法和操作 jobject 对象时需要先使用 JVM 的实例绑定当前线程。然后就可以调用到 Java 层 Engine 类的 getPathStatic 方法。