胡琪

为今天工作,为明天投资,为未来孵化一些东西

安卓NDK开发之JNI函数动态注册

安卓NDK开发之Hello JNI 中相信大家已经注意到,AS向导为我们自动生成的NDK工程的JNI函数中,和其对应的java层函数相比,除了参数会自动增加JNIEnv *和 jobject这2个类型的参数以外,函数的名称也发生了变化,如下所示:

首先增加了JNICALL修饰符,其次函数的名称也不再是getString,而是在前面追加了一些其他内容,事实上,如过你多观察几个函数就会发现,前面追加的内容不是随意的,而是符合一定规则的,这个规则就是JNI函数注册命名规则:

Java_完整包名_类名_方法名 //其中包名的.分隔符要用下划线分隔符_替代

其实仔细想想也对,当我们在java层调用一个native函数时逻辑就会进入到so中,但是虚拟机如何知道这个java层定义的函数在so中叫什么名称呢?所以肯定需要一套java层函数和c/c++层函数映射的机制,这个机制就是JNI的函数注册机制。上面AS向导生成的JNI代码中已经依据该机制自动为我们生成了.h文件中的函数名称,所以我们没感觉到这套机制的存在。这套机制可以分为静态注册和动态注册2大类。其中AS向导为我们自动生成的.h文件的形式就属于静态注册方式。而实际开发过程中静态注册实际使用的比较少,更多的时候是使用动态注册的方式,下面重点讲解JNI函数的动态注册。

JNI函数的动态注册

JNI_OnLoad

提到JNI函数的动态注册就不得不讲下JNI_OnLoad函数了,该函数定义如下:

其各个参数及意义如下:

  • JavaVM*vm:指向JavaVM对象的指针,JavaVM可以理解为虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,该对象是线程共享的。有了JVM指针就可以使用其如下方法获取到JNIEnv指针,有了JNIEnv指针就可以调用各种JNI的API接口了。
  • void *reserved:保留字段(题外话:从这里可以学习到为了以后方便扩展函数功能,可以在函数中先预留一些保留字段)

jni官方文档对该函数的说明可以参看:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#JNJI_OnLoad

一般而言JNI函数动态注册操作就是放到JNI_OnLoad函数中的,如下所示:

该操作基本可以看作是一个模板代码,一般无需修改其内容,只需要定义一个registerNativeMethods的方法,在该方法中完成native方法注册即可。具体见后面一小节。

那么为何放到JNI_OnLoad函数中进行注册呢?其原因是什么呢?这就涉及到java的类加载时模块加载顺序和so加载原理了。

在java中当某个类被加载的时候,静态代码块的执行是早于该类的构造函数的(所以更加早于类的函数调用),不清楚的可以参看:https://www.cnblogs.com/jycboy/p/5257615.html。而System.loadLibrary()操作一般是放在静态代码块中的。此时逻辑就会进入到so中,在从System.loadLibrary()的执行流程看UnsatisfiedLinkError的几种情况这篇博客中就讲过在加载so的时候会执行so中的JNI_OnLoad函数。所以若在该函数中进行注册,则注册完成之后java层函数和c/c++层函数的映射关系就已经建立起来。而这个时候java代码中任何native方法的调用还未进行。而理论上注册一定要比任何的native方法调用都要早才行,放到该函数中符合此条件,所以函数动态注册的最佳时机就是JNI_OnLoad被执行的时候。

RegisterNatives函数与JNINativeMethod

RegisterNatives函数

前面说过为了代码逻辑层次结构更清晰,我们往往把方法注册这块抽取为一个registerNativeMethods函数,然后在JNI_OnLoad中调用。registerNativeMethods函数内容一般如下所示:

主要就是调用JNIEnv的RegisterNatives方法进行native方法注册,该方法定义如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)

其各参数及返回值的意义如下:

  • jclass:被注册方法所在的类,也即java层native方法声明所在的类
  • JNINativeMethod*:方法数组,该数组的每个元素是JNINativeMethod结构体对象(描述该方法的一个数据结构,后面会讲到),每个该元素对应着一个java层方法和JNI方法的映射关系
  • jint nMethods:数组长度,一般使用sizeof(methods) / sizeof(methods[0])计算得到
  • 返回值jint,返回0表示注册成功,返回值<0表示注册失败

JNINativeMethod结构

该数据结构定义如下:

  • name表示在java代码中定义的native函数的名称
  • signature表示该函数在JNI中的方法签名描述符,在安卓ndk开发之java jni技术这篇博客中已经详细讲过,不清楚的可以点击查看
  • fnPtr表示映射到JNI代码中的函数指针,即(void*)JNI层函数名,建议动态注册时函数名保持和java层声明的一致性

完整示例

假设java层的类为com.example.MethodRegisterSample,在该类中定义了2个如下的native方法:

则JNI示例代码如下:

从上面可以总结出动态注册JNI函数的几大步骤:

  • 在JNI_OnLoad回调函数中通过JavaVM*参数调用GetEnv方法得到JNIEnv对象指针
  • 通过JNIEnv对象指针获取要注册的java方法所在的类的jclass对象
  • 定义JNINativeMethod数组,建立Java层方法和JNI方法的映射关系
  • 通过JNIEnv对象指针调用RegisterNatives方法,传入前面获取的jclass对象和JNINativeMethod数组完成注册
打赏

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注