JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法

转载请注明出处:http://blog.csdn.net/xyang81/article/details/42582213

通过前面5章的学习,我们知道了如何通过JNI函数来访问JVM中的基本数据类型、字符串和数组这些数据类型。下一步我们来学习本地代码如何与JVM中任意对象的属性方法进行交互。比如本地代码调用Java层某个对象的方法或属性,也就是通常我们所说的来自C/C++层本地函数的callback(回调)。这个知识点分2篇文章分别介绍,本篇先介绍方法回调,在第七章中介绍本地代码访问Java的属性。

在这之前,先回顾一下在Java中调用一个方法时在JVM中的实现原理,有助于下面讲解本地代码调用Java方法实现的机制。写过Java的童鞋都知道,调用一个类的静态方法,直接通过 类名.方法 就可以调用。这也太简单了,有什么好讲的呢。。。但在这个调用过程中,JVM是帮我们做了很多工作的。当我们在运行一个Java程序时,JVM会先将程序运行时所要用到所有相关的class文件加载到JVM中,并采用按需加载的方式加载,也就是说某个类只有在被用到的时候才会被加载,这样设计的目的也是为了提高程序的性能和节约内存。所以我们在用类名调用一个静态方法之前,JVM首先会判断该类是否已经加载,如果没有被ClassLoader加载到JVM中,JVM会从classpath路径下查找该类,如果找到了,会将其加载到JVM中,然后才是调用该类的静态方法。如果没有找到,JVM会抛出java.lang.ClassNotFoundException异常,提示找不到这个类。ClassLoader是JVM加载class字节码文件的一种机制,不太了解的童鞋,请移步阅读《深入分析Java ClassLoader原理》一文。其实在JNI开发当中,本地代码也是按照上面的流程来访问类的静态方法或实例方法的,下面通过一个例子,详细介绍本地代码调用Java方法流程当中的每个步聚:

package com.study.jnilearn;

/**
 * AccessMethod.java
 * 本地代码访问类的实例方法和静态方法
 * @author yangxin
 */
public class AccessMethod {

	public static native void callJavaStaticMethod();
	public static native void callJavaInstaceMethod();

	public static void main(String[] args) {
		callJavaStaticMethod();
		callJavaInstaceMethod();
	}

	static {
		System.loadLibrary("AccessMethod");
	}
}
package com.study.jnilearn;

/**
 * ClassMethod.java
 * 用于本地代码调用
 * @author yangxin
 */
public class ClassMethod {

	private static void callStaticMethod(String str, int i) {
		System.out.format("ClassMethod::callStaticMethod called!-->str=%s," +
				" i=%d\n", str, i);
	}

	private void callInstanceMethod(String str, int i) {
		System.out.format("ClassMethod::callInstanceMethod called!-->str=%s, " +
				"i=%d\n", str, i);
	}
}

由AccessMethod.class生成的头文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_AccessMethod */

#ifndef _Included_com_study_jnilearn_AccessMethod
#define _Included_com_study_jnilearn_AccessMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_AccessMethod
 * Method:    callJavaStaticMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod
  (JNIEnv *, jclass);

/*
 * Class:     com_study_jnilearn_AccessMethod
 * Method:    callJavaInstaceMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

本地代码对头文件中函数原型的实现:

// AccessMethod.c

#include "com_study_jnilearn_AccessMethod.h"

/*
 * Class:     com_study_jnilearn_AccessMethod
 * Method:    callJavaStaticMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod
(JNIEnv *env, jclass cls)
{
    jclass clazz = NULL;
    jstring str_arg = NULL;
    jmethodID mid_static_method;
    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
    clazz =(*env)->FindClass(env,"com/study/jnilearn/ClassMethod");
    if (clazz == NULL) {
        return;
    }

    // 2、从clazz类中查找callStaticMethod方法
    mid_static_method = (*env)->GetStaticMethodID(env,clazz,"callStaticMethod","(Ljava/lang/String;I)V");
    if (mid_static_method == NULL) {
        printf("找不到callStaticMethod这个静态方法。");
        return;
    }

    // 3、调用clazz类的callStaticMethod静态方法
    str_arg = (*env)->NewStringUTF(env,"我是静态方法");
    (*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100);

    // 删除局部引用
    (*env)->DeleteLocalRef(env,clazz);
    (*env)->DeleteLocalRef(env,str_arg);
}

/*
 * Class:     com_study_jnilearn_AccessMethod
 * Method:    callJavaInstaceMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod
(JNIEnv *env, jclass cls)
{
    jclass clazz = NULL;
    jobject jobj = NULL;
    jmethodID mid_construct = NULL;
    jmethodID mid_instance = NULL;
    jstring str_arg = NULL;
    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
    clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");
    if (clazz == NULL) {
        printf("找不到‘com.study.jnilearn.ClassMethod‘这个类");
        return;
    }

    // 2、获取类的默认构造方法ID
    mid_construct = (*env)->GetMethodID(env,clazz, "<init>","()V");
    if (mid_construct == NULL) {
        printf("找不到默认的构造方法");
        return;
    }

    // 3、查找实例方法的ID
    mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");
    if (mid_instance == NULL) {

        return;
    }

    // 4、创建该类的实例
    jobj = (*env)->NewObject(env,clazz,mid_construct);
    if (jobj == NULL) {
        printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");
        return;
    }

    // 5、调用对象的实例方法
    str_arg = (*env)->NewStringUTF(env,"我是实例方法");
    (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);

    // 删除局部引用
    (*env)->DeleteLocalRef(env,clazz);
    (*env)->DeleteLocalRef(env,jobj);
    (*env)->DeleteLocalRef(env,str_arg);
}

运行结果:

代码解析:

AccessMethod.java是程序的入口,在main方法中,分别调用了callJavaStaticMethod和callJavaInstaceMethod这两个native方法,用于测试native层调用MethodClass.java中的callStaticMethod静态方法和callInstanceMethod实例方法,这两个方法的返回值都为Void,参数都有两个,分别为String和int

一、callJavaStaticMethod静态方法实现说明

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod
(JNIEnv *env, jclass cls)

定位到AccessMethod.c的31行:

(*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100);

CallStaticVoidMethod函数的原型如下:

void (JNICALL *CallStaticVoidMethod)(JNIEnv *env, jclass cls, jmethodID methodID, ...);

该函数接收4个参数:

env:JNI函数表指针

cls:调用该静态方法的Class对象

methodID:方法ID(因为一个类中会存在多个方法,需要一个唯一标识来确定调用类中的哪个方法) 
      参数4:方法实参列表

根据函数参数的提示,分以下四步完成Java静态方法的回调:

第一步:调用FindClass函数,传入一个Class描述符,JVM会从classpath路径下搜索该类,并返回jclass类型(用于存储Class对象的引用)。注意ClassMethod的Class描述符为com/study/jnilearn/ClassMethod,要将.(点)全部换成/(反斜杠)

(*env)->FindClass(env,"com/study/jnilearn/ClassMethod");

第二步:调用GetStaticMethodID函数,从ClassMethod类中获取callStaticMethod方法ID,返回jmethodID类型(用于存储方法的引用)。实参clazz是第一步找到的jclass对象,实参"callStaticMethod"为方法名称,实参“(Ljava/lang/String;I)V”为方法的签名

(*env)->GetStaticMethodID(env,clazz,"callStaticMethod","(Ljava/lang/String;I)V");

第三步:调用CallStaticVoidMethod函数,执行ClassMethod.callStaticMethod方法调用。str_arg和100是callStaticMethod方法的实参。

str_arg = (*env)->NewStringUTF(env,"我是静态方法");
(*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100);

注意:JVM针对所有数据类型的返回值都定义了相关的函数。上面callStaticMethod方法的返回类型为Void,所以调用CallStaticVoidMethod。根据返回值类型不同,JNI提供了一系列不同返回值的函数,如:CallStaticIntMethod、CallStaticFloatMethod、CallStaticShortMethod、CallStaticObjectMethod等,分别表示调用返回值为int、float、short、Object类型的函数,引用类型统一调用CallStaticObjectMethod函数。另外,每种返回值类型的函数都提供了接收3种实参类型的实现:CallStaticXXXMethod(env, clazz, methodID, ...),CallStaticXXXMethodV(env, clazz, methodID, va_list args),CallStaticXXXMethodA(env, clazz, methodID, const jvalue *args),分别表示:接收可变参数列表、接收va_list作为实参和接收const jvalue*为实参。下面是jni.h头文件中CallStaticVoidMethod的三种实参的函数原型:

 void (JNICALL *CallStaticVoidMethod)
      (JNIEnv *env, jclass cls, jmethodID methodID, ...);
    void (JNICALL *CallStaticVoidMethodV)
      (JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
    void (JNICALL *CallStaticVoidMethodA)
      (JNIEnv *env, jclass cls, jmethodID methodID, const jvalue * args);

第四步、释放局部变量

// 删除局部引用
(*env)->DeleteLocalRef(env,clazz);
(*env)->DeleteLocalRef(env,str_arg);

虽然函数结束后,JVM会自动释放所有局部引用变量所占的内存空间。但还是手动释放一下比较安全,因为在JVM中维护着一个引用表,用于存储局部和全局引用变量,经测试,这个表的最大存储空间是512个引用,如果超过这个数就会造成引用表溢出,JVM崩溃。所以有申请就及时释放是一个好的习惯!(局部引用和全局引用在后面的文章中会详细介绍)

二、callInstanceMethod实例方法实现说明

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod
(JNIEnv *env, jclass cls)

定位到AccessMethod.c的43行:

(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);

CallVoidMethod函数的原型如下:

void (JNICALL *CallVoidMethod) (JNIEnv *env, jobject obj, jmethodID methodID, ...);

该函数接收4个参数:

env:JNI函数表指针

obj:调用该方法的实例

methodID:方法ID 
      参数4:方法的实参列表

根据函数参数的提示,分以下六步完成Java静态方法的回调:
第一步、同调用静态方法一样,首先通过FindClass函数获取类的Class对象

第二步、获取类的构造方法ID,因为创建类的对象首先会调用类的构造方法。这里以默认构造方法为例

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

<init>代表类的构造方法名称,()V代表无参无返回值的构造方法(即默认构造方法)

第三步、调用GetMethodID获取callInstanceMethod的方法ID

(*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");

第四步、调用NewObject函数,创建类的实例对象

(*env)->NewObject(env,clazz,mid_construct);

第五步、调用CallVoidMethod函数,执行ClassMethod.callInstanceMethod方法调用,str_arg和200是方法实参

str_arg = (*env)->NewStringUTF(env,"我是实例方法");
(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);

同JNI调用Java静态方法一样,JVM针对所有数据类型的返回值都定义了相关的函数(CallXXXMethod),如:CallIntMethod、CallFloatMethod、CallObjectMethod等,也同样提供了支持三种类型实参的函数实现,以CallVoidMethod为例,如下是jni.h头文件中该函数的原型:

void (JNICALL *CallVoidMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...);
void (JNICALL *CallVoidMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
void (JNICALL *CallVoidMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue * args);

第六步、删除局部引用(从引用表中移除)

// 删除局部引用
(*env)->DeleteLocalRef(env,clazz);
(*env)->DeleteLocalRef(env,jobj);
(*env)->DeleteLocalRef(env,str_arg);

三、方法签名

在上面的的例子中,无论是调用静态方法还是实例方法,都必须传入一个jmethodID的参数。因为在Java中存在方法重载(方法名相同,参数列表不同),所以要明确告诉JVM调用的是类或实例中的哪一个方法。调用JNI的GetMethodID函数获取一个jmethodID时,需要传入一个方法名称和方法签名,方法名称就是在Java中定义的方法名,方法签名的格式为(形参参数类型列表)返回值。形参参数列表中,引用类型以L开头,后面紧跟类的全路径名(需将.全部替换成/),以分号结尾。下面是一些示例:

Java基本类型与方法签名中参数类型和返回值类型的映射关系如下:

比如,String fun(int a, float b, boolean c, String d) 对应的JNI方法签名为:"(IFZLjava/lang/String;)Ljava/lang/String;"

总结:

1、调用静态方法使用CallStaticXXXMethod/V/A函数,XXX代表返回值的数据类型。如:CallStaticIntMethod

2、调用实例方法使用CallXXXMethod/V/A函数,XXX代表返回的数据类型,如:CallIntMethod

3、获取一个实例方法的ID,使用GetMethodID函数,传入方法名称和方法签名

4、获以一个静态方法的ID,使用GetStaticMethodID函数,传入方法名称和方法签名

5、获取构造方法ID,方法名称使用"<init>"

6、获取一个类的Class实例,使用FindClass函数,传入类描述符。JVM会从classpath目录下开始搜索。

7、创建一个类的实例,使用NewObject函数,传入Class引用和构造方法ID

8、删除局部变量引用,使用DeleteLocalRef,传入引用变量

9、方法签名格式:(形参参数列表)返回值类型。注意:形参参数列表之间不需要用空格或其它字符分隔

10、类描述符格式:L包名路径/类名;,包名之间用/分隔。如:Ljava/lang/String;

11、调用GetMethodID获取方法ID和调用FindClass获取Class实例后,要做异常判断

时间: 2024-10-08 21:51:31

JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法的相关文章

JNI/NDK开发指南(五)——访问数组(基本类型数组与对象数组)

转载请注明出处:http://blog.csdn.net/xyang81/article/details/42346165 JNI中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是JNI的基本数据类型,可以直接访问.而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问Java传递给JNI层的数组,必须选择合适的JNI函数来访问和设置Java层的数组对象.阅读此文假设你已经了解了JNI与Java数据类型的映射关系,如果还不了解

JNI/NDK开发指南(八)——调用构造方法和父类实例方法

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44002089 在第6章我们学习到了在Native层如何调用Java静态方法和实例方法,其中调用实例方法的示例代码中也提到了调用构造函数来实始化一个对象,但没有详细介绍,一带而过了.还没有阅读过的同学请移步<JNI/NDK开发指南(六)--C/C++访问Java实例方法和静态方法>阅读.这章详细来介绍下初始一个对象的两种方式,以及如何调用子类对象重写的父类实例方法. 我们先回过一下,在J

JNI/NDK开发指南(开山篇)

转载请注明出处:http://blog.csdn.net/xyang81/article/details/41759643 相信很多做过Java或Android开发的朋友经常会接触到JNI方面的技术,由其做过Android的朋友,为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码(C或C++)来实现,然后打包成so动态库文件,并提供Java接口供应用层调用,这么做的目的主要就是为了提供应用的安全性,防止被反编译后被不法分子分析应用的逻辑.当然打包成so也不能说完全安全了,只是相对反编译Jav

JNI/NDK开发指南(七)——C/C++访问Java实例变量和静态变量

转载请注明出处:http://blog.csdn.net/xyang81/article/details/42836783 在上一章中我们学习到了如何在本地代码中访问任意Java类中的静态方法和实例方法,本章我们也通过一个示例来学习Java中的实例变量和静态变量,在本地代码中如何来访问和修改.静态变量也称为类变量(属性),在所有实例对象中共享同一份数据,可以直接通过[类名.变量名]来访问.实例变量也称为成员变量(属性),每个实例都拥有一份实例变量数据的拷贝,它们之间修改后的数据互不影响.下面看一

JNI/NDK开发指南(四)——字符串处理

转载请注明出处:http://blog.csdn.net/xyang81/article/details/42066665 从第三章中可以看出JNI中的基本类型和Java中的基本类型都是一一对应的,接下来先看一下JNI的基本类型定义: typedef unsigned char jboolean; typedef unsigned short jchar; typedef short jshort; typedef float jfloat; typedef double jdouble; ty

JNI/NDK开发指南(九)——JNI调用性能测试及优化

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44279725 在前面几章我们学习到了,在Java中声明一个native方法,然后生成本地接口的函数原型声明,再用C/C++实现这些函数,并生成对应平台的动态共享库放到Java程序的类路径下,最后在Java程序中调用声明的native方法就间接的调用到了C/C++编写的函数了,在C/C++中写的程序可以避开JVM的内存开销过大的限制.处理高性能的计算.调用系统服务等功能.同时也学习到了在本

JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44657385 ????这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项.可能看起来有点枯燥,但引用是在JNI中最容易出错的一个点,如果使用不当,容易使程序造成内存溢出,程序崩溃等现象.所以讲得比较细,有些地方看起来可能比较啰嗦,还请轻啪!下一篇文章会写一个在Android由于JNI引用使用不当,造成局部引用表溢出而导致程序闪退的案例,请关注! ????做Java

JNI/NDK开发指南(一)—— JNI开发流程及HelloWorld

转载请注明出处:http://blog.csdn.net/xyang81/article/details/41777471 JNI全称是Java Native Interface(Java本地接口)单词首字母的缩写,本地接口就是指用C和C++开发的接口.由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行.同时,这个特性使我们可以复用以前用C/C++写的大量代码. 开发JNI程序会受到系统环境的限制,因为用C/C++语言写出来的代码或模块,编

JNI/NDK开发指南(二)——JVM查找java native方法的规则

通过第一篇文章,大家明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.UnsatisfiedLinkError异常,找不到XX方法的提示.现在我们想想,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动态库中C/C++实现的那个native函数呢? JVM查找native方法有两种方式: 1> 按照JNI规范的命名规则 2> 调用JNI提供的Regist