Android Studio NDK 入门教程(5)--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对象返回。
时间: 2024-12-11 16:00:22

Android Studio NDK 入门教程(5)--Java对象的传递与修改的相关文章

Android Studio NDK 入门教程(2)--Java与C++之间的简单数据转换与传递

概述 本文将讲解Java与原生代码之间的数据转换,包括基础类型之间的转换,以及数组的传递与转换. 类型转换表 JAVA基础类型与C++之间的对应表 Java类型 C/C++类型 描述 boolean jboolean 无符号8位整数 byte jbyte 无符号8位整数 char jchar 有符号16位整数 short jshort 有符号16位整数 int jint 有符号32位整数 long jlong 有符号64位整数 float jfloat 32位单精度浮点数 double jdou

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

概述 根据前面的文章来看,JNI其实只实现了关键代码加密,如果别人拿到了你的Java Native方法定义和对应的so,即可完成对你so里方法的调.因为native 方法和类都是不能混淆的,混淆了方法的函数名就变了,调用的时候就找不到方法了,因此如果反编译APK可以非常容易拿到相关文件和代码. 显然我们需要一些手段来在JNI的验证请求接口的是不是我们的程序. 签名验证的原理 可以用如下图来表明加了验证之后调用JNI的逻辑,用一个isValid 来表明请求的应用是不是我们自己的应用.isValid

Android Studio NDK 学习之接受Java传入的Int数组

本博客是基于Android Studio 1.3 preview版本,且默认你已经安装了Android SDK, Android NDK. 用Android Studio新建一个工程叫AndroidJNI_IntArray,其目录结构如下: ├── AndroidJNI_IntArray.iml ├── app │   ├── app.iml │   ├── build │   ├── build.gradle │   ├── libs │   ├── proguard-rules.pro │ 

Android Studio NDK 学习之接受Java传入的字符串

本博客是基于Android Studio 1.3 preview版本,且默认你已经安装了Android SDK, Android NDK. 用Android Studio新建一个工程叫Prompt,其目录结构如下: ├── Prompt.iml ├── app │   ├── app.iml │   ├── build │   ├── build.gradle │   ├── libs │   ├── proguard-rules.pro │   └── src ├── build │   └─

Android Studio NDK开发-JNI调用Java方法

相对于NDK来说SDK里面有更多API可以调用,有时候我们在做NDK开发的时候,需要在JNI直接Java中的方法和变量,比如callback,系统信息等.... 如何在JNI中调用Java方法呢?就需要先了解FindClass和GetMethodID了. FindClass和GetMethodID 在JNI中可以通过FindClass可以找到Java类,得到jclass,例如: jclass clz=(*env)->FindClass(env,"com/jjz/JniHandle"

eclipse再见,android studio 新手入门教程(二)项目的导入

上一篇博客介绍了AS的一些常用设置方法,当工具调教妥当后,自然就要开始项目的开发啦.从零开始新建一个项目,这个简单,不必多说,这篇博客会分享我从旧平台eclipse导入项目到AS的过程,以及遇到的一些问题并如何解决.开篇先粗略的提一些需要注意的地方. 结构目录 和eclipse不同,在android 视图下的项目目录分为java,res和manifests. manifests目录存放清单文件,不必多说. java目录会默认生成三个文件夹,其中test为在本机执行单元测试代码的目录, andro

第五章:Reminders实验:第一部分[Learn Android Studio 汉化教程]

Learn Android Studio 汉化教程 By now you are familiar with the basics of creating a new project, programming, and refactoring.It is time to create an Android application, otherwise known as an app. This chapter introduces the first of four lab projects.

android studio NDK 开发初探

android studio NDK 开发初探 环境配置 1)下载ndk 2)在android studio中配置ndk 路径 3)配置gradle 在gradle.properties中加入 android.useDeprecatedNdk=true配置 开启ndk 在开发过程中我们有时还会用到c++中到stl库这时我们就需要在build.gradle 中加入 defaultConfig { ndk { moduleName "jnitest" stl "stlport_s

Android Studio NDK开发

整理完Eclipse的NDK开发,再整理下Android Studio的.. 一个比较不错的百度网盘: http://pan.baidu.com/share/home?uk=2383159761  经常更新最新的Android方面的开发包,可以到里面下载到ndk的包 创建一个Android的工程,放一个TextView用于显示文字,功能很简单,从native层获取字符串并显示到TextView上 然后编写相关代码: public class MainActivity extends AppCom