29
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 所示。

Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

  • Upload
    others

  • View
    40

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 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 所示。

Page 2: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 11

图 2-2 新建 Android 项目

点击 Add Native Support,出现如图 2-3 所示的 lib 命令。

点击“Finish”,再次观察项目多了 jni 文件夹,如图 2-4 所示。

图 2-3 命令 lib 图 2-4 添加了 jni 文件夹

在 jni 下面就可以开始编写 native 层的代码。

第三步:使用 javah 生成 native 的头文件,如图 2-5 所示。

图 2-5 生成 native 头文件

Page 3: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇12

注意: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 所示。

第二步:用 javah 命令生成 native 的头文件,如图 2-10 所示。

第三步:配置项目的 NDK 目录,如图 2-11 所示。

Page 4: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 13

图 2-9 新建 jni 目录

图 2-10 生成 native 头文件

图 2-11 配置 NDK 目录

Page 5: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇14

选择模块的设置选项 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.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){

//调用动态链接库

图 2-14 项目的 libs 目录

Page 6: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 15

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。

 在 bin\classes 目录下运行命令,但是如果一个类引用了系统的 api,会提示找不到指定的类文件,所以需要到 src 目录下运行命令

 需要切换到 src 目录下,运行命令,然后

就可以产生指定的头文件在 src 目录下

图 2-15 javah 命令参数说明

注意:如果包含 native 方法的类,引用其他地方的类,那么进入 bin\classes\ 目录下会出现

问题提示找不到指定的类,这时候需要切换到源码目录 src 下运行即可。

第三步:使用 VC6.0 生成 .dll 文件。

首先创建一个 dll 工程,如图 2-16 ~图 2-18 所示。

在 .cpp 文件中输入如下代码:

#include<iostream.h>

#include "com_jni_demo_JNIDemo.h"

JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj)

Page 7: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇16

{

cout<<"Hello World"<<endl;

}

图 2-16 VC6.0 生成 .dll 文件 图 2-17 VC6.0 生成 .dll 文件

图 2-18 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。Java 安装目录中的 include 文件夹下,jni_md.h 文件在 win32 文件夹中,找到这两个文

件后,将其拷贝到 C++ 的工程目录中。

Page 8: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 17

图 2-19 编译之后的头文件

图 2-20 jni.h 文件

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 所示。

这样,编译成功,生成 JNIDemo.dll 文件在 C++ 工程中的 Debug 目录中。

注意:因为之前开发都是使用 VC 工具,所以这里使用了 VC 6.0 来进行 C++ 代码的编写和

运行,其实可以直接使用 Eclipse 或在 Android Studio 中也可以进行编写,这样会更方便。

第四步:将 JNIDemo.dll 文件添加到环境变量中,如图 2-22 所示。

注意:在用户变量中的 path 设置,用分号隔开: “ ;E:\C++\Debug”,这样就将 .dll 文件添加

到环境变量中了。

第五步:在 Eclipse 中调用 sayHello 方法,输出“Hello World”。代码如下:

public static void main(String[] args){

//调用动态链接库 System.loadLibrary("JNIDemo");

JNIDemo jniDemo = new JNIDemo();

Page 9: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇18

jniDemo.sayHello();

}

图 2-21 预编译头文件 图 2-22 将 JNIDemo.dll 文件添加到环境变量中

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)

{

Page 10: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 19

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++ 中的基本类型的映射关系

Java 类型 本地类型 JNI 定义的别名 Java 类型 本地类型 JNI 定义的别名

int long jint/jsize short short jshort

long __int64 jlong float float jfloat

byte signed char jbyte double double jdouble

boolean unsigned char jboolean Object _ jobject* jobject

char unsigned short jchar

具体的说明可以查看 jni.h 文件。

2.3.4 jclass 类型

为了能够在 C/C++ 中使用 Java 类,jni.h 头文件中专门定义了 jclass 类型来表示 Java 中

Page 11: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇20

的 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 对象时可以调用指定

Page 12: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 21

的构造方法,后续将向大家介绍,如:

env->GetMethodID(data_Class,"<init>","()V");

签名的格式见表 2-2。

表 2-2 签名的格式

类型 相应的签名 类型 相应的签名

boolean Z float F

byte B double D

char C void V

short S object L 用 / 分割包的完整类名;Ljava/lang/String;

int I Array [ 签名 [I [Ljava/lang/Object;

long L Method (参数类型签名….)返回值类型签名

下面来看一例子:

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

Page 13: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇22

代码中定义一个属性,然后再从 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"

JNIE XPORT 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 所示。

第一个 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);

图 2-24 运行成功效果图

Page 14: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 23

第一种是最常用的方式。第二种是当调用这个函数的时候有一个指向参数表的 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;

}

Page 15: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇24

这时候用 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");

}

}

//这行代码中执行的结果是什么?

Page 16: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 25

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 类:

Page 17: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇26

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 所示。

Page 18: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 27

图 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"

JNIE XPORT 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

Page 19: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇28

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"

JNIE XPORT 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 对

Page 20: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 29

象是 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* 指向的内存赋值。

其对应的释放内存指针的方法:

Page 21: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇30

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;

Page 22: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 31

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;

JNIE XPORT void JNICALL Java_com_jni_demo_JNIDemo_callCppFunction (JNIEnv * env,

jobject obj)

{

//获取 Java中的属性 :msg

jfie ldID 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);

//释放指针

Page 23: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇32

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)

Page 24: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 33

这类函数可以把 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[] 数组的函数,

Page 25: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇34

而是通过 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;

JNIE XPORT 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;

Page 26: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 35

//偶数位存入到 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

Page 27: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇36

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 函数呢。

实际上局部引用存在是防止其指向的对象被垃圾回收,尤其是当一个局部引用指向一

个很庞大的对象,或是在一个循环中生成了局部引用。最好的做法就是在使用完该对象后,

在该循环尾部把这个引用释放掉,以确保在触发垃圾回收器的时候能够回收。

Page 28: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

第 2 章 Android 中 NDK 开发 37

在局部引用的有效期中,可以传递到别的本地函数中,要强调的是它的有效期仍然只

在一次的 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 通常返回的是一样的值:

Page 29: Android 中NDK 开发 - images.china-pub.comimages.china-pub.com/ebook6995001-7000000/6998726/ch02.pdf · 第2 章 Android 中NDK 开发 11 图2-2 新建Android 项目 点击Add Native

基 础 篇38

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 的案例,为后面的学习做准备。