Android JNI學(xué)習(xí)(三)——Java與Native相互調(diào)用

本系列文章如下:

  • 1、注冊native函數(shù)
  • 2、JNI中的簽名
  • 3、native代碼反調(diào)用Java層代碼

思維導(dǎo)圖如下:


image.png

前面兩篇文章簡單的介紹了JNI,下面我們就進(jìn)一步了解下一下JNI的調(diào)用原則,要想了解JNI的調(diào)用原則, 前面我們說了JNI中的JNIEnv以及Java類型和native中的類型映射關(guān)系。下面我們先來看注冊native函數(shù)

一、注冊native函數(shù)

當(dāng)Java代碼中執(zhí)行Native的代碼的時候,首先是通過一定的方法來找到這些native方法。而注冊native函數(shù)的具體方法不同,會導(dǎo)致系統(tǒng)在運行時采用不同的方式來尋找這些native方法。

JNI有如下兩種注冊native方法的途徑:

  • 靜態(tài)注冊:
    先由Java得到本地方法的聲明,然后再通過JNI實現(xiàn)該聲明方法
  • 動態(tài)注冊:
    先通過JNI重載JNI_OnLoad()實現(xiàn)本地方法,然后直接在Java中調(diào)用本地方法。

(一)、靜態(tài)注冊native函數(shù)

根據(jù)函數(shù)名找到對應(yīng)的JNI函數(shù);Java層調(diào)用某個函數(shù)時,會從對應(yīng)的JNI中尋找該函數(shù),如果沒有就會報錯,如果存在就會建立一個關(guān)聯(lián)關(guān)系,以后再調(diào)用時會直接使用這個函數(shù),這部分的操作由虛擬機完成。

靜態(tài)注冊就是根據(jù)函數(shù)名來遍歷Java和JNI函數(shù)之間的關(guān)聯(lián),而且要求JNI層函數(shù)的名字必須遵循特定的格式。具體的實現(xiàn)很簡單,首先在Java代碼中聲明native函數(shù),然后通過javah來生成native函數(shù)的具體形式,接下來在JNI代碼中實現(xiàn)這些函數(shù)即可。

舉例如下:

public class JniDemo1{
       static {
             System.loadLibrary("samplelib_jni");
        }

        private native void nativeMethod();
}

接來下通過javah來產(chǎn)生jni代碼,假設(shè)你的包名為com.gebilaolitou.jnidemo

javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.gebilaolitou.jnidemo.JniDemo1

然后就會得到一個JNI的.h文件,里面包含這幾個native函數(shù)的聲明,觀察一下文件名以及函數(shù)名。其實JNI方法名的規(guī)范就出來了:

返回值 + Java前綴+全路徑類名+方法名+參數(shù)1JNIEnv+參數(shù)2jobject+其他參數(shù)

:注意事項:

  • 注意分隔符:
    Java前綴與類名以及類名之間的包名和方法名之間使用"_"進(jìn)行分割;
  • 注意靜態(tài):
    如果在Java中聲明的方法是"靜態(tài)的",則native方法也是static。否則不是
  • 如果你的JNI的native方法不是通過靜態(tài)注冊方式來實現(xiàn)的,則不需要符合上面的這些規(guī)范,可以格局自己習(xí)慣隨意命名

(二)、動態(tài)注冊native函數(shù)

上面我們介紹了靜態(tài)注冊native方法的過程,就是Java層聲明的nativ方法和JNI函數(shù)一一對應(yīng)。以我來說,剛開始做JNI的前期,可能會遵守靜態(tài)注冊的流程:1、編寫帶有native方法的Java類,2、使用Javah命令生成.h頭文件;3、編寫代碼實現(xiàn)頭文件中的方法,這樣的單調(diào)的標(biāo)準(zhǔn)流程,而且還要忍受這么"長"的函數(shù)名。那有沒有更簡單的方式呢?比如讓Java層的native方法和任意JNI函數(shù)連接起來?答案是有的——動態(tài)注冊,也就是通過RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而無需遵循特定的方法命名格式。

當(dāng)我們使用System.loadLibarary()方法加載so庫的時候,Java虛擬機就會找到這個JNI_OnLoad函數(shù)兵調(diào)用該函數(shù),這個函數(shù)的作用是告訴Dalvik虛擬機此C庫使用的是哪一個JNI版本,如果你的庫里面沒有寫明JNI_OnLoad()函數(shù),VM會默認(rèn)該庫使用最老的JNI 1.1版本。由于最新版本的JNI做了很多擴充,也優(yōu)化了一些內(nèi)容,如果需要使用JNI新版本的功能,就必須在JNI_OnLoad()函數(shù)聲明JNI的版本。同時也可以在該函數(shù)中做一些初始化的動作,其實這個函數(shù)有點類似于Android中的Activity中的onCreate()方法。該函數(shù)前面也有三個關(guān)鍵字分別是JNIEXPORT,JNICALL ,jint。其中JNIEXPORTJNICALL是兩個宏定義,用于指定該函數(shù)時JNI函數(shù)。jint是JNI定義的數(shù)據(jù)類型,因為Java層和C/C++的數(shù)據(jù)類型或者對象不能直接相互的引用或者使用,JNI層定義了自己的數(shù)據(jù)類型,用于銜接Java層和JNI層,這塊前面已經(jīng)介紹過了,我這里就不嘮叨了。

PS:與JNI_OnLoad()函數(shù)相對應(yīng)的有JNI_OnUnload()函數(shù),當(dāng)虛擬機釋放的該C庫的時候,則會調(diào)用JNI_OnUnload()函數(shù)來進(jìn)行善后清除工作。

該函數(shù)會有兩個參數(shù),其中*jvm為Java虛擬機實例,JavaVM結(jié)構(gòu)體定義一下函數(shù):

DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv

下面我們就舉例說明

舉例說明,首先是加載so庫

public class JniDemo1{
       static {
             System.loadLibrary("samplelib_jni");
        }
}

在jni中的實現(xiàn)

jint JNI_OnLoad(JavaVM* vm, void* reserved)

并且在這個函數(shù)里面去動態(tài)的注冊native方法,完整的參考代碼如下:

#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2";

static void sayHello(JNIEnv *env, jobject, jlong handle) {
    LOGI("JNI", "native: say hello ###");
}

static JNINativeMethod gJni_Methods_table[] = {
    {"sayHello", "(J)V", (void*)sayHello},
};

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGI("JNI","Registering %s natives\n", className);
    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        LOGE("JNI","Native registration unable to find class '%s'\n", className);
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
        LOGE("JNI","RegisterNatives failed for '%s'\n", className);
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    LOGI("JNI", "enter jni_onload");

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

我們一個個來說,首先看JNI_OnLoad函數(shù)的實現(xiàn),里面代碼很簡單,主要就是兩個代碼塊,一個是if語句,一個是jniRegisterNativeMethods函數(shù)的實現(xiàn)。那我們一個一個來分析。

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
    return result ;
}

這里調(diào)用了GetEnv函數(shù)時為了獲取JNIEnv結(jié)構(gòu)體指針,其實JNIEnv結(jié)構(gòu)體指向了一個函數(shù)表,該函數(shù)表指向了對應(yīng)的JNI函數(shù),我們通過這些JNI函數(shù)實現(xiàn)JNI編程。

然后就調(diào)用了jniRegisterNativeMethods函數(shù)來實現(xiàn)注冊,這里面注意一個靜態(tài)變量gJni_Methods_table。它其實代表了一個native方法的數(shù)組,如果你在一個Java類中有一個native方法,這里它的size就是1,如果是兩個native方法,它的size就是2,大家看下我這個gJni_Methods_table變量的實現(xiàn)

static JNINativeMethod gJni_Methods_table[] = {
    {"sayHello", "(J)V", (void*)sayHello},
};

我們看到他的類型是JNINativeMethod ,那我們就來研究下JNINativeMethod

JNI允許我們提供一個函數(shù)映射表,注冊給Java虛擬機,這樣JVM就可以用函數(shù)映射表來調(diào)用相應(yīng)的函數(shù)。這樣就可以不必通過函數(shù)名來查找需要調(diào)用的函數(shù)了。Java與JNI通過JNINativeMethod的結(jié)構(gòu)來建立聯(lián)系,它被定義在jni.h中,其結(jié)構(gòu)內(nèi)容如下:

typedef struct { 
    const char* name; 
    const char* signature; 
    void* fnPtr; 
} JNINativeMethod; 

這里面有3個變量,那我們就依次來講解下:

  • 第一個變量name,代表的是Java中的函數(shù)名
  • 第二個變量signature,代表的是Java中的參數(shù)和返回值
  • 第三個變量fnPtr,代表的是的指向C函數(shù)的函數(shù)指針

下面我們再來看下jniRegisterNativeMethods函數(shù)內(nèi)部的實現(xiàn)

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGI("JNI","Registering %s natives\n", className);
    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        LOGE("JNI","Native registration unable to find class '%s'\n", className);
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
        LOGE("JNI","RegisterNatives failed for '%s'\n", className);
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

首先通過clazz = (env)->FindClass( className);找到聲明native方法的類
然后通過調(diào)用RegisterNatives函數(shù)將注冊函數(shù)的Java類,以及注冊函數(shù)的數(shù)組,以及個數(shù)注冊在一起,這樣就實現(xiàn)了綁定。

上面在講解JNINativeMethod結(jié)構(gòu)體的時候,提到一個概念,就是"signature"即簽名,這個是什么東西?我們下面就來講解下。

二、JNI中的簽名

(一)、為什么JNI中突然多出了一個概念叫"簽名"?

因為Java是支持函數(shù)重載的,也就是說,可以定義相同方法名,但是不同參數(shù)的方法,然后Java根據(jù)其不同的參數(shù),找到其對應(yīng)的實現(xiàn)的方法。這樣是很好,所以說JNI肯定要支持的,那JNI要怎么支持那,如果僅僅是根據(jù)函數(shù)名,沒有辦法找到重載的函數(shù)的,所以為了解決這個問題,JNI就衍生了一個概念——"簽名",即將參數(shù)類型和返回值類型的組合。如果擁有一個該函數(shù)的簽名信息和這個函數(shù)的函數(shù)名,我們就可以順序的找到對應(yīng)的Java層中的函數(shù)了。

(二)、如果查看類中的方法的簽名

可以使用 javap命令:

javap -s -p MainActivity.class

Compiled from "MainActivity.java"
public class com.example.hellojni.MainActivity extends android.app.Activity {
  static {};
    Signature: ()V

  public com.example.hellojni.MainActivity();
    Signature: ()V

  protected void onCreate(android.os.Bundle);
    Signature: (Landroid/os/Bundle;)V

  public boolean onCreateOptionsMenu(android.view.Menu);
    Signature: (Landroid/view/Menu;)Z

  public native java.lang.String stringFromJNI(); //native 方法
    Signature: ()Ljava/lang/String;  //簽名

  public native int max(int, int); //native 方法
    Signature: (II)I    //簽名
}

我們看到上面有()V(Landroid/os/Bundle;)V,(Landroid/view/Menu;)Z,(II)I我們一臉懵逼,這是什么鬼,所以我們要來研究下簽名的格式

(三)、JNI規(guī)范定義的函數(shù)簽名信息

具體格式如下:

(參數(shù)1類型標(biāo)示;參數(shù)2類型標(biāo)示;參數(shù)3類型標(biāo)示...)返回值類型標(biāo)示

當(dāng)參數(shù)為引用類型的時候,參數(shù)類型的標(biāo)示的根式為"L包名",其中包名的.(點)要換成"/",看我上面的例子就差不多,比如String就是Ljava/lang/String,MenuLandroid/view/Menu

類型標(biāo)示 Java類型
Z boolean
B byte
C char
S short
I int
J long
F float
D double

如果是基本類類型,其簽名如下:

類型標(biāo)示 Java類型
Z boolean
B byte
C char
S short
I int
J long
F float
D double

這個 其實很好記的,除了boolean和long,其他都是首字母大寫。

如果返回值是void,對應(yīng)的簽名是V。
這里重點說1個特殊的類型,一個是數(shù)組及Array

類型標(biāo)示 Java類型
[簽名 數(shù)組
[i int[]
[Ljava/lang/Object String[]

三、native代碼反調(diào)用Java層代碼

上面講解了如何從JNI中調(diào)用Java類中的方法,其實在jni.h中已經(jīng)定義了一系列函數(shù)來實現(xiàn)這一目的,下面我們就以此舉例說明:

(一)、獲取Class對象

為了能夠在C/C++中調(diào)用Java中的類,jni.h的頭文件專門定義了jclass類型表示Java中Class類。JNIEnv中有3個函數(shù)可以獲取jclass。

  • jclass FindClass(const char* clsName):
    通過類的名稱(類的全名,這時候包名不是用'"."點號而是用"/"來區(qū)分的)來獲取jclass。比如:
jclass jcl_string=env->FindClass("java/lang/String");

來獲取Java中的String對象的class對象

  • jclass GetObjectClass(jobject obj):
    通過對象實例來獲取jclass,相當(dāng)于Java中的getClass()函數(shù)
  • jclass getSuperClass(jclass obj):
    通過jclass可以獲取其父類的jclass對象

(二)、獲取屬性方法

在Native本地代碼中訪問Java層的代碼,一個常用的常見的場景就是獲取Java類的屬性和方法。所以為了在C/C++獲取Java層的屬性和方法,JNI在jni.h頭文件中定義了jfieldID和jmethodID這兩種類型來分別代表Java端的屬性和方法。在訪問或者設(shè)置Java某個屬性的時候,首先就要現(xiàn)在本地代碼中取得代表該Java類的屬性的jfieldID,然后才能在本地代碼中進(jìn)行Java屬性的操作,同樣,在需要調(diào)用Java類的某個方法時,也是需要取得代表該方法的jmethodID才能進(jìn)行Java方法操作。

常見的調(diào)用Java層的方法如下:

一般是使用JNIEnv來進(jìn)行操作

  • GetFieldID/GetMethodID:獲取某個屬性/某個方法
  • GetStaticFieldID/GetStaticMethodID:獲取某個靜態(tài)屬性/靜態(tài)方法

方法的具體實現(xiàn)如下:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);

大家發(fā)現(xiàn)什么規(guī)律沒有?對了,我們發(fā)現(xiàn)他們都是4個入?yún)?,而且每個入?yún)⒌亩际?code>*JNIEnv *env,jclass clazz,const char *nameconst char *sig。關(guān)于JNIEnv,前面我們已經(jīng)講過了,這里我們就不詳細(xì)講解了,JNIEnv代表一個JNI環(huán)境接口,jclass上面也說了代表Java層中的"類",name則代表方法名或者屬性名。那最后一個char *sig代表什么?它其實代表了JNI中的一個特殊字段——簽名,上面已經(jīng)講解過了。我們這里就不在冗余了。

(三)、構(gòu)造一個對象

常用的JNI中創(chuàng)建對象的方法如下:

jobject NewObject(jclass clazz, jmethodID methodID, ...)

比如有我們知道Java類中可能有多個構(gòu)造函數(shù),當(dāng)我們要指定調(diào)用某個構(gòu)造函數(shù)的時候,會調(diào)用下面這個方法

jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);

即把指定的構(gòu)造函數(shù)傳入進(jìn)去即可。
現(xiàn)在我們來看下他上面的二個主要參數(shù)

  • clazz:是需要創(chuàng)建的Java對象的Class對象
  • methodID:是傳遞一個方法ID,想一想Java對象創(chuàng)建的時候,需要執(zhí)行什么操作?就是執(zhí)行構(gòu)造函數(shù)。

有人會說這要走兩行代碼,有沒有一行代碼的,是有的,如下:

jobject NewObjectA(JNIEnv *env, jclass clazz, 
jmethodID methodID, jvalue *args);

這里多了一個參數(shù),即jvalue *args,這里是args代表的是對應(yīng)構(gòu)造函數(shù)的所有參數(shù)的,我們可以應(yīng)將傳遞給構(gòu)造函數(shù)的所有參數(shù)放在jvalues類型的數(shù)組args中,該數(shù)組緊跟著放在methodID參數(shù)的后面。NewObject()收到數(shù)組中的這些參數(shù)后,將把它們傳給編程任索要調(diào)用的Java方法。

上面說到,參數(shù)是個數(shù)組,如果參數(shù)不是數(shù)組怎么處理,jni.h同樣也提供了一個方法,如下:

jobject NewObjectV(JNIEnv *env, jclass clazz, 
jmethodID methodID, va_list args);

這個方法和上面不同在于,這里將構(gòu)造函數(shù)的所有參數(shù)放到在va_list類型的參數(shù)args中,該參數(shù)緊跟著放在methodID參數(shù)的后面。

上一篇文章Android JNI學(xué)習(xí)(二)——實戰(zhàn)JNI之“hello world”
下一篇文章Android JNI學(xué)習(xí)(四)——JNI的常用方法的中文API

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容