Android Studio NDK 入门教程--Java对象的传递与修改

Posted Wastrel_xyz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Studio NDK 入门教程--Java对象的传递与修改相关的知识,希望对你有一定的参考价值。

概述

本文主要Java与C++之间的对象传递与取值。包括传递Java对象、返回Java对象、修改Java对象、以及性能对比。

通过JNIEnv完成数据转换

Java对象是存在于JVM虚拟机中的,而C++是脱离JVM而运行的,如果在C++中访问和使用Java中的对象,必然会使用JNIEnv这个桥梁。其实通过下面的代码很容易看出,这种访问方式和Java中的反射十分雷同。

这里定义一个简单Java对象用于下文测试:

package com.example.wastrel.hellojni;
/**
 * Created by wastrel on 2016/8/24.
 */
public class Bean 
    private String msg;
    private int what;

    public Bean(String msg,int what)
    
        this.msg=msg;
        this.what=what;
    


    public String getMsg() 
        return msg;
    

    public void setMsg(String msg) 
        this.msg = msg;
    

    public int getWhat() 
        return what;
    

    public void setWhat(int what) 
        this.what = what;
    

    @Override
    public String toString() 
        return "Msg:"+msg+";What:"+what;
    

从C++中创建一个Java对象并返回

    //Java中的native方法声明
    public native Bean newBean(String msg,int what);
//C++中的方法实现
JNIEXPORT jobject JNICALL Java_com_example_wastrel_hellojni_HelloJNI_newBean
        (JNIEnv *env, jobject obj, jstring msg,jint what)
    //先找到class
    jclass bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean");
    //在实际应用中应该确保你的class、method、field存在。减少此类判断。
    if(bean_clz==NULL)
    
        LOGE("can't find class");
        return NULL;
    
    //获取构造函数。构造函数的返回值是void,因此这里方法签名最后为V
    jmethodID bean_init=env->GetMethodID(bean_clz,"<init>","(Ljava/lang/String;I)V");
    if(bean_init==NULL)
    
        LOGE("can't find init function");
        return NULL;
    
    //然后调用构造函数获得bean
    jobject bean=env->NewObject(bean_clz,bean_init,msg,what);
    return bean;

注:如果提示找不到NULL 请include<stddef.h>

C++中解析Java对象

//java方法Native声明
public native String getString(Bean bean);
//C++中的方法实现
JNIEXPORT jstring JNICALL Java_com_example_wastrel_hellojni_HelloJNI_getString
        (JNIEnv *env, jobject obj,jobject bean)
    jclass bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean");

//这部分是通过get函数去获取对应的值 
//    jmethodID bean_getmsg=env->GetMethodID(bean_clz,"getMsg","()Ljava/lang/String;");
//    jmethodID bean_getwhat=env->GetMethodID(bean_clz,"getWhat","()I");
//    jstring jmsg=(jstring)env->CallObjectMethod(bean,bean_getmsg);
//    jint what=env->CallIntMethod(bean,bean_getwhat);

//这部分是通过类的成员变量直接取获取值,你可能注意到在Java中定义的变量都是private修饰的,但在反射的调用下是毫无作用的。
    jfieldID bean_fmsg=env->GetFieldID(bean_clz,"msg","Ljava/lang/String;");
    jfieldID bean_fwhat=env->GetFieldID(bean_clz,"what","I");
    jstring jmsg=(jstring)env->GetObjectField(bean,bean_fmsg);
    jint  what=env->GetIntField(bean,bean_fwhat);

//将拿到的值拼装一个String返回回去
    const char * msg=env->GetStringUTFChars(jmsg,NULL);
    char *str=new char[1024];
    sprintf(str,"Msg:%s;What:%d(From C++)",msg,what);
    jstring rs=env->NewStringUTF(str);
    delete  []str;
    env->ReleaseStringUTFChars(jmsg,msg);
    return rs;

注:sprintf函数包含在stdio.h头文件中

C++中修改Java对象属性值

//java方法Native声明
public native void ModifyBean(Bean bean);
//C++实现
JNIEXPORT void JNICALL Java_com_example_wastrel_hellojni_HelloJNI_ModifyBean
        (JNIEnv *env, jobject obj,jobject bean)
    jclass bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean");
    jfieldID bean_fmsg=env->GetFieldID(bean_clz,"msg","Ljava/lang/String;");
    jfieldID bean_fwhat=env->GetFieldID(bean_clz,"what","I");
    jstring msg=env->NewStringUTF("Modify in C++");
    //重新设置属性
    env->SetObjectField(bean,bean_fmsg,msg);
    env->SetIntField(bean,bean_fwhat,20);
    return;

结果图

//java中调用代码
        HelloJNI helloJNI=new HelloJNI();
        Bean bean=helloJNI.newBean("This is from C++ bean",10);
        tv.setText(bean.toString());
        bean=new Bean("This is from Java bean",15);
        tv.append("\\n"+helloJNI.getString(bean));
        helloJNI.ModifyBean(bean);
        tv.append("\\n"+bean.toString());

Java中new Object和C++中new Object的性能对比

下面我们通过一个测试函数来比较通过两种方式的性能,这里可以毫无疑问的告诉你,Java一定比C++的快。那么这个对比的意义就在于,使用C++创建Java对象的时候会不会造成不可接受的卡顿。
这里使用的测试机是华为Mate7,具体硬件配置可自行百度。
测试函数如下:

     void Test(int count)
    
        long startTime=System.currentTimeMillis();
        for (int i=0;i<count;i++)
        
            new Bean("123",i);
        
        long endTime=System.currentTimeMillis();
        Log.e("Java","Java new "+count+"s waste "+(endTime-startTime)+"ms");

        HelloJNI helloJNI=new HelloJNI();
       startTime=System.currentTimeMillis();
        for (int i=0;i<count;i++)
        
            helloJNI.newBean("123",i);
        
        endTime=System.currentTimeMillis();
        Log.e("C++","C++ new "+count+"s waste "+(endTime-startTime)+"ms");
    

测试结果:

Java: Java new 5000s waste 3ms
C++: C++ new 5000s waste 38ms

Java: Java new 10000s waste 6ms
C++: C++ new 10000s waste 79ms

Java: Java new 50000s waste 56ms
C++: C++ new 50000s waste 338ms

Java: Java new 100000s waste 60ms
C++: C++ new 100000s waste 687ms

通过结果可以看出,通过C++来new对象比Java慢了足足10倍左右。但是从时间上来讲,如果只是在C++中new一个Java对象,几个微秒的时间差距完全是可以忽略不计的。

也许有人就会说,C++慢那么多是因为每次都在FindClass,GetMethodId,而在程序运行过程中这两个值是不会改变的。听起来确实有这样一个原因,下面我们将C++中的代码稍作修改缓存jclass和jmethodId。
修改后的newBean函数:

//用静态变量缓存
static jclass bean_clz=NULL;
static jmethodID bean_init=NULL;
JNIEXPORT jobject JNICALL Java_com_example_wastrel_hellojni_HelloJNI_newBean
        (JNIEnv *env, jobject obj, jstring str,jint what)
    //先找到class
    if(bean_clz==NULL)
    
        jclass  _bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean");
        bean_clz=(jclass)env->NewGlobalRef(_bean_clz);
    
    //获取构造函数。构造函数的返回值是void,因此这里方法签名最后为V
    if(bean_init==NULL)
    
        bean_init=env->GetMethodID(bean_clz,"<init>","(Ljava/lang/String;I)V");
    
    //然后调用构造函数获得bean
    jobject bean=env->NewObject(bean_clz,bean_init,str,what);
    return bean;

 你可能发现了缓存方法ID和缓存jclass似乎不一样。那是因为jclass其实是java.lang.Class对象,而方法ID是JNI中定义的一个结构体。如果这里不使用env—>NewGlobalRef()函数声明其是一个全局引用的话,在运行的时候可能就会报错:JNI ERROR (app bug): accessed stale local reference 0x5900021;表明在Jvm中该对象已经被回收了,引用已经失效了。而NewGlobalRef的作用就在于告诉JVM,C++中一直持有该引用,请不要回收。显然这又引发了另外一个问题,你需要在你不需要该引用的时候告诉JVM,那么就需要调用env->DelGlobalRef()。当然你也可以不调用,那么该Java对象将在你的程序关闭的时候被回收。

测试结果:

Java: Java new 5000s waste 3ms
C++: C++ new 5000s waste 18ms

Java: Java new 10000s waste 5ms
C++: C++ new 10000s waste 24ms

Java: Java new 50000s waste 44ms
C++: C++ new 50000s waste 121ms

Java: Java new 100000s waste 65ms
C++: C++ new 100000s waste 259ms

这次的结果表明,如果缓存方法ID和jclass能缩短一半的时间。但仍然不如Java快。这也很好理解,C++创建Java对象最终还是通过Java创建的,反复的通过反射去创建自然不如自身创建来得快。

总结

  • JNI中想访问Java Object方法签名、类名和变量名十分重要,一旦确定了就不要轻易单方面修改Java中的定义。因为这会导致JNI找不到相关的方法或类等,而引发JNI错误。
  • 虽然JNI提供了各种方法来完成Java的反射操作,但是请酌情使用,因为这会让Java代码与C++代码之间过度依赖。
  • 当你需要返回C++中的结构体数据的时候,可以考虑把结构体转换成对应的Java对象返回。

以上是关于Android Studio NDK 入门教程--Java对象的传递与修改的主要内容,如果未能解决你的问题,请参考以下文章

Android Studio NDK 入门教程--优雅的在C++中输出Logcat

Android Studio NDK 入门教程--JNI动态注册本地方法

Android Studio NDK 入门教程--Java与C++之间的类型签名

Android Studio NDK 入门教程--Java对象的传递与修改

NDK开发 从入门到放弃(七:Android Studio 2.2 CMAKE 高效NDK开发)

Android Studio NDK 入门教程--JNI签名验证防止恶意调用