内容简介:安卓NDK开发之so调用java代码
在前面 http://www.huqi.tk/index.php/2017/06/05/android_ndk/ 中,我们讲解了如何在 java 代码中调用本地C/C++代码,接下来就讲解如何在so中调用java层的代码,即如何在本地C/C++代码中调用java层代码。
我们首先来思考一个问题:就是我们的主程序是安卓程序,也就是主程序是java代码,要让C代码调用java,那在这之前我们是不是得用java代码来调用C呢?因为java是我们的主程序入口,因此如果要让C代码调用java代码,那么我们需要先在java层调C代码,也就是前面讲解的内容,不过此时我们的C代码中的内容不再是简单的c代码处理,而是C代码调用java层代码。
在正式讲解本节内容之前,我们来看一下上一节中利用javah命令自动生成的.h文件中的函数与我们在java层定义的函数之间的关系:
//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"); }
可以看到在java层原本无参数的函数对应到c++层却多了两个参数JNIEnv*和jobject,那么这两个参数的作用是什么呢?jobject很容易理解就是java层对应的C++层引用类型,即表示java中的Object类型,当我们在java代码中调用某个native函数时,该类即为该native函数对应到.cpp代码函数的jobject参数。这一点和java代码中类编译为class文件时,函数参数中会自动多一个this指针用来表示调用该函数的类的对象一样,这个很容易理解,因此重点来看下JNIEnv*是个啥意思?
JNIEnv*
我们来看下谷歌ndk开发官方文档是如何说明的:
#if defined(__cplusplus) typedef _JNIEnv JNIEnv; typedef _JavaVM JavaVM; #else typedef const struct JNINativeInterface* JNIEnv; typedef const struct JNIInvokeInterface* JavaVM; #endif
从上面可以看到JNIEnv表示的是JNINativeInterface这个结构体的指针,从上面也可以看到JNIEnv在C和C++下的定义是不同的,这个仅仅影响到我们调用函数的方式而已,对逻辑无影响,主要来说就是在cpp文件中调用函数不需要JNIEnv参数,而.c文件中通常将该参数作为函数第一个参数:
//C++调用方式 jclass jclazz = env->FindClass("com/htq/baidu/ndk/NDKTest"); //C调用方式 jclass jclazz = (*env)->FindClass(env, "com/htq/baidu/ndk/NDKTest");
这两种方式仅仅是调用方式不同而已,对逻辑无影响,这里以C为例进行分析,那么我们来看下JNINativeInterface是如何定义的:
/* * Table of interface function pointers. */ struct JNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void* reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize); jclass (*FindClass)(JNIEnv*, const char*); jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); jfieldID (*FromReflectedField)(JNIEnv*, jobject); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean); jclass (*GetSuperclass)(JNIEnv*, jclass); jboolean (*IsAssignableFrom)(JNIEnv*, jclass, jclass); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedField)(JNIEnv*, jclass, jfieldID, jboolean); ...... };
从注释可以看到JNINativeInterface表示的是接口函数指针表(Table of interface function pointers),也就是说该结构体定义了一系列的接口函数指针,注意是接口函数->指针,说白了就是该结构体中申明了一系列的功能函数,如FindClass函数,只不过这些函数不是普通形式的函数申明,而是通过函数指针的形式申明的,如:
//定义了一个名为FindClass的指针,该指针指向格式为 jclass fun(JNIEnv*, const char*)的函数 jclass (*FindClass)(JNIEnv*, const char*);
而这些函数指针最终指向的是DVM虚拟机中对应的JNI函数的地址,这样当我们在C/C++代码中通过JNIEnv调用函数的时候才能够被DVM虚拟机正确执行。用图示表示如下:
弄清楚了最核心的JNIEnv*的概念,接下来就来实现在so中调java代码的功能,我们在前面一节代码的基础上进行改进,将原本的C代码中的返回一个字符串的功能改写为C调用java层代码即可。那么既然是C调java代码,那么本质上肯定是通过调用java字节码来完成的,那么如果我们要调用某个java函数,首先得知道该函数属于哪个类,然后需要知道其函数名。在c代码层是通过JNIEnv对象的FindClass函数来获取字节码对象的。该函数定义如下:
jclass (*FindClass)(JNIEnv*, const char*);
该函数的参数及返回值意义如下:
获得了类的字节码对象之后就可以通过函数名来调用函数了,是通过GetMethodID函数完成的,该函数定义如下:
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
该函数的参数及返回值意义如下:
- JNIEnv*,JNIEnv对象指针
- jclass java字节码对象,即通过FindClass函数得到的返回值对象
- const char *,函数名字符串
- const char*,函数名签名格式字符串,也就是函数对应的各个参数类型
- jmethodID,函数的ID,可以理解为函数的指针
其中的第四个参数函数签名字符串,我们怎么知道函数的签名呢?这个可以使用javap命令,命令格式如下:
javap -s 类名(包含包名的类名)
同样的该命令也需要在字节码所在的目录执行,即在app\build\intermediates\classes\debug目录下执行上述命令,另外当我们执行javah命令自动生成JNI头文件时函数的签名信息在.h文件会以注释的信息告知,如图:
得到了MethodID之后就可以调用该函数了,在JNIEnv中定义了一系列的调用对应返回值的函数,都是形如CallXXXMethod的形式,如:
jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...); void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); jchar (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
这类函数的第一个参数为JNIEnv对象,第二个参数为类对象,第三个参数为函数ID,当我们调用的函数签名返回值属于void则调用CallVoidMethod,返回值为int则调用CallIntMethod,依此类推,如如果java层代码为:
public native void printString();
则对应的C++层调用java代码为:
//首先得到类的字节码对象 jclass jclazz = env->FindClass("com/htq/baidu/ndk/NDKTest"); //得到函数ID jmethodID methodID = env->GetMethodID(jclazz, "prinString", "()V"); //调用函数,第一个参数为JNIEnv对象,第二个参数为类对象,第三个参数为函数ID,C++中调用时无需JNIEnv参数 env->CallVoidMethod(object,methodID);
可以看到首先获取字节码对象,然后获取函数ID,最后调用函数,这一点和java中的反射调用某个函数的过程非常类似。
so调用java代码实例
通过前面的分析可以知道,so调java实质上还是通过java调c来完成的,只不过此时的c代码不是简单的处理native代码,而是会在c代码中反过来调用java代码,而且这个过程和java中的反射极其类似。那么我们就在前面一节代码的基础上进行改进,此时的主界面包含三个按钮,分别对应c调java中的void函数,c调java中的返回值为int的函数,c调java中函数参数为字符串的函数。此时的MainActivity代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private TextView tv; private Button btnVoid,btnInt,btnString; private NDKTest ndkTest; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ndkTest=new NDKTest(); tv= (TextView) findViewById(R.id.text); btnVoid= (Button) findViewById(R.id.btn_void); btnVoid.setOnClickListener(this); btnInt= (Button) findViewById(R.id.btn_int); btnInt.setOnClickListener(this); btnString= (Button) findViewById(R.id.btn_string); btnString.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_void: ndkTest.callBackMethod(); tv.setText(ndkTest.text); break; case R.id.btn_int: int sum=ndkTest.callBackIntMethod(1, 2); tv.setText("1加2的和为:"+String.valueOf(sum)); break; case R.id.btn_string: ndkTest.callBackStringArgMethod(); tv.setText(ndkTest.text); break; } } }
代码很简单,大家应该都能够看得懂,就是三个Button用来响应调用三个不同签名格式的native函数。其中的NDKTest类就是用来定义native函数的类,代码如下:
public class NDKTest { public String text; static { System.loadLibrary("ndk"); } //java调C函数 public native void callBackMethod(); public native int callBackIntMethod(int x,int y); public native void callBackStringArgMethod(); //c层回调java函数, public void prinString(){ String str="this string is from java code but it called by native code"; this.text=str; } public int addTwoNum(int x,int y){ return x+y; } public void setString(String str){ this.text=str; } }
最核心的当然是.cpp代码,在cpp代码中反过来调用java层的代码,调用的原理即是前面讲解的JNIEnv中的那些关键函数,代码如下:
#include <jni.h> #include "com_htq_baidu_ndk_NDKTest.h" JNIEXPORT void JNICALL Java_com_htq_baidu_ndk_NDKTest_callBackMethod(JNIEnv * env,jobject object){ //首先得到类的字节码对象 jclass jclazz = env->FindClass("com/htq/baidu/ndk/NDKTest"); //得到函数ID jmethodID methodID = env->GetMethodID(jclazz, "prinString", "()V"); //调用函数,第一个参数为JNIEnv对象,第二个参数为类对象,第三个参数为函数ID,C++中调用时无需JNIEnv参数 env->CallVoidMethod(object,methodID); } JNIEXPORT jint JNICALL Java_com_htq_baidu_ndk_NDKTest_callBackIntMethod(JNIEnv *env, jobject object, jint x, jint y){ //首先得到类的字节码对象 jclass jclazz = env->FindClass("com/htq/baidu/ndk/NDKTest"); //得到函数ID jmethodID methodID = env->GetMethodID( jclazz, "addTwoNum", "(II)I"); //调用函数,第一个参数为JNIEnv对象,第二个参数为类对象,第三个参数为函数ID,C++中调用时无需JNIEnv参数 int sum=env->CallIntMethod(object,methodID,x,y); return sum; } JNIEXPORT void JNICALL Java_com_htq_baidu_ndk_NDKTest_callBackStringArgMethod(JNIEnv *env, jobject object){ //首先得到类的字节码对象 jclass jclazz = env->FindClass("com/htq/baidu/ndk/NDKTest"); //得到函数ID jmethodID methodID = env->GetMethodID( jclazz, "setString", "(Ljava/lang/String;)V"); //将cpp文件中的字符串转化为java层的字符串jstring jstring str=env->NewStringUTF("this string is from c call java"); //调用函数,第一个参数为JNIEnv对象,第二个参数为类对象,第三个参数为函数ID,C++中调用时无需JNIEnv参数 env->CallVoidMethod(object,methodID,str); }
代码注释很详细,大家应该能够看懂。然后运行程序,依次点击void,ing,参数为String三种情况对应的按钮,程序输出结果如下:
当点击void按钮时首先在MainActivity的java代码中调用了native函数callBackMethod(),然后在该函数中我们通过JNIEnv中的一系列函数反过来调用了java层的prinString函数,在该函数中将字符串”this string is from java code but it called by native code”赋值给NDKTest类的text成员变量,最终在MainActivity中通过TextView的setText函数将text显示在控件上。这样就完成了Java层和C++层相互调用的过程。
源码下载地址:
注:本文首次发表于www.huqi.tk,谢绝转载,如需转载,请注明出处:www.huqi.tk
扫描下方二维码最新技术干货实时推送
扫描二维码实时接收最新技术干货推送,而且会不定期的发布互联网名企内推机会哦!
以上所述就是小编给大家介绍的《安卓NDK开发之so调用java代码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 链式调用 | 我的代码没有else
- golang之调用C语言代码
- 从JavaScript调用正确的TypeScript代码
- 对 Golang 代码调用 Elasticsearch 进行单元测试
- Python学习,VNR调用Jbeijing翻译的代码
- C和Lua之间的相互调用-代码例子2
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。