天问

  • Blog
  • 热修复与动态加载
  • python爬虫
  • 专栏
    • 热修复与动态加载
    • NDK开发
    • 设计模式与架构
    • React Native
  • 好文转载
    • 计算机技术
    • 投资理财
    • 摄影与拍照技巧
  • 投稿
  • 关于本站及隐私政策
  • Github
StevenQ
为今天工作,为明天投资,为未来孵化一些东西!
  1. 首页
  2. 计算机技术
  3. NDK
  4. 正文

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

2017年6月15日 359点热度 4人点赞 0条评论

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

//java层代码
public native String getString();
 
//C/C++层代码
jstring JNICALL Java_com_htq_baidu_ndk_NDKTest_getString
    (JNIEnv * env, jobject object){
   return env->NewStringUTF("hello.this is from native code");
}

首先增加了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函数了,该函数定义如下:

jint JNI_OnLoad(JavaVM *vm, void *reserved);

其各个参数及意义如下:

  • JavaVM*vm:指向JavaVM对象的指针,JavaVM可以理解为虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,该对象是线程共享的。有了JVM指针就可以使用其如下方法获取到JNIEnv指针,有了JNIEnv指针就可以调用各种JNI的API接口了。
    //第一个参数传入JNIEnv指针的引用,第二个参数传入JNI的版本号
    //获取成功返回JNI_OK(即0值)
    jint GetEnv(void **penv, jint version)
  • void *reserved:保留字段(题外话:从这里可以学习到为了以后方便扩展函数功能,可以在函数中先预留一些保留字段)

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

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

/*
* Returns the JNI version on success, -1 on failure.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
	JNIEnv* env = NULL;
	jint result = -1;
	if(vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK){//获取JNIEnv指针
		return -1;
	}
	assert(env != NULL);
	if (!registerNativeMethods(env)) {//注册native函数
		LOGE("registerNativeMethods failed");
		return -1;
	}
	/* success -- return valid version number */
	result = JNI_VERSION_1_6;
	return result;
}

该操作基本可以看作是一个模板代码,一般无需修改其内容,只需要定义一个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函数内容一般如下所示:

static int registerNativeMethods(JNIEnv* env)
{
	int errorCode = 0;
	jclass clazz = NULL;

	clazz = env->FindClass("com/example/MethodRegisterSample");
	if (clazz == NULL) {
		LOGE("clazz is null");
		return JNI_FALSE;
	}

        JNINativeMethod methods[] = {
	        { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
	        { "add",           "(II)I",                (void *)add }
         };
	errorCode = env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]) );
	if ( errorCode < 0 ) {
                LOGE("RegisterNatives failed,error code: %d", errorCode);
		return JNI_FALSE;
	}
	env->DeleteLocalRef(clazz);
	return JNI_TRUE;
}

主要就是调用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结构

该数据结构定义如下:

/*
 * used in RegisterNatives to describe native method name, signature,
 * and function pointer.
 */

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

完整示例

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

    public native String stringFromJNI(); 
    public static native int add(int a, int b);

则JNI示例代码如下:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
	JNIEnv* env = NULL;
	jint result = -1;
	if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {//获取JNIEnv指针
		return -1;
	}
	assert(env != NULL);
	if (!registerNativeMethods(env)) {//注册native函数
		LOGE("registerNativeMethods failed");
		return -1;
	}
	/* success -- return valid version number */
	result = JNI_VERSION_1_6;
	return result;
}

static int registerNativeMethods(JNIEnv* env)
{
	int errorCode = 0;
	jclass clazz = NULL;

	clazz = env->FindClass("com/example/MethodRegisterSample");//获取MethodRegisterSample类的jclass对象
	if (clazz == NULL) {
		LOGE("clazz is null");
		return JNI_FALSE;
	}

	JNINativeMethod methods[] = { //定义方法映射结构JNINativeMethod
		{ "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
		{ "add",           "(II)I",                (void *)add }
	};
	errorCode = env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));//动态注册方法
	if (errorCode < 0) {
		LOGE("RegisterNatives failed,error code: %d", errorCode);
		return JNI_FALSE;
	}
	env->DeleteLocalRef(clazz);
	return JNI_TRUE;
}

//实现JNI层对应的函数
jstring stringFromJNI(JNIEnv *env, jobject instance) {
 std::string hello = "hello,this msg is from jni code";
 return env->NewStringUTF(hello.c_str());
}

jint add(JNIEnv *env, jclass clazz, jint a, jint b) {
 return a + b;
}

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

  • 在JNI_OnLoad回调函数中通过JavaVM*参数调用GetEnv方法得到JNIEnv对象指针
  • 通过JNIEnv对象指针获取要注册的java方法所在的类的jclass对象
  • 定义JNINativeMethod数组,建立Java层方法和JNI方法的映射关系
  • 通过JNIEnv对象指针调用RegisterNatives方法,传入前面获取的jclass对象和JNINativeMethod数组完成注册
本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: JNI ndk
最后更新:2018年9月21日

天问

追梦追自由,穷则独善其身,达则兼济天下

打赏 点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2022 天问 ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang