JNI/NDK入门指南之调用Java构造方法和父类实例方法

   JNI/NDK入门指南之调用Java构造方法和父类实例方法


前言

  在前面的章节我们学习了JNI/NDK入门指南之C/C++通过JNI访问Java实例属性和类静态属性和JNI/NDK入门指南之C/C++通过JNI访问Java实例方法和类静态方法,其中都有讲到调用构造函数来实例化一个对象,但是都没有详细介绍过,只是讲了一个大概的用法。在本章我们将深入挖掘,分别介绍一下初始化一个对象的两种方法,以及如何调用子类对象重写父类的实例方法。




一. 初探JNI对象操作相关函数

由于本章会牵涉到许多的对象操作的函数,所以还是老规矩让我们先行热热身了解了解JNI对象操作相关函数,后续再正式进入本章的主题。

1.1 AllocObject

函数原型: jobject AllocObject (JNIEnv *env, jclass clazz)
函数功能:创建一个未被初始化的Java对象并分配内存空间。
函数参数:

  • env: JNIEnv接口指针
  • clazz: Java类对象引用
    返回值: 返回Java对象。如果无法创建该对象则返回NULL。

异常抛出

  • InstantiationException:如果该类为一个接口或抽象类。
  • OutOfMemoryError:如果系统内存不足。

1.2 NewObject函数组

这是一系列函数组集合,功能都是一致,只是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)。
函数参数:

  • env:JNIEnv接口指针
  • clazz: Java类对象
  • methodID构造函数的方法ID
  • NewObject 的其它参数: …(这里不是省略) 传给构造函数的参数,可以为空
    NewObjectA 的其它参数: args:传给构造函数的参数数组
    NewObjectV 的其它参数: args:传给构造函数的参数 va_list

返回值: 返回Java对象实例,如果无法构造该对象,则会返回NULL。

异常抛出:

  • InstantiationException: 如果该类为接口或抽象类时将会抛出该异常
  • OutOfMemoryError: 如果分配给改进程的系统内存不足,将会抛出该异常

1.3 GetObjectClass

函数原型: jclass GetObjectClass (JNIEnv *env, jobject obj)
函数功能:根据Java对象实例获取Java类对象
函数参数:

  • env: JNIEnv接口指针
  • obj: Java对象实例

函数返回值: 返回Java类对象


1.4 IsInstanceOf

函数原型: jboolean IsInstanceOf (JNIEnv *env, jobject obj, jclass clazz)
函数功能: 判断测试对象是否为某个类的实例。
参数:

  • env: JNIEnv接口指针
  • obj: Java对象实例
  • clazz: Java类对象

返回值: 如果obj是clazz的对象实例,则返回JNI_TRUE,否则返回JNI_FALSE。


1.5 IsSameObject

函数原型: jboolean IsSameObject (JNIEnv *env, jobject ref1, jobject ref2)
函数功能: 判断两个Java引用是否引用自同一个Java对象。

函数参数

  • env: JNIEnv接口指针
  • ref1: Java实例对象引用
  • ref2: Java实例对象引用

返回值: 如果 ref1 和 ref2 引用同一 Java 对象或均为 NULL,则返回 JNI_TRUE。否则返回 JNI_FALSE,同时当ref2为NULL的时候可以用于检测弱全局引用是否被GC了。


1.6 CallNonvirtualMethod

这里的PrimitiveType指代的是一系列的JNI数据类型,如果是引用类型的话PrimitiveType用Object替代。
函数原型: NativeType CallNonvirtualMethod(JNIEnv* env, jobject object, jclass clazz, jmethodID, …)

函数功能:调用被子类覆盖的父类方法,或者调用构造函数
使用说明: 在实际使用中,根据需要调用方法将 CallNonvirtualMethod中的 PrimitiveType替换为所调用方法的Java 类型(或使用表中的实际方法名),同时将 NativeType 替换为该方法相应的本地类型。引用类型需要特别对待。 这里我们以调用void返回值为例看看函数定义:

void   (*CallNonvirtualVoidMethod)(JNIEnv*, jobject, jclass,jmethodID, ...);

函数返回值: 返回调用Java方法的结果。



二. Java调用构造方法和调用父类实例方法

在开始JNI中调用构造方法和调用父类实例方法相关的介绍前,让我们看看在Java层是怎么做的,然后我们在JNI层照葫芦画瓢的实现。好了不多说了,直接上代码走起!

2.1 演示代码

定义一个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...

2.2 源码分析

前面的代码非常简单,都是一些入门级的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的基本知识就不过多啰嗦了。


2.3 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对象的步骤分为如下:

  • 为对象分配内存空间
  • 初始化对象(调用对象的构造方法)


三. JNI调用Java构造方法和父类实例方法

在前面的篇章中,我们演示了怎么在Java中实现调用Java类的构造方法和父类实例方法,那么在本章我们将在JNI本地方法的世界里面来重复这一操作。

3.1 演示代码

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

3.2 源码分析

好了,通过前面的代码,我们成功的在JNI中调用了Java构造方法和父类实例方法,那么下面让我们仔细分析分析,一一突破!

3.2.1 JNI调用Java构造方法

JNI调用Java构造方法其实和调用Java普通方法的步骤都是一样的,只是在细节上处理有一丁点区别,下面让我们跟着代码解读一下:
(1) 通过JNI函数FindClass获取Programmer的类引用,并且这里也一定要做异常处理,因为可能查找的类不存在。
(2) 通过JNI函数GetMethodID获取构造方法的MethodID,这里有一点一定要注意的就是构造方法的名字有且仅可能是""。这个读者也不要较真为啥一定是这个呢,因为有些规则是约定俗成的就是这样的。
(3) 调用JNI函数NewObject创建Programmer实例对象,这段代码主要做了两件事情

  • 创建一个未被初始化的对象并分配内存空间
  • 调用对象的构造函数初始化实例对象
    上述的NewObject也可以使用其它JNI函数替代,即先为对象分配内存,然后再初始化对象,代码如下:
	//方式二
	// 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 函数。

3.2.2 通过JNI调用Java父类实例方法

如果一个方法被定义在父类中,在子类中被覆盖,也可以调用父类中的这个实例方法。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构造方法和父类实例方法就告一段落了。也不总结了,偷懒一次,因为确实没有啥好总结的。在最后麻烦读者朋友们如果本篇对你有帮助,关注和点赞一下,当然如果有错误和不足的地方也可以拍砖。

你可能感兴趣的:(JNI/NDK入门指南)