安卓NDK开发之so调用java代码

栏目: Java · 发布时间: 7年前

内容简介:安卓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虚拟机正确执行。用图示表示如下:

安卓NDK开发之so调用java代码

弄清楚了最核心的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文件会以注释的信息告知,如图:

安卓NDK开发之so调用java代码

得到了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三种情况对应的按钮,程序输出结果如下:

安卓NDK开发之so调用java代码

当点击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代码

扫描二维码实时接收最新技术干货推送,而且会不定期的发布互联网名企内推机会哦!


以上所述就是小编给大家介绍的《安卓NDK开发之so调用java代码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员面试宝典(第5版)

程序员面试宝典(第5版)

欧立奇、刘洋、段韬 / 电子工业出版社 / 2015-10 / 55.00

容提要 《程序员面试宝典(第5版)》是《程序员面试宝典》的第5 版,在保留第4 版的数据结构、面向对象、程序设计等主干的基础上,修正了前4 版近40 处错误,解释清楚一些读者提出的问题,并使用各大IT 公司及相关企业最新面试题(2014-2015)替换和补充原内容,以反映自第4 版以来两年多的时间内所发生的变化。 《程序员面试宝典(第5版)》取材于各大公司面试真题(笔试、口试、电话面试......一起来看看 《程序员面试宝典(第5版)》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器