本系列文章如下:
- 1、注冊native函數(shù)
- 2、JNI中的簽名
- 3、native代碼反調(diào)用Java層代碼
思維導(dǎo)圖如下:

前面兩篇文章簡單的介紹了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。其中JNIEXPORT和JNICALL是兩個宏定義,用于指定該函數(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,Menu為Landroid/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 *name,const 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