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

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

内容简介:安卓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代码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

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

Elements of Information Theory

Elements of Information Theory

Thomas M. Cover、Joy A. Thomas / Wiley-Blackwell / 2006-7 / GBP 76.50

The latest edition of this classic is updated with new problem sets and material The Second Edition of this fundamental textbook maintains the book's tradition of clear, thought-provoking instr......一起来看看 《Elements of Information Theory》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具