在前面的章节我们学习了JNI/NDK入门指南之C/C++通过JNI访问Java实例属性和类静态属性和JNI/NDK入门指南之C/C++通过JNI访问Java实例方法和类静态方法,其中都有讲到调用构造函数来实例化一个对象,但是都没有详细介绍过,只是讲了一个大概的用法。在本章我们将深入挖掘,分别介绍一下初始化一个对象的两种方法,以及如何调用子类对象重写父类的实例方法。
由于本章会牵涉到许多的对象操作的函数,所以还是老规矩让我们先行热热身了解了解JNI对象操作相关函数,后续再正式进入本章的主题。
函数原型: jobject AllocObject (JNIEnv *env, jclass clazz)
函数功能:创建一个未被初始化的Java对象并分配内存空间。
函数参数:
异常抛出:
这是一系列函数组集合,功能都是一致,只是JNI根据参数传入方式的不同而定制了一系列的马甲给穿上,追根究底它们还是同一个东西。
函数原型:
jobject NewObject (JNIEnv *env , jclass clazz, jmethodID methodID, …) //参数附加在函数后面,通常使用该函数
jobject NewObjectA (JNIEnv *env , jclassclazz, jmethodID methodID, jvalue *args) //参数以指针形式附加
jobjec tNewObjectV (JNIEnv *env , jclassclazz, jmethodID methodID, va_list args) //参数以"链表"形式附加
函数组关系:
jobject NewObject(jclass clazz, jmethodID methodID, ...)
{
va_list args;
va_start(args, methodID);
jobject result = functions->NewObjectV(this, clazz, methodID, args);
va_end(args);
return result;
}
jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)
{ return functions->NewObjectV(this, clazz, methodID, args); }
jobject NewObjectA(jclass clazz, jmethodID methodID, jvalue* args)
{ return functions->NewObjectA(this, clazz, methodID, args); }
函数功能:通过构造方法methodID创建新的Java对象实例。这里的methodID一定要是Java类对象的构造方法的ID,并且要通过 GetMethodID() 获得,且调用时的方法名必须为 ,返回类型有且仅必须为 void (V)。
函数参数:
返回值: 返回Java对象实例,如果无法构造该对象,则会返回NULL。
异常抛出:
函数原型: jclass GetObjectClass (JNIEnv *env, jobject obj)
函数功能:根据Java对象实例获取Java类对象
函数参数:
函数返回值: 返回Java类对象
函数原型: jboolean IsInstanceOf (JNIEnv *env, jobject obj, jclass clazz)
函数功能: 判断测试对象是否为某个类的实例。
参数:
返回值: 如果obj是clazz的对象实例,则返回JNI_TRUE,否则返回JNI_FALSE。
函数原型: jboolean IsSameObject (JNIEnv *env, jobject ref1, jobject ref2)
函数功能: 判断两个Java引用是否引用自同一个Java对象。
函数参数
返回值: 如果 ref1 和 ref2 引用同一 Java 对象或均为 NULL,则返回 JNI_TRUE。否则返回 JNI_FALSE,同时当ref2为NULL的时候可以用于检测弱全局引用是否被GC了。
这里的PrimitiveType指代的是一系列的JNI数据类型,如果是引用类型的话PrimitiveType用Object替代。
函数原型: NativeType CallNonvirtual
函数功能:调用被子类覆盖的父类方法,或者调用构造函数
使用说明: 在实际使用中,根据需要调用方法将 CallNonvirtual
void (*CallNonvirtualVoidMethod)(JNIEnv*, jobject, jclass,jmethodID, ...);
函数返回值: 返回调用Java方法的结果。
在开始JNI中调用构造方法和调用父类实例方法相关的介绍前,让我们看看在Java层是怎么做的,然后我们在JNI层照葫芦画瓢的实现。好了不多说了,直接上代码走起!
定义一个Java父类People,代码如下:
import android.util.Log;
public class People {
private static final String TAG = "AccessSuperMethod";
public void feature() {
Log.e(TAG, "People Can Walk upright");// 人吗,当然是能直立行走
}
protected String name;
public People(String name) {
this.name = name;
Log.e(TAG, "People Construct call...");
}
public String getName() {
Log.e(TAG, "People.getName Call...");
return this.name;
}
}
定义一个Java子类Programmer继承People,代码如下:
package com.study.jni.object_opertate;
import android.util.Log;
public class Programmer extends People {
public Programmer(String name) {
super(name);
Log.e(TAG, "Programmer Construct call....");
}
private static final String TAG = "OPERATE_OBJECT";
@Override
public void feature() {
super.feature();
Log.e(TAG, "Ha ha, Programmer can coding");// 我会编程
}
@Override
public String getName() {
super.getName();
Log.e(TAG, "Programmer.getName Call...");
return "My name is " + this.name;
}
}
Java端测试代码:
private void operateJavaObject() {
People mPeople = new Programmer("Java");
mPeople.feature();
mPeople.getName();
}
运行演示
E/AccessSuperMethod( 7175): People Construct call...
E/AccessSuperMethod( 7175): Programmer Construct call....
E/AccessSuperMethod( 7175): People Can Walk upright
E/AccessSuperMethod( 7175): Ha ha, Programmer can coding
E/AccessSuperMethod( 7175): People.getName Call...
E/AccessSuperMethod( 7175): Programmer.getName Call...
前面的代码非常简单,都是一些入门级的Java代码,当然这里也主要是为了演示使用。
(1) 上述的代码中定义了两个类People和Programmer并且是父子关系。People类中定义了feature方法和getName方法,Programmer继承自People并重写了feather和getName方法。
(2) 在测试代码中,首先定义了一个People类型的变量mPeople,并指向了Programmer类的实例对象,然后调用它的feather方法和getName方法。
(3) 在执行new Programmer(“Java”)这段代码时,会先为Programmer类分配内存空间(所分配的内存空间大小由Cat类的成员变量数量决定),然后调用Programmer的带构造方法初始化对象。mPeople是People类型,单它指向的是Programmer实例对象的引用,而且Programmer重写了父类的feather和getName方法,并且在这两个方法里面我们也调用了父类的方法,即调用super,所以你看到了子类和父类都被调用了。这个都是Java的基本知识就不过多啰嗦了。
写过C/C++ 的同学应该都有一个很深刻的内存管理概念,栈空间和堆空间,栈空间的内存大小受操作系统限制,由操作系统自动来管理,速度较快,所以在函数中定义的局部变量、函数形参变量都存储在栈空间。操作系统没有限制堆空间的内存大小,只受物理内存的限制,内存需要程序员自己管理。在 C 语言中用 malloc 关键字动态分配的内存和在 C++ 中用 new 创建的对象所分配内存都存储在堆空间,内存使用完之后分别用 free 或 delete/delete[] 释放。这里不过多的讨论 C/C++ 内存管理方面的知识,有兴趣的同学请自行百度。做 Java 的童鞋众所周知,写 Java 程序是不需要手动来管理内存的,内存管理那些烦锁的事情全都交由一个叫 GC 的线程来管理(当一个对象没有被其它对象所引用时,该对象就会被 GC 释放)。但我觉得 Java 内部的内存管理原理和 C/C++ 是非常相似的,上例中,People mPeople = new Programmer(“Java”),局部变量 mPeople 存放在栈空间上,new Programmer(“Java”)创建的实例对象存放在堆空间,返回一个内存地址的引用,存储在mPeople变量中。这样就可以通过mPeople变量所指向的引用访问Programmer实例当中所有可见的成员了。
所以综上所述,创建一个Java对象的步骤分为如下:
在前面的篇章中,我们演示了怎么在Java中实现调用Java类的构造方法和父类实例方法,那么在本章我们将在JNI本地方法的世界里面来重复这一操作。
Java端代码:
Java端Native方法定义JNIAccessMethodManager.java代码:
package com.study.jni.object_opertate;
public class AccessSuperMethod {
public native void callSuperInstanceMethod();
static {
System.loadLibrary("accessSuperMethod");
}
}
Java端测试代码:
private void operateAccessSuperMethod(){
AccessSuperMethod mAccessSuperMethod = new AccessSuperMethod();
mAccessSuperMethod.callSuperInstanceMethod();
}
JNI端代码:
Java的Native方法对应的com_study_jni_object_opertate_AccessSuperMethod.h代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_study_jni_object_opertate_AccessSuperMethod */
#ifndef _Included_com_study_jni_object_opertate_AccessSuperMethod
#define _Included_com_study_jni_object_opertate_AccessSuperMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_study_jni_object_opertate_AccessSuperMethod
* Method: callSuperInstanceMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_study_jni_object_1opertate_AccessSuperMethod_callSuperInstanceMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
对应的com_study_jni_object_opertate_AccessSuperMethod.cpp代码如下:
#include "com_study_jni_object_opertate_AccessSuperMethod.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define TAG "AccessSuperMethod"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
//将char * 转换成 jstring
jstring nativeTojstring( JNIEnv* env,const char* str )
{
//定义java String类 strClass
jclass strClass = (env)->FindClass("java/lang/String");
//获取java String类方法String(byte[],String)的构造器,用于将本地byte[]数组转换为一个新String
jmethodID ctorID = (env)->GetMethodID( strClass, "" , "([BLjava/lang/String;)V");
//建立byte数组
jbyteArray bytes = (env)->NewByteArray( (jsize)strlen(str));
//将char* 转换为byte数组
(env)->SetByteArrayRegion( bytes, 0, (jsize)strlen(str), (jbyte*)str);
//设置String, 保存语言类型,用于byte数组转换至String时的参数
jstring encoding = (env)->NewStringUTF( "utf-8");
//将byte数组转换为java String,并输出
return (jstring)(env)->NewObject( strClass, ctorID, bytes, encoding);
}
//将jstring转换成char *
char* jstringToNative(JNIEnv *env, jstring jstr)
{
if ((env)->ExceptionCheck() == JNI_TRUE || jstr == NULL)
{
(env)->ExceptionDescribe();
(env)->ExceptionClear();
//printf("jstringToNative函数转换时,传入的参数str为空");
return NULL;
}
jbyteArray bytes = 0;
jthrowable exc;
char *result = 0;
if ((env)->EnsureLocalCapacity(2) < 0)
{
return 0; /* out of memory error */
}
jclass jcls_str = (env)->FindClass("java/lang/String");
jmethodID MID_String_getBytes = (env)->GetMethodID(jcls_str, "getBytes", "()[B");
bytes = (jbyteArray)(env)->CallObjectMethod(jstr, MID_String_getBytes);
exc = (env)->ExceptionOccurred();
if (!exc)
{
jint len = (env)->GetArrayLength( bytes);
result = (char *)malloc(len + 1);
if (result == 0)
{
//JNU_ThrowByName( "java/lang/OutOfMemoryError", 0);
(env)->DeleteLocalRef(bytes);
return 0;
}
(env)->GetByteArrayRegion(bytes, 0, len, (jbyte *)result);
result[len] = 0; /* NULL-terminate */
}
else
{
(env)->DeleteLocalRef( exc);
}
(env)->DeleteLocalRef( bytes);
return (char*)result;
}
/*
* Class: com_study_jni_object_opertate_AccessSuperMethod
* Method: callSuperInstanceMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_study_jni_object_1opertate_AccessSuperMethod_callSuperInstanceMethod
(JNIEnv * env, jobject object)
{
/*****JNI中创建Programmer对象的实例,并调用其父类的feather和getName方法*****/
jclass cls_people = NULL;
jclass cls_programmer = NULL;
jmethodID mid_programmer_construct = NULL;
jmethodID mid_feather = NULL;
jmethodID mid_getName = NULL;
jobject obj_programmer = NULL;
//1.获取Programmer类的引用
cls_programmer = env->FindClass("com/study/jni/object_opertate/Programmer");
if(cls_programmer == NULL)
{
LOGE(TAG,"FindClass Programmer failed\n");
return;
}
//2.获取Programmer的构造方法的ID(构造方法的名统一为:,但是签名得根据具体情况确定)
mid_programmer_construct = env->GetMethodID(cls_programmer, "" , "(Ljava/lang/String;)V");
if(mid_programmer_construct == NULL)
{
LOGE(TAG,"GetMethodID failed\n" );
return;
}
//3.创建一个jstring对象作为Programmer构造函数的参数
jstring s_name = nativeTojstring(env,"Hello Programmer");
if(s_name == NULL)
{
LOGE(TAG,"nativeTojstring failed, may be memory not enough\n");
return;
}
//4.创建Programmer实例对象(调用构造方法,并初始化对象)
obj_programmer = env->NewObject(cls_programmer, mid_programmer_construct, s_name);
if(obj_programmer == NULL)
{
LOGE(TAG,"NewObject Programmer failed\n");
return;
}
//5.调用Programmer父类People的feather和getName方法
cls_people = env->FindClass("com/study/jni/object_opertate/People");
if(cls_people == NULL)
{
LOGE(TAG,"FindClass People failed\n");
return;
}
//6.判断obj_programmer是否是cls_people的实例
jboolean flag = env->IsInstanceOf(obj_programmer, cls_people);
if(flag)
{
LOGE(TAG,"obj_programmer IsInstanceOf cls_people\n");
}
flag = env->IsInstanceOf(obj_programmer, cls_programmer);
if(flag)
{
LOGE(TAG,"obj_programmer IsInstanceOf cls_programmer\n");
}
//获取父类People中的feather方法ID
mid_feather = env->GetMethodID(cls_people, "feature", "()V");
if(mid_feather == NULL)
{
LOGE(TAG,"GetMethodID feature failed\n");
return;
}
//7.调用CallNonvirtualVoidMethod获取被子类覆盖的父类方法,即Programmer父类的方法
//其中obj_programmer是Programmer的实例,cls_people是People的类引用,mid_feather是People类中的方法ID
env->CallNonvirtualVoidMethod(obj_programmer, cls_people, mid_feather);
//8.获取父类People中的getName方法ID
mid_getName= env->GetMethodID(cls_people, "getName", "()Ljava/lang/String;");
if(mid_getName == NULL)
{
LOGE(TAG,"GetMethodID getName failed\n");
return;
}
//9.调用CallNonvirtualVoidMethod获取被子类覆盖的父类方法,即Programmer父类的方法
//其中obj_programmer是Programmer的实例,cls_people是People的类引用,mid_getName是People类中的方法ID
s_name = (jstring)env->CallNonvirtualObjectMethod(obj_programmer, cls_people, mid_getName);
LOGE(TAG,"The peopel getName %s\n", jstringToNative(env, s_name));
exit:
//10.删除局部引用
env->DeleteLocalRef(s_name);
env->DeleteLocalRef(cls_people);
env->DeleteLocalRef(cls_programmer);
env->DeleteLocalRef(obj_programmer);
}
运行演示:
E/AccessSuperMethod( 8464): People Construct call...
E/AccessSuperMethod( 8464): Programmer Construct call....
I/AccessSuperMethod( 8464): obj_programmer IsInstanceOf cls_people
I/AccessSuperMethod( 8464): obj_programmer IsInstanceOf cls_programmer
E/AccessSuperMethod( 8464): People Can Walk upright
E/AccessSuperMethod( 8464): People.getName Call...
I/AccessSuperMethod( 8464): The peopel getName Hello Programmer
好了,通过前面的代码,我们成功的在JNI中调用了Java构造方法和父类实例方法,那么下面让我们仔细分析分析,一一突破!
JNI调用Java构造方法其实和调用Java普通方法的步骤都是一样的,只是在细节上处理有一丁点区别,下面让我们跟着代码解读一下:
(1) 通过JNI函数FindClass获取Programmer的类引用,并且这里也一定要做异常处理,因为可能查找的类不存在。
(2) 通过JNI函数GetMethodID获取构造方法的MethodID,这里有一点一定要注意的就是构造方法的名字有且仅可能是""。这个读者也不要较真为啥一定是这个呢,因为有些规则是约定俗成的就是这样的。
(3) 调用JNI函数NewObject创建Programmer实例对象,这段代码主要做了两件事情
//方式二
// 1、创建一个未初始化的对象,并分配内存
obj_programmer= env->AllocObject(cls_programmer);
if (obj_programmer) {
// 2、调用对象的构造函数初始化对象
(env)->CallNonvirtualVoidMethod(obj_programmer, cls_programmer, mid_programmer_construct, s_name);
if (env->ExceptionCheck()) { // 检查异常
goto exit
}
AllocObject 函数创建的是一个未初始化的对象,后面在用这个对象之前,必须调用CallNonvirtualVoidMethod调用对象的构造函数初始化该对象。而且在使用时一定要非常小心,确保在一个对象上面,构造函数最多被调用一次。有时,先创建一个初始化的对象,然后在合适的时间再调用构造函数的方式是很有用的。尽管如此,大部分情况下,应该使用 NewObject,尽量避免使用容易出错的 AllocObject/CallNonvirtualVoidMethod 函数。
如果一个方法被定义在父类中,在子类中被覆盖,也可以调用父类中的这个实例方法。JNI 提供了一系列函数CallNonvirtualXXXMethod 来支持调用各种返回值类型的实例方法。下面我们参照前面的实例代码一一解读:
(1) 调用JNI函数FindClass获取Programmer父类的People的类对象引用。
(2) 调用JNI函数IsInstanceOf判断Programmer实例是否是People的实例引用,通过代码和逻辑分析我们都知道是的。
(3) 调用GetMethodID获取父类People中的faether方法ID。
(4) 最后调用JNI函数CallNonvirtualVoidMethod获取被子类覆盖的父类方法,这里传入的参数分别是子类对象Programmer实例,父类People对象引用,父类People方法feather的ID,当然CallNonvirtualVoidMethod 也可以被用来调用父类的构造函数。也许读者会说,这个场景存在吗,是的这个在实际开发当中是很少遇到的。但是掌握多一点招式,还是有必要的吗,这样才能解锁更多动作吗。
各位读者看官朋友们,关于调用Java构造方法和父类实例方法就告一段落了。也不总结了,偷懒一次,因为确实没有啥好总结的。在最后麻烦读者朋友们如果本篇对你有帮助,关注和点赞一下,当然如果有错误和不足的地方也可以拍砖。