鍍金池/ 教程/ Java/ C/C++ 訪問 Java 實例方法和靜態(tài)方法
Android NDK 開發(fā)環(huán)境
Android NDK 簡介
JNI 字符串處理
JVM 查找 native 方法的規(guī)則
JNI 開發(fā)流程
JNI 局部引用、全局引用和弱全局引用
JNI 數(shù)據(jù)類型與 Java 數(shù)據(jù)類型的映射關系
JNI 概述
JNI 調(diào)用性能測試及優(yōu)化
C/C++ 訪問 Java 實例變量和靜態(tài)變量
JNI 調(diào)用構造方法和父類實例方法
C/C++ 訪問 Java 實例方法和靜態(tài)方法
開發(fā)自己的 NDK 程序
JNI 訪問數(shù)組

C/C++ 訪問 Java 實例方法和靜態(tài)方法

通過前面 5 章的學習,我們知道了如何通過 JNI 函數(shù)來訪問 JVM 中的基本數(shù)據(jù)類型、字符串和數(shù)組這些數(shù)據(jù)類型。下一步我們來學習本地代碼如何與 JVM 中任意對象的屬性和方法進行交互。比如本地代碼調(diào)用 Java 層某個對象的方法或?qū)傩?,也就是通常我們所說的來自 C/C++層本地函數(shù)的 callback(回調(diào))。這個知識點分 2 篇文章分別介紹,本篇先介紹方法回調(diào),在第七章中介紹本地代碼訪問 Java 的屬性。

在這之前,先回顧一下在 Java 中調(diào)用一個方法時在 JVM 中的實現(xiàn)原理,有助于下面講解本地代碼調(diào)用 Java 方法實現(xiàn)的機制。寫過 Java 的童鞋都知道,調(diào)用一個類的靜態(tài)方法,直接通過類名.方法就可以調(diào)用。這也太簡單了,有什么好講的呢。但在這個調(diào)用過程中,JVM 是幫我們做了很多工作的。當我們在運行一個 Java 程序時,JVM 會先將程序運行時所要用到所有相關的 class 文件加載到 JVM 中,并采用按需加載的方式加載,也就是說某個類只有在被用到的時候才會被加載,這樣設計的目的也是為了提高程序的性能和節(jié)約內(nèi)存。所以我們在用類名調(diào)用一個靜態(tài)方法之前,JVM 首先會判斷該類是否已經(jīng)加載,如果沒有被 ClassLoader 加載到 JVM 中,JVM 會從classpath 路徑下查找該類,如果找到了,會將其加載到 JVM 中,然后才是調(diào)用該類的靜態(tài)方法。如果沒有找到,JVM 會拋出 java.lang.ClassNotFoundException 異常,提示找不到這個類。ClassLoader 是 JVM 加載class 字節(jié)碼文件的一種機制,不太了解的童鞋,請移步閱讀《深入分析Java ClassLoader原理》一文。其實在 JNI 開發(fā)當中,本地代碼也是按照上面的流程來訪問類的靜態(tài)方法或?qū)嵗椒ǖ?,下面通過一個例子,詳細介紹本地代碼調(diào)用 Java 方法流程當中的每個步聚:

package com.study.jnilearn;  

/** 
 * AccessMethod.java 
 * 本地代碼訪問類的實例方法和靜態(tài)方法 
 * @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 
 * 用于本地代碼調(diào)用 
 * @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  

本地代碼對頭文件中函數(shù)原型的實現(xiàn)

// 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這個靜態(tài)方法。");  
        return;  
    }  

    // 3、調(diào)用clazz類的callStaticMethod靜態(tài)方法  
    str_arg = (*env)->NewStringUTF(env,"我是靜態(tài)方法");  
    (*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、創(chuàng)建該類的實例  
    jobj = (*env)->NewObject(env,clazz,mid_construct);  
    if (jobj == NULL) {  
        printf("在com.study.jnilearn.ClassMethod類中找不到callInstanceMethod方法");  
        return;  
    }  

    // 5、調(diào)用對象的實例方法  
    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);  
}  

運行結(jié)果:

ClassMethod::callStaticMethod called!-->str==我是靜態(tài)方法,i=100
ClassMethod::callInstanceMethod called!-->str==我是實例方法,i=200

代碼解析:

AccessMethod.java 是程序的入口,在 main 方法中,分別調(diào)用了 callJavaStaticMethod 和callJavaInstaceMethod 這兩個 native 方法,用于測試 native 層調(diào)用 MethodClass.java 中的callStaticMethod 靜態(tài)方法和 callInstanceMethod 實例方法,這兩個方法的返回值都為 Void,參數(shù)都有兩個,分別為 String 和 int。

callJavaStaticMethod 靜態(tài)方法實現(xiàn)說明

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

定位到AccessMethod.c的代碼

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

CallStaticVoidMethod函數(shù)的原型如下

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

該函數(shù)接收 4 個參數(shù):

  • env:JNI 函數(shù)表指針
  • cls:調(diào)用該靜態(tài)方法的 Class 對象
  • methodID:方法 ID(因為一個類中會存在多個方法,需要一個唯一標識來確定調(diào)用類中的哪個方法)
  • 參數(shù)4:方法實參列表

根據(jù)函數(shù)參數(shù)的提示,分以下四步完成 Java 靜態(tài)方法的回調(diào):

第一步:調(diào)用 FindClass 函數(shù),傳入一個 Class 描述符,JVM 會從 classpath 路徑下搜索該類,并返回jclass 類型(用于存儲 Class 對象的引用)。注意 ClassMethod 的 Class 描述符為 com/study/jnilearn/ClassMethod,要將 .(點)全部換成 /(反斜杠)。

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

第二步:調(diào)用 GetStaticMethodID 函數(shù),從 ClassMethod 類中獲取 callStaticMethod 方法 ID,返回jmethodID 類型(用于存儲方法的引用)。實參 clazz 是第一步找到的 jclass 對象,實參"callStaticMethod"為方法名稱,實參“(Ljava/lang/String;I)V”為方法的簽名。

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

第三步:調(diào)用 CallStaticVoidMethod 函數(shù),執(zhí)行 ClassMethod.callStaticMethod 方法調(diào)用。str_arg 和100 是 callStaticMethod 方法的實參。

str_arg = (*env)->NewStringUTF(env,"我是靜態(tài)方法");  
(*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100);  

注意:JVM 針對所有數(shù)據(jù)類型的返回值都定義了相關的函數(shù)。上面 callStaticMethod 方法的返回類型為 void,所以調(diào)用 CallStaticVoidMethod。根據(jù)返回值類型不同,JNI 提供了一系列不同返回值的函數(shù),如:CallStaticIntMethod、CallStaticFloatMethod、CallStaticShortMethod、CallStaticObjectMethod等,分別表示調(diào)用返回值為 int、float、short、Object 類型的函數(shù),引用類型統(tǒng)一調(diào)用CallStaticObjectMethod 函數(shù)。另外,每種返回值類型的函數(shù)都提供了接收3種實參類型的實現(xiàn):CallStaticXXXMethod(env, clazz, methodID, ...),CallStaticXXXMethodV(env, clazz, methodID, va_list args),CallStaticXXXMethodA(env, clazz, methodID, const jvalue args),分別表示:接收可變參數(shù)列表、接收 va_list 作為實參和接收`const jvalue為實參。下面是jni.h`頭文件中CallStaticVoidMethod 的三種實參的函數(shù)原型:

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

雖然函數(shù)結(jié)束后,JVM 會自動釋放所有局部引用變量所占的內(nèi)存空間。但還是手動釋放一下比較安全,因為在 JVM 中維護著一個引用表,用于存儲局部和全局引用變量,經(jīng)測試在 Android NDK 環(huán)境下,這個表的最大存儲空間是512 個引用,如果超過這個數(shù)就會造成引用表溢出,JVM 崩潰。在 PC 環(huán)境下測試,不管申請多少局部引用也不釋放都不會崩,我猜可能與 JVM 和 Android Dalvik 虛擬機實現(xiàn)方式不一樣的原因。所以有申請就及時釋放是一個好的習慣?。ň植恳煤腿忠迷诤竺娴奈恼轮袝敿毥榻B)

callInstanceMethod實例方法實現(xiàn)說明

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

定位到AccessMethod.c的代碼:

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

CallVoidMethod函數(shù)的原型如下

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

該函數(shù)接收 4 個參數(shù):

  • env:JNI 函數(shù)表指針
  • obj:調(diào)用該方法的實例
  • methodID:方法 ID
  • 參數(shù)4:方法的實參列表

根據(jù)函數(shù)參數(shù)的提示,分以下六步完成 Java 靜態(tài)方法的回調(diào):

第一步:同調(diào)用靜態(tài)方法一樣,首先通過 FindClass 函數(shù)獲取類的 Class 對象。

第二步:獲取類的構造方法 ID,因為創(chuàng)建類的對象首先會調(diào)用類的構造方法。這里以默認構造方法為例。

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

<init>代表類的構造方法名稱,()V代表無參無返回值的構造方法(即默認構造方法)

第三步:調(diào)用 GetMethodID 獲取 callInstanceMethod 的方法 ID。

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

第四步:調(diào)用 NewObject 函數(shù),創(chuàng)建類的實例對象。

<span style="font-size:18px;">(*env)->NewObject(env,clazz,mid_construct);</span>  

第五步:調(diào)用 CallVoidMethod 函數(shù),執(zhí)行 ClassMethod.callInstanceMethod 方法調(diào)用,str_arg 和 200是方法實參。

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

同 JNI 調(diào)用 Java 靜態(tài)方法一樣,JVM 針對所有數(shù)據(jù)類型的返回值都定義了相關的函數(shù)(CallXXXMethod),如:CallIntMethod、CallFloatMethod、CallObjectMethod 等,也同樣提供了支持三種類型實參的函數(shù)實現(xiàn),以 CallVoidMethod 為例,如下是jni.h頭文件中該函數(shù)的原型:

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

方法簽名

在上面的的例子中,無論是調(diào)用靜態(tài)方法還是實例方法,都必須傳入一個 jmethodID 的參數(shù)。因為在 Java 中存在方法重載(方法名相同,參數(shù)列表不同),所以要明確告訴 JVM 調(diào)用的是類或?qū)嵗械哪囊粋€方法。調(diào)用 JNI 的 GetMethodID 函數(shù)獲取一個 jmethodID 時,需要傳入一個方法名稱和方法簽名,方法名稱就是在 Java 中定義的方法名,方法簽名的格式為:(形參參數(shù)類型列表)返回值。形參參數(shù)列表中,引用類型以 L 開頭,后面緊跟類的全路徑名(需將.全部替換成/),以分號結(jié)尾。下面是一些示例:

http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/images/function1.png" alt="" />

Java 基本類型與方法簽名中參數(shù)類型和返回值類型的映射關系如下:

http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/images/function2.png" alt="" />

比如,String fun(int a, float b, boolean c, String d)對應的 JNI 方法簽名為:"(IFZLjava/lang/String;)Ljava/lang/String;"。

總結(jié):

  • 調(diào)用靜態(tài)方法使用 CallStaticXXXMethod/V/A 函數(shù),XXX 代表返回值的數(shù)據(jù)類型。如:CallStaticIntMethod
  • 調(diào)用實例方法使用 CallXXXMethod/V/A 函數(shù),XXX 代表返回的數(shù)據(jù)類型,如:CallIntMethod
  • 獲取一個實例方法的 ID,使用 GetMethodID 函數(shù),傳入方法名稱和方法簽名
  • 獲以一個靜態(tài)方法的 ID,使用 GetStaticMethodID 函數(shù),傳入方法名稱和方法簽名
  • 獲取構造方法 ID,方法名稱使用""
  • 獲取一個類的 Class 實例,使用 FindClass 函數(shù),傳入類描述符。JVM 會從 classpath 目錄下開始搜索。
  • 創(chuàng)建一個類的實例,使用 NewObject 函數(shù),傳入 Class 引用和構造方法 ID
  • 刪除局部變量引用,使用 DeleteLocalRef,傳入引用變量
  • 方法簽名格式:(形參參數(shù)列表)返回值類型。注意:形參參數(shù)列表之間不需要用空格或其它字符分隔
  • 類描述符格式:L 包名路徑/類名;,包名之間用/分隔。如:Ljava/lang/String;
  • 調(diào)用 GetMethodID 獲取方法 ID 和調(diào)用 FindClass 獲取 Class 實例后,要做異常判斷

示例代碼下載地址:https://code.csdn.net/xyang81/jnilearn