第2章 Android中NDK开发
本章主要介绍Android中的NDK开发技术相关知识,因为后续章节特别是在介绍安全应用防护和逆向应用的时候,会涉及NDK的相关知识,而且考虑到项目的安全性开发,把一些重要的代码放到底层也是很重要的,同时能提高执行效率。
2.1 搭建开发环境
在搭建环境之前必须先去官网下载NDK工具包,官网地址是http://wear.techbrood.com/tools/sdk/ndk/,选择相应平台的NDK版本即可。
2.1.1 Eclipse环境搭建
第一步:配置NDK路径,如图2-1所示。
图2-1 配置NDK路径
第二步:新建Android项目,如图2-2所示。
图2-2 新建Android项目
点击Add Native Support,出现如图2-3所示的lib命令。
图2-3 命令lib
点击“Finish”,再次观察项目多了jni文件夹,如图2-4所示。
图2-4 添加了jni文件夹
在jni下面就可以开始编写native层的代码。
第三步:使用javah生成native的头文件,如图2-5所示。
图2-5 生成native头文件
注意:javah执行的目录,必须是类包名路径的最上层,然后执行:
javah类全名
注意没有后缀名java。
第四步:运行项目,点击工具栏中的小锤子图标如图2-6所示。
图2-6 运行项目
运行结果如图2-7所示。
图2-7 运行结果
2.1.2 Android Studio环境搭建
去官网下载NDK工具,然后使用Android Studio中进行新建一个简单项目,然后创建JNI即可,如图2-8所示。
图2-8 创建jni
第一步:在项目中新建jni目录,如图2-9所示。
图2-9 新建jni目录
第二步:用javah命令生成native的头文件,如图2-10所示。
图2-10 生成native头文件
第三步:配置项目的NDK目录,如图2-11所示。
图2-11 配置NDK目录
选择模块的设置选项Open Module Settings,如图2-12所示。
图2-12 模块的设置选项
在其中设置NDK目录即可。
第四步:配置Gradle中的ndk选项,如图2-13所示。
图2-13 配置gradle
这里只需要设置编译之后的模块名,即so文件的名称,以及产生哪几个平台下的so文件,需要用到的lib库,这里用到了Android中打印log的库文件。
第五步:编译运行生成so文件
在build目录下生成指定的so文件,拷贝到项目的libs目录下即可,如图2-14所示。
图2-14 项目的libs目录
2.2 第一行代码:HelloWorld
本节开始介绍JNI技术,先输出一个Hello World。具体流程如下,在Java中定义一个方法,在C++中实现这个方法,在方法内部输出“Hello World”,然后再回到Java中进行调用。
第一步:在Eclipse中建立一个类:JNIDemo。
命令如下:
package com.jni.demo; public class JNIDemo { //定义一个本地方法 public native void sayHello(); public static void main(String[] args){ //调用动态链接库 System.loadLibrary("JNIDemo"); JNIDemo jniDemo = new JNIDemo(); jniDemo.sayHello(); } }
其中sayHello就是要在C++中实现的方法。
第二步:使用javah命令将JNIDemo生成.h的头文件。
命令如下:
E:\workspace\JNIDemo\bin>javah com.jni.demo.JNIDemo
注意:
● 首先要确保配置了Java的环境变量,不然javah命令不能用。
● 案例的Java项目是放在E:\workspace中的,所以首先进入项目的bin目录中,然后使用javah命令生成头文件。
● javah后面的类文件格式是类的全名(包名+class文件名),同时不能有.class后缀。
命令执行成功后会在bin目录中生成头文件com_jni_demo_JNIDemo.h,参见图2-15。
图2-15 javah命令参数说明
注意:如果包含native方法的类,引用其他地方的类,那么进入bin\classes\目录下会出现问题提示找不到指定的类,这时候需要切换到源码目录src下运行即可。
第三步:使用VC6.0生成.dll文件。
首先创建一个dll工程,如图2-16~图2-18所示。
图2-16 VC6.0生成.dll文件
图2-18 VC6.0生成.dll文件
在.cpp文件中输入如下代码:
#include<iostream.h> #include "com_jni_demo_JNIDemo.h" JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj) { cout<<"Hello World"<<endl; }
图2-17 VC6.0生成.dll文件
这个方法的声明可以在上面生成的com_ jni_demo_JNIDemo.h头文件中找到,这个就是Java工程中的sayHello方法的实现:
JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj) { cout<<"Hello World"<<endl; }
这里编译会出现以下几个问题:
1)会提示找不到相应的头文件,如图2-19所示。这时需要将jni.h、jni_md.h文件拷贝到工程目录中,这两个文件的具体位置参见图2-20。
图2-19 编译之后的头文件
图2-20 jni.h文件
Java安装目录中的include文件夹下,jni_md.h文件在win32文件夹中,找到这两个文件后,将其拷贝到C++的工程目录中。
2)当拷贝到这两个文件之后,编译还是提示找不到这两个文件:主要原因是#include<jni.h>是从系统目录中查找jni.h头文件的,而这里只把jni.h拷贝到工程目录中,所以需要在com_ jni_demo_JNIDemo.h头文件中将#include<jni.h>改成#include "jni.h"。同理,在jni.h文件中将#include<jni_md.h>改成#include "jni_md.h"。
3)同时还有一个错误提示:e:\c++\jnidemo\jnidemo.cpp(9) : fatal error C1010: unexpected end of file while looking for precompiled header directive,这是指预编译头文件读写错误,这时候还要在VC中进行设置:项目→设置→C/C++。在分类中选择预编译的头文件,选择不使用预补偿页眉,如图2-21所示。
图2-21 预编译头文件
这样,编译成功,生成JNIDemo.dll文件在C++工程中的Debug目录中。
注意:因为之前开发都是使用VC工具,所以这里使用了VC 6.0来进行C++代码的编写和运行,其实可以直接使用Eclipse或在Android Studio中也可以进行编写,这样会更方便。
第四步:将JNIDemo.dll文件添加到环境变量中,如图2-22所示。
图2-22 将JNIDemo.dll文件添加到环境变量中
注意:在用户变量中的path设置,用分号隔开:“; E:\C++\Debug”,这样就将.dll文件添加到环境变量中了。
第五步:在Eclipse中调用sayHello方法,输出“Hello World”。代码如下:
public static void main(String[] args){ //调用动态链接库 System.loadLibrary("JNIDemo"); JNIDemo jniDemo = new JNIDemo(); jniDemo.sayHello(); }
System.loadLibrary方法是加载JNIDemo.dll文件的,一定要注意不要有.dll后缀名,只需要文件名即可。
注意,运行的时候会报错,如图2-23所示。
图2-23 运行错误
这个提示是没有找到JNIDemo.dll文件,这时需要关闭Eclipse,然后再打开,运行就没有错了。原因是Eclipse每次打开的时候都会去读取环境变量的配置,刚才配置的path没有立即生效,所以要关闭Eclipse,然后重新打开一次即可。
注意:这里因为使用了VC编辑器进行native代码的编写,所以需要配置dll文件操作,但是现在更多的是习惯直接在Eclipse/Android Studio中配置C++环境直接编写了,这样更方便。
2.3 JNIEnv类型和jobject类型
上一节介绍的是一个简单的应用,说明JNI是怎么工作的,这一节介绍本地方法sayHello的参数及其使用。
首先来看一下C++中的sayHello方法的实现:
JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj) { cout<<"Hello World"<<endl; }
2.3.1 JNIEnv类型
JNIEnv类型实际上代表了Java环境,通过JNIEnv* 指针就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等。
JNIEnv类中有很多函数可以用,如下所示:
● NewObject:创建Java类中的对象。
● NewString:创建Java类中的String对象。
● New<Type>Array:创建类型为Type的数组对象。
● Get<Type>Field:获取类型为Type的字段。
● Set<Type>Field:设置类型为Type的字段的值。
● GetStatic<Type>Field:获取类型为Type的static的字段。
● SetStatic<Type>Field:设置类型为Type的static的字段的值。
● Call<Type>Method:调用返回类型为Type的方法。
● CallStatic<Type>Method:调用返回值类型为Type的static方法。
更多的函数使用可以查看jni.h文件中的函数名称。
2.3.2 jobject参数obj
如果native方法不是static, obj就代表native方法的类实例。
如果native方法是static, obj就代表native方法的类的class对象实例(static方法不需要类实例的,所以就代表这个类的class对象)。
2.3.3 Java类型和native中的类型映射关系
Java和C++中的基本类型的映射关系参见表2-1。
表2-1 Java和C++中的基本类型的映射关系
具体的说明可以查看jni.h文件。
2.3.4 jclass类型
为了能够在C/C++中使用Java类,jni.h头文件中专门定义了jclass类型来表示Java中的Class类。
JNIEnv类中有如下几个简单的函数可以取得jclass:
● jclass FindClass(const char* clsName):通过类的名称(类的全名,这时候包名不是用点号而是用/来区分的)来获取jclass。如:jclass str = env->FindClass("java/lang/String");获取Java中的String对象的class对象。
● jclass GetObjectClass(jobject obj):通过对象实例来获取jclass,相当于Java中的getClass方法。
● jclass GetSuperClass(jclass obj):通过jclass可以获取其父类的jclass对象。
2.3.5 native中访问Java层代码
在C/C++本地代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了在C/C++中表示属性和方法,JNI在jni.h头文件中定义了jfieldId、jmethodID类型来分别代表Java端的属性和方法。在访问或者设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能在本地代码中进行Java属性操作,同样,需要调用Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java方法调用。
使用JNIEnv的如下方法:
● GetFieldID/GetMethodID
● GetStaticFieldID/GetStaticMethodID
来取得相应的jfieldID和jmethodID。
下面来具体看一下这几个方法。
GetFieldID方法如下:
GetFieldID(jclass clazz, const char* name, const char* sign)
方法的参数说明:
● clazz:这个方法依赖的类对象的class对象。
● name:这个字段的名称。
● sign:这个字段的签名(每个变量,每个方法都是有签名的)。
怎么查看类中的字段和方法的签名呢?使用javap命令,如下所示。
GetMethodID也能够取得构造函数的jmethodID,创建一个Java对象时可以调用指定的构造方法,后续将向大家介绍,如:
env->GetMethodID(data_Class, "<init>", "()V");
签名的格式见表2-2。
表2-2 签名的格式
下面来看一例子:
import java.util.Date; public class Hello{ public int property; public int function(int foo, Date date, int[] arr){ System.out.println("function"); return 0; } public native void test(); } //test本地方法实现 JNIEXPORT void Java_Hello_test(JNIEnv* env, jobject obj){ //因为test不是静态函数,所以传进来的就是调用这个函数的对象 //否则就传入一个jclass对象表示native方法所在的类 jclass hello_clazz = env->GetObjectClass(obj); jfieldId fieldId_prop = env->GetFieldId( hello_clazz, "property", "I"); jmethodId methodId_func = env->GetMethodId( hello_clazz, "function", "(ILjava/util/Data; [I)I"); env->CallIntMethod(obj, methodId_func, 0L, NULL, NULL ); }
上面的native代码中,首先取得property字段,因为property字段是int类型的,所以在签名中传入“I”,取得方法function的ID时:
int function(int foo, Date date, int[] arr);
签名为(Iljava/util/Date; [I)I。
关于GetStaticFieldID/GetStaticMethodID这两个方法的用法大同小异,区别在于这两个方法是获取静态字段和方法的ID。
2.4 JNIEnv类型中方法的使用
前面说到JNIEnv类型,下面通过例子来看一下这些方法的使用。第一个例子是在Java代码中定义一个属性,然后再从C++代码中将其设置成另外的值,并且输出来。
2.4.1 native中获取方法的Id
先来看一下Java代码:
package com.jni.demo; public class JNIDemo { public int number = 0; //定义一个属性 //定义一个本地方法 public native void sayHello(); public static void main(String[] args){ //调用动态链接库 System.loadLibrary("JNIDemo"); JNIDemo jniDemo = new JNIDemo(); jniDemo.sayHello(); System.out.print(jniDemo.number); } }
再来看一下C++代码:
#include<iostream.h> #include "com_jni_demo_JNIDemo.h" JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj) { //获取obj中对象的class对象 jclass clazz = env->GetObjectClass(obj); //获取Java中的number字段的id(最后一个参数是number的签名) jfieldID id_number = env->GetFieldID(clazz, "number", "I"); //获取number的值 jint number = env->GetIntField(obj, id_number); //输出到控制台 cout<<number<<endl; //修改number的值为100,这里要注意的是jint对应C++是long类型,所以后面要加一个L env->SetIntField(obj, id_number,100L); }
编译成功后,在Eclipse运行后的结果如图2-24所示。
图2-24 运行成功效果图
第一个0是在C++代码中的cout<<number<<endl。
第二个100是在Java中的System.out.println(jniDemo.number)。
JNIEnv提供了众多的Call<Type>Method和CallStatic<Type>Method,还有CallNonvirtual<Type>Method函数,需要通过GetMethodID取得相应方法的jmethodID来传入到上述函数的参数中。
调用示例方法的三种形式如下:
Call<Type>Method(jobject obj, jmethodID id, ....); Call<Type>Method(jobject obj, jmethodID id, va_list lst); Call<Type>Method(jobject obj, jmethodID id, jvalue* v);
第一种是最常用的方式。第二种是当调用这个函数的时候有一个指向参数表的va_list变量时使用的(很少使用)。第三种是当调用这个函数的时候有一个指向jvalue或jvalue数组的指针时用的。
jvalue在jni.h头文件中定义是一个union联合体,在C/C++中,union可以存放不同类型的值,但是当你给其中一个类型赋值之后,这个union就是这种类型了,比如你给jvalue中的s赋值的话,jvalue就变成了jshort类型了,所以可以定义一个jvalue数组(这样就可以包含多种类型的参数了)传递到方法中,如下所示:
typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue;
假如现在Java中有这样的一个方法:
boolean function(int a, double b, char c) { ........ }
1)在C++中使用第一种方式调用function方法:
env->CallBooleanMethod(obj , id_function , 10L, 3.4 , L'a')
obj是方法funtion的对象。id_function是方法function的id,可以通过GetMethodID()方法获取。然后就是对应的参数,这和Java中的可变参数类似。最后一个char类型的参数L'a’为什么前面要加一个L呢?原因是Java中的字符是Unicode双字节的,而C++中的字符是单字节的,所以要变成宽字符,即前面加一个L。
2)在C++中使用第三种方式调用function方法:
jvalue* args = new jvalue[3]; //定义jvalue数组 args[0].i = 10L; //i是jvalue中的jint值 args[1].d = 3.44; args[2].c = L'a'; env->CallBooleanMethod(obj, id_function, args); delete[] args; //是否指针堆内存
例子:C++中调用Java中的方法。
Java代码如下:
public double max(double value1, double value2){ return value1>value2 ? value1:value2; }
这时候用javap获取max方法的签名,如下所示。
max方法的签名是(DD)D。在C++中的代码如下:
JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj) { //获取obj中对象的class对象 jclass clazz = env->GetObjectClass(obj); //获取Java中的max方法的id(最后一个参数是max方法的签名) jmethodID id_max = env->GetMethodID(clazz, "max", "(DD)D"); //调用max方法 jdouble doubles = env->CallDoubleMethod(obj, id_max,1.2,3.4); //输出返回值 cout<<doubles<<endl; }
编译成动态文件后,到Eclipse中执行sayHello方法,运行结果如图2-25所示。
图2-25 运行成功效果图
可见,成功地输出了最大值。
2.4.2 Java和C++中的多态机制
JNIEnv中有一个特殊的方法CallNonvirtual<Type>Method,如下所示:
public class Father{ public void function(){ System.out.println("Father.func"); } } public class Child extends Father{ public void function(){ System.out.println("Child.func"); } } //这行代码中执行的结果是什么? Father p = new Child(); p.function;
首先来了解一下,上面调用的function是子类的function方法,但是在C++中就不一样了:
class Father{ public: void function(){ count<<"Father.func"<<endl; } }; class Child:public Father{ public: void function(){ count<<"Child.func"<<endl; } }; //下面这段代码执行的结果是什么呢? Father* p = new Childe(); p->function();
这段C++代码中执行的是父类的function方法,如果想执行子类的function方法怎么办呢?就需要将父类的function方法定义成virtual虚函数:
class Father{ //这里设置了虚函数 public: virtual void function(){ count<<"Father.func"<<endl; } }; class Child:public Father{ public: void function(){ count<<"Child.func"<<endl; } }; //这里执行的结果是什么呢? Father* p = new Childe(); p->function();
所以,C++和Java对于继承后执行的是父类还是子类的方法是有区别的,在Java中所有的方法都是虚拟的,所以总是调用子类的方法,因此CallNonVirtual<Type>Method方法就出来了,这个方法可以帮助调用Java中父类的方法。
在JNI中定义的CallNonvirtual<Type>Method能够实现子类对象调用父类方法的功能,如果想要调用一个对象的父类方法,而不是子类的方法,就可以使用CallNonvirtual<Type>Method。要使用它,首先要获得父类及其要调用的父类方法的jmethodID,然后传入到这个函数就能通过子类对象调用被覆写的父类方法了。
例如:在Java中定义Father类:
package com.jni.demo; public class Father { public void function(){ System.out.println("Father:function"); } }
定义一个子类Child,继承Father类,重写父类中的function方法:
package com.jni.demo; public class Child extends Father{ @Override public void function(){ System.out.println("Child:function"); } }
在JNIDemo代码,定义Father类型的属性:
package com.jni.demo; public class JNIDemo { public Father father = new Child(); //定义一个本地方法 public native void sayHello(); public static void main(String[] args){ //调用动态链接库 System.loadLibrary("JNIDemo"); JNIDemo jniDemo = new JNIDemo(); jniDemo.sayHello(); } }
再来看一下C++中的代码:
#include<iostream.h> #include "com_jni_demo_JNIDemo.h" JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj) { //获取obj中对象的class对象 jclass clazz = env->GetObjectClass(obj); //获取Java中的father字段的id(最后一个参数是father字段的签名) jfieldID id_father = env->GetFieldID(clazz, "father", "Lcom/jni/demo/Father; "); //获取father字段的对象类型 jobject father = env->GetObjectField(obj, id_father); //获取father对象的class对象 jclass clazz_father = env->FindClass("com/jni/demo/Father"); //获取father对象中的function方法的id jmethodID id_father_function = env->GetMethodID(clazz_father, "function", "()V"); //调用父类中的function方法(但是会执行子类的方法) env->CallVoidMethod(father, id_father_function); //调用父类中的function方法(执行就是父类中的function方法) env->CallNonvirtualVoidMethod(father, clazz_father, id_father_function); }
编译成功.dll文件,回到Eclipse中运行结果参如图2-26所示。
图2-26 运行结果
其中:
● Child:function是调用env->CallVoidMethod(...)方法的。
● Father:function是调用env->CallNonvirtualMethod(...)方法的。
这样就能够控制到底调用哪个类的function方法了。
2.5 创建Java对象及字符串的操作方法
首先来看一下C/C++中怎么创建Java对象,然后再介绍如何操作Java字符串。
2.5.1 native中创建Java对象
在JNIEnv中有两种方法创建Java对象,下面分别介绍。
第一种方法创建Java对象
代码如下:
jobject NewObject(jclass clazz , jmethodID methodID, ....)
参数如下:
● clazz:是需要创建的Java对象的Class对象。
● methodID:是传递一个方法的ID,想一想Java对象在创建的时候,需要执行什么方法呢?对,没错那就是构造方法。
● 第三个参数:是构造函数需要传入的参数值(默认的构造方法是不需要传入这个参数的)。所以在创建Java对象之前要做的工作就是要获取这个对象的class对象,然后再获取该对象的构造方法。想要获取方法的id,就需要方法的签名,因为构造方法没有返回值,所以认为类的默认构造方法的返回值类型的签名始终是“()V”(因为默认的构造方法是没有参数的),方法的名称始终为“<init>”。
在C++中构造Java中的Date对象,并且调用它的getTime()方法打印当前时间。
Java中的代码不需要改变,主要是在C++代码中改写:
#include<iostream.h> #include "com_jni_demo_JNIDemo.h" JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobjec t obj) { //获取Java中Date对象的Class对象 jclass clazz_date = env->FindClass("java/util/Date"); //获取构造方法的id jmethodID mid_date = env->GetMethodID(clazz_date, "<init>", "()V"); //生成Date对象 jobject now = env->NewObject(clazz_date, mid_date); //获取Date对象中的getTime方法的id jmethodID mid_date_getTime = env->GetMethodID(clazz_date, "getTime", "()J"); //调用getTime方法返回时间 jlong time = env->CallLongMethod(now, mid_date_getTime); //打印时间,这里要注意的是不能使用cout输出,因为cout并没有对__int64的输出进行重载, //要输出的话用printf("%I64d", time); printf("%I64d", time); }
编译成.dll文件,在Eclipse中运行结果如图2-27所示。
图2-27 运行结果
第二种方法创建Java对象
用AllocObject函数创建一个对象,可以根据传入的jclass创建一个Java对象,但是状态是非初始化的,在这个对象之前绝对要用CallNonvirtualVoidMethod来调用该jclass的构造函数,这样就可以延迟构造函数的调用。这种方法用得很少,下面只对代码做简单的说明。
Java中的代码不做任何修改,C++代码修改如下:
#include<iostream.h> #include "com_jni_demo_JNIDemo.h" JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobjec t obj) { //获取java中的Date对象 jclass clazz_date = env->FindClass("java/util/Date"); jmethodID methodID_str = env->GetMethodID(clazz_date, "<init>", "()V"); jobject now = env->AllocObject(clazz_date); //调用构造方法 env->CallNonvirtualVoidMethod(now, clazz_date, methodID_str); //获取Date对象中的getTime方法的id jmethodID mid_date_getTime = env->GetMethodID(clazz_date, "getTime", "()J"); //调用getTime方法返回时间 jlong time = env->CallLongMethod(now, mid_date_getTime); //打印时间,这里要注意的是不能使用cout输出,因为cout并没有对__int64的输出进行重载, //要输出的话用printf("%I64d", time); printf("%I64d", time); }
2.5.2 native中操作Java字符串
首先来了解一下Java和C/C++中字符串的区别。在Java中,使用的字符串String对象是Unicode(UTF-16)码,即每个字符不论是中文还是英文还是符号,一个字符总是占两个字节。Java通过JNI接口可以将Java的字符串转换到C/C++中的宽字符串(wchar_t*),或传回一个UTF-8的字符串(char*)到C/C++;反过来,C/C++可以通过一个宽字符串,或一个UTF-8编码的字符串来创建一个Java端的String对象。
接下来看一下JNIEnv中的一些C++方法。
1)获取字符串的长度:
jsize GetStringLength(jstring j_msg)
参数j_msg是一个jstring对象。
2)将jstring对象拷贝到const jchar*指针字符串:
//这个方法是:拷贝Java字符串并以UTF-8编码传入jstr env->GetStringRegion(jstring j_msg , jsize start , jsize len , jchar* jstr); //这个方法是:拷贝Java字符串并以UTF-16编码传入jstr env->GetStringUTFRegion(jstring j_msg , jsize start , jsize len , char* jstr);
这是在Java 1.2出来的函数,这个函数把Java字符串的内容直接拷贝到C/C++的字符串数组中,在调用这个函数之前必须有一个C/C++分配出来的字符串(具体看下面的例子),然后传入到这个函数中进行字符串的拷贝。
由于C/C++中分配内存开销相对小,而且Java中的String内容拷贝的开销可以忽略,更好的一点是此函数不分配内存,不会抛出OutOfMemoryError异常。
参数j_msg是一个jstring对象,start是拷贝字符串的开始位置,len是拷贝字符串的长度,jstr是目标指针字符串。
3)生成一个jstring对象:
jobject NewString(const jchar* jstr , int size)
参数:jstr是字符串指针,size是字符串长度。
这个方法可以认为是将字符串指针jstr转换成字符串对象jstring。
4)将jstring对象转换成const jchar*字符串指针。有两个方法:GetStringChars和GetStringUTFChars方法。
GetStringChars方法如下:
const* jchar* GetStringChars(jstring j_msg , jboolean* copied)
返回一个UTF-16编码的宽字符串(jchar*)。
参数如下:
● j_msg是字符串对象。
● copied是指传入的是一个jboolean指针,用来标识是否对Java的String对象进行了拷贝,如果传入的这个jboolean指针不是NULL,则它会给该指针所指向的内存传入JNI_TRUE或JNI_FALSE标识是否进行了拷贝,传入NULL表示不关心是否拷贝字符串,也就不会给jboolean* 指向的内存赋值。
其对应的释放内存指针的方法:
ReleaseStringChars(jstring j_msg , const jchar* jstr)
参数:j_msg是jstring对象,jstr是字符串指针。
GetStringUTFChars方法如下:
const char* GetStringUTFChars(jstring str , jboolean* copied)
这个方法是可以取得UTF-8编码的字符串(char*)。参数的含义和GetStringChars方法是一样的。这个方法也有对应的一个释放内存的方法:
ReleaseStringUTFChars(jstring jstr , const char*str)
参数的含义和上面的ReleaseStringChars方法的参数的含义是一样的。
提示:这两个函数分别都会有两个不同的动作:
● 开辟一个新内存,然后在Java中的String拷贝到这个内存中,然后返回指向这个内存地址的指针。
● 直接返回指向Java中String的内存的指针,这个时候千万不要改变这个内存的内容,这个将会破坏String在Java中始终是常量的这个原则。
5)将jstring对象转化成const jchar*字符串指针:
const jchar* GetStringCritical(jstring j_msg , jboolean* copied);
参数j_msg是字符串对象,copied同上面的解释,这里就不多说了。
这个方法的作用是为了增加直接传回指向Java字符串的指针的可能性(而不是拷贝), JDK 1.2出来了新的函数GetStringCritical/ReleaseStringCritical。
在GetStringCritical/ReleaseStringCritical之间是一个关键区,在这个关键区域之间不能调用JNI的其他函数,否则将造成关键区代码执行期间垃圾回收器停止运作,任何触发垃圾回收器的线程也会暂停,其他的触发垃圾回收器的线程不能前进直到当前线程结束而激活垃圾回收器。就是说在关键区域中千万不要出现中断操作,或在JVM中分配任何新对象;否则会造成JVM死锁。虽然这个函数会增加直接传回指向Java字符串的指针的可能性,不过还是会根据情况传回拷贝过的字符串。不支持GetStringUTFCritical,没有这样的函数,由于Java字符串用的是UTF-16,要转成UTF-8编码的字符串始终需要进行一次拷贝,所以没有这样的函数。
这个方法和第四个方法是一样的。其对应的释放内存指针的方法如下:
env->ReleaseStringCritical(jstring j_msg , const jchar* jstr)
下面来看一下实例:在Java中定义一个String属性,通过控制台输入值,然后定义一个本地方法callCppFunction,在C++中这个方法的实现就是:获取到Java中这个字符串属性,将其进行倒序操作,然后再从Java中输出。
先来看一下Java代码:
package com.jni.demo; import java.io.BufferedReader; import java.io.InputStreamReader; public class JNIDemo { //定义一个本地方法 public native void callCppFunction(); //定义一个String属性 public String msg = null; public static void main(String[] args)throws Exception{ //调用动态链接库 System.loadLibrary("JNIDemo"); //从控制台中获取值 BufferedReader reader=new BufferedReader(new InputStreamReader(System.in)); String str = reader.readLine(); JNIDemo jniDemo = new JNIDemo(); jniDemo.msg = str; jniDemo.callCppFunction(); System.out.println(jniDemo.msg); } }
再来看一下C++代码:
#include<iostream> #include"com_jni_demo_JNIDemo.h" #include"windows.h" #include<string> #include<algorithm> using namespace std; JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_callCppFunction (JNIEnv * env, jobject obj) { //获取Java中的属性:msg jfieldID fid_msg = env->GetFieldID(env->GetObjectClass(obj), "msg", "Ljava/ lang/String; "); //获取属性msg的对象 jstring j_msg = (jstring)env->GetObjectField(obj, fid_msg); /**第一种方式START*/ /* //获得字符串指针 const jchar* jstr = env->GetStringChars(j_msg, NULL); //转换成宽字符串 wstring wstr((const wchar_t*)jstr); //释放指针 env->ReleaseStringChars(j_msg, jstr); */ /**第一种方式END*/ /**第二种方式START*/ /* //获取字符串指针 const jchar* jstr = env->GetStringCritical(j_msg, NULL); //转换成宽字符串 wstring wstr((const wchar_t*)jstr); //释放指针 env->ReleaseStringCritical(j_msg, jstr); */ /**第二种方式END*/ /**第三种方式START*/ //获取字符串的长度 jsize len = env->GetStringLength(j_msg); //生成长度为len的字符串指针 jchar* jstr = new jchar[len+1]; //C++中字符串以’\0’结尾,不然会输出意想不到的字符 jstr[len] = L'\0'; //将字符串j_msg复制到jstr中 env->GetStringRegion(j_msg,0, len, jstr); //转换成宽字符串 wstring wstr((const wchar_t*)jstr); //释放指针 delete[] jstr; /**第三种方式END*/ //将字符串进行倒序 reverse(wstr.begin(), wstr.end()); //获取倒序后新的字符串 jstring j_new_str = env->NewString((const jchar*)wstr.c_str(), (jint)wstr.size()); //将新的字符串设置变量中 env->SetObjectField(obj, fid_msg, j_new_str); }
这里使用了三种方式实现功能。要注意的是,还有一个方法是将const jchar*转换成wstring,因为reverse方法接受的参数是wstring。在Eclipse中的运行结果如图2-28所示。
图2-28 Eclipse中的运行结果
2.6 C/C++中操作Java中的数组
在Java中数组分为两种:
● 基本类型数组。
● 对象类型(Object[])的数组(数组中存放的是指向Java对象中的引用)。
一个能用于两种不同类型数组的函数是GetArrayLength(jarray array)。
2.6.1 操作基本类型数组
首先来看一下怎么处理基本类型的数组,有如下几种方法。
1. Get<Type>ArrayElements方法
Get<Type>ArrayElements(<Type>Array arr , jboolean* isCopide)
这类函数可以把Java基本类型的数组转换到C/C++中的数组,有两种处理方式,一种是拷贝一份传回本地代码,另一种是把指向Java数组的指针直接传回到本地代码中,处理完本地化的数组后,通过Release<Type>ArrayElements来释放数组。
2. Release<Type>ArrayElements方法
Release<Type>ArrayElements(<Type>Array arr , <Type>* array , jint mode)
用这个函数可以选择将如何处理Java和C++的数组,是提交,还是撤销等,内存释放还是不释放等。
mode可以取下面的值:
● 0:对Java的数组进行更新并释放C/C++的数组。
● JNI_COMMIT:对Java的数组进行更新但是不释放C/C++的数组。
● JNI_ABORT:对Java的数组不进行更新,释放C/C++的数组。
3. GetPrimittiveArrayCritical方法
GetPrimittiveArrayCritical(jarray arr , jboolean* isCopied)
4. ReleasePrimitiveArrayCritical方法
ReleasePrimitiveArrayCritical(jarray arr , void* array , jint mode)
也是JDK1.2出来的函数,为了增加直接传回指向Java数组的指针而加入的函数,同样也会有同GetStringCritical一样死锁的问题。
5. Get<Type>ArrayRegion方法
Get<Type>ArrayRegion(<Type>Array arr , jsize start , jsize len , <Type>* buffer)
在C/C++预先开辟一段内存,然后把Java基本类型的数组拷贝到这段内存中,这个方法和之前拷贝字符串的GetStringRegion方法的原理是类似的。
6. Set<Type>ArrayRegion方法
Set<Type>ArrayRegion(<Type>Array arr , jsize start , jsize len , const <Type>* buffer)
把Java基本类型数组中的指定范围的元素用C/C++数组中的元素来赋值。
7. <Type>ArrayNew方法
<Type>ArrayNew<Type>Array(jsize sz)
指定一个长度然后返回相应的Java基本类型的数组。
2.6.2 操作对象类型数组
JNI没有提供把Java对象类型数组(Object[])直接转到C++中的Object[]数组的函数,而是通过Get/SetObjectArrayElement这样的函数来对Java的Object[]数组进行操作。由于对象数组没有进行拷贝,所以不需要释放任何资源。NewObjectArray可以通过指定长度和初始值来创建某个类的数组。
下面来看个例子:操作两种类型的数组。
Java中的代码:
package com.jni.demo; public class JNIDemo { //定义一个int型数组 int[] arrays = {4,3,12,56,1,23,45,67}; //定义Father对象数组 Father[] objArrays = {new Father(), new Father(), new Father()}; //定义一个本地方法 public native void callCppFunction(); public static void main(String[] args)throws Exception{ //调用动态链接库 System.loadLibrary("JNIDemo"); JNIDemo jniDemo = new JNIDemo(); jniDemo.callCppFunction(); } }
C++中的代码:
#include<iostream> #include"com_jni_demo_JNIDemo.h" #include<algorithm> using namespace std; JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_callCppFunction (JNIEnv * env, jobject obj) { //获取Java中数组属性arrays的id jfieldID fid_arrays = env->GetFieldID(env->GetObjectClass(obj), "arrays", "[I"); //获取Java中数组属性arrays的对象 jintArray jint_arr = (jintArray)env->GetObjectField(obj, fid_arrays); //获取arrays对象的指针 jint* int_arr = env->GetIntArrayElements(jint_arr, NULL); //获取数组的长度 jsize len = env->GetArrayLength(jint_arr); //打印数组中的值 cout<<"数组的值为:"; for(int s =0; s<len; s++){ cout<<int_arr[s]<<', '; } cout<<endl; //新建一个jintArray对象 jintArray jint_arr_temp = env->NewIntArray(len); //获取jint_arr_temp对象的指针 jint* int_arr_temp = env->GetIntArrayElements(jint_arr_temp, NULL); //计数 jint count = 0; //偶数位存入到int_arr_temp内存中 for(jsize j=0; j<len; j++){ if(j%2==0){ int_arr_temp[count++] = int_arr[j]; } } //打印int_arr_temp内存中的数组 cout<<"数组中位置是偶数的值为:"; for(jsize k=0; k<count; k++){ cout<<int_arr_temp[k]<<', '; } cout<<endl; //将数组中一段(0-2)数据拷贝到内存中,并且打印出来 jint* buffer = new jint[len]; //获取数组中从0开始长度为3的一段数据值 env->GetIntArrayRegion(jint_arr,0,3, buffer); cout<<"打印数组中0-3一段值:"; for(int l=0; l<3; l++){ cout<<buffer[l]<<', '; } cout<<endl; //将数组中的一段(3-7)设置成一定的值,并且打印出来 jint* buffers = new jint[4]; for(int n=0; n<4; n++){ buffers[n] = n+1; } //将buffers这个数组中值设置到数组从3开始长度是4的值中 env->SetIntArrayRegion(jint_arr,3,4, buffers); //从新获取数组指针 int_arr = env->GetIntArrayElements(jint_arr, NULL); cout<<"数组中3-7这段的值变成了:"; for(int m=0; m<len; m++){ cout<<int_arr[m]<<', '; } cout<<endl; //调用C++标准库中的排序方法sort(...),传递一个数组的开始指针和结束指针 std::sort(int_arr, int_arr+len); //迭代打印数组中的元素 cout<<"数组排序后的结果:"; for(jsize i=0; i<len; i++){ cout<<int_arr[i]<<', '; } cout<<endl; //释放数组指针 env->ReleaseIntArrayElements(jint_arr, int_arr, JNI_ABORT); //获取Java中对象Father数组属性的id jfieldID fid_obj_arrays = env->GetFieldID(env->GetObjectClass(obj), "objArrays", "[Lcom/jni/demo/Father; "); //获取Java中对象数组Father属性objArrays的对象 jobjectArray jobj_arr = (jobjectArray)env->GetObjectField(obj, fid_obj_arrays); //从对象数组中获取索引值为1的对象Father jobject jobj = env->GetObjectArrayElement(jobj_arr,1); //获取Father对象的class对象 jclass clazz_father = env->GetObjectClass(jobj); //获取Father对象中的function方法的id jmethodID id_father_function = env->GetMethodID(clazz_father, "function", "()V"); //调用Father对象中的function方法 env->CallVoidMethod(jobj, id_father_function); //在本地创建一个大小为10的对象数组,对象的初始化都是jobj, //也就是方法的第三个参数 jobjectArray jobj_arr_temp=env->NewObjectArray(10, env->GetObjectClass(jobj), jobj); //获取本地对象数组中第4个对象 jobject jobj_temp = env->GetObjectArrayElement(jobj_arr_temp,3); //调用Father对象中的function方法 env->CallVoidMethod(jobj_temp, id_father_function); }
在Eclipse编译运行,结果如图2-29所示。
图2-29 在Eclipse中运行的结果
2.7 C/C++中的引用类型和ID的缓存
2.7.1 引用类型
从Java虚拟机创建的对象传到本地C/C++代码时会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该引用所指的Java对象的垃圾回收。下面介绍C/C++中的引用类型。
1.局部引用
局部引用是最常见的引用类型,基本上通过JNI返回来的引用都是局部引用,例如使用NewObject就会返回创建出来的实例的局部引用,局部引用只在该native函数中有效,所有在该函数中产生的局部引用,都会在函数返回的时候自动释放,也可以使用DeleteLocalRef函数手动释放该引用。那么,既然局部引用能够在函数返回时自动释放,为什么还需要DeleteLocalRef函数呢。
实际上局部引用存在是防止其指向的对象被垃圾回收,尤其是当一个局部引用指向一个很庞大的对象,或是在一个循环中生成了局部引用。最好的做法就是在使用完该对象后,在该循环尾部把这个引用释放掉,以确保在触发垃圾回收器的时候能够回收。
在局部引用的有效期中,可以传递到别的本地函数中,要强调的是它的有效期仍然只在一次的Java本地函数调用中,所以千万不能用C++全局变量保存它或者把它定义为C++静态局部变量。
2.全局引用
全局引用可以跨越当前线程,在多个native函数中有效,不过需要编程人员手动来释放该引用,全局引用存在期间会防止在Java的垃圾回收器的回收。
与局部引用不同,全局引用的创建不是由JNI自动创建的,全局引用是需要调用NewGlobalRef函数,而释放它需要使用ReleaseGlobalRef函数。
3.弱全局引用
弱全局引用是Java 1.2新出来的功能,与全局引用相似,创建和删除都需要由编程人员来进行,这种引用与全局引用一样可以在多个本地代码中有效,也跨越多线程有效。不一样的是,这种引用将不会阻止垃圾回收器回收这个引用所指向的对象,使用NewWeakGlobalRef和ReleaseWeakGlobalRef来产生和解除引用。
关于引用的一个函数如下:
jobject NewGlobalRef(jobject obj); jobject NewLocalRef(jobject obj); jobject new WeakGlobalRef(jobject obj); void DeleteGobalRef(jobject obj); void DeleteLocalRef(jobject obj); void DeleteWeakGlobalRef(jobject obj);
上述的六种方法很好理解,这里就不做解释了。
jboolean IsSameObject(jobject obj1 , jobject obj2);
这个函数是用来比较两个引用是否相等,但是对于弱全局引用还有一个特别的功能,如果把NULL传入要比较的对象中,就能够判断弱全局引用所指向的Java对象是否被回收。
缓存jfi eldID/jmethodID,取得jfi eldID和jmethodID的时候会通过该属性/方法名称加上签名来查询相应的jfi eldID/jmethodID。这种查询相对来说开销大,我们可以将这些FieldID/MethodID缓存起来,这样就需要查询一次,以后就是用缓存起来的FieldID/MethodID了。
2.7.2 缓存方法
1.在用的时候缓存
在native代码中使用static局部变量来保存已经查询过的id,这样就不会在每次函数调用时查询,而只要第一次查询成功后就保存起来了。不过在这种情况下就不得不考虑多线程同时调用此函数时可能会招致同时查询的危机,不过这种情况是无害的,因为查询同一个属性方法的ID通常返回的是一样的值:
JNIEXPORT void JNICALL Java_Test_native(JNIEnv* env, jobject obj){ static jfieldID fieldID_string = NULL; jclass clazz = env-GetObjectClass(obj); if(fieldId_string == NULL){ fieldId_string = env-GetFieldID( clazz, "string", "Ljava/lang/String; "); } }
static jfieldID fieldID_string = NULL;这段代码只执行一次。
2.在Java类初始化时缓存
更好的一个方式是在任何native函数调用前把ID全部存起来,可以让Java在第一次加载这个类的时候首先调用本地代码初始化所有的jfieldID/jmethodID,这样就可以省去多次确定ID是否存在的语句。当然,这些jfieldID/jmethodID是定义在C/C++的全局,使用这种方式还是有好处的,当Java类卸载或者重新加载的时候,也会调用该本地代码来重新计算ID的。
//Java代码 public class TestNative{ static{ initNativeIDs(); } static native void initNativeIDs(); int propInt = 0; String propStr = ""; public native void otherNative(); ..... } //Native代码 //global variables jfieldID g_propInt_id = 0; jfieldID g_propStr_id = 0; JNIEXPORT void JNICALL Java_TestNative_initNativeIDs(JNIEnv* env, jobject clazz){ g_propInt_id = GetFieldID(clazz, "propInt", "I"); g_propStr_id = GetFieldID(clazz, "propStr", "Ljava/lang/String; "); } JNIEXPORT void JNICALL Java_TestNative_otherNative(JNIEnv* env, jobject obj){ //get field with g_propInt_id/g_propStr_id... }
在Java中使用静态代码块进行初始化。
2.8 本章小结
本章主要介绍了Android中的NDK开发,其实Android中的NDK就是Java中的JNI,两者没有本质区别,特别是在语法和开发流程上几乎是一样的。后续章节有很多地方会用到这里的相关知识,建议读者能够自己独立编写出一个native的案例,为后面的学习做准备。