本系列文章如下:
- 1、注冊native函數
- 2、JNI中的簽名
- 3、native代碼反調用Java層代碼
思維導圖如下:

前面兩篇文章簡單的介紹了JNI,下面我們就進一步了解下一下JNI的調用原則,要想了解JNI的調用原則, 前面我們說了JNI
中的JNIEnv
以及Java類型和native中的類型映射關系。下面我們先來看注冊native函數
一、注冊native函數
當Java代碼中執行Native的代碼的時候,首先是通過一定的方法來找到這些native方法。而注冊native函數的具體方法不同,會導致系統在運行時采用不同的方式來尋找這些native方法。
JNI有如下兩種注冊native方法的途徑:
- 靜態注冊:
先由Java得到本地方法的聲明,然后再通過JNI實現該聲明方法- 動態注冊:
先通過JNI重載JNI_OnLoad()實現本地方法,然后直接在Java中調用本地方法。
(一)、靜態注冊native函數
根據函數名找到對應的JNI函數;Java層調用某個函數時,會從對應的JNI中尋找該函數,如果沒有就會報錯,如果存在就會建立一個關聯關系,以后再調用時會直接使用這個函數,這部分的操作由虛擬機完成。
靜態注冊就是根據函數名來遍歷Java和JNI函數之間的關聯,而且要求JNI層函數的名字必須遵循特定的格式。具體的實現很簡單,首先在Java代碼中聲明native函數,然后通過javah來生成native函數的具體形式,接下來在JNI代碼中實現這些函數即可。
舉例如下:
public class JniDemo1{ static { System.loadLibrary("samplelib_jni"); } private native void nativeMethod(); }
接來下通過javah
來產生jni代碼,假設你的包名為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函數的聲明,觀察一下文件名以及函數名。其實JNI方法名的規范就出來了:
返回值 + Java前綴+全路徑類名+方法名+參數1JNIEnv+參數2jobject+其他參數
:注意事項:
- 注意分隔符:
Java前綴與類名
以及類名之間的包名
和方法名之間使用"_"進行分割;- 注意靜態:
如果在Java中聲明的方法是"靜態的",則native方法也是static。否則不是- 如果你的JNI的native方法不是通過靜態注冊方式來實現的,則不需要符合上面的這些規范,可以格局自己習慣隨意命名
(二)、動態注冊native函數
上面我們介紹了靜態注冊native方法的過程,就是Java層聲明的nativ方法和JNI函數一一對應。以我來說,剛開始做JNI的前期,可能會遵守靜態注冊的流程:1、編寫帶有native方法的Java類,2、使用Javah命令生成.h頭文件;3、編寫代碼實現頭文件中的方法,這樣的單調的標准流程,而且還要忍受這么"長"的函數名。那有沒有更簡單的方式呢?比如讓Java層的native方法和任意JNI函數連接起來?答案是有的——動態注冊,也就是通過
RegisterNatives
方法把C/C++中的方法映射到Java中的native方法,而無需遵循特定的方法命名格式。
當我們使用System.loadLibarary()方法加載so庫的時候,Java虛擬機就會找到這個JNI_OnLoad
函數兵調用該函數,這個函數的作用是告訴Dalvik虛擬機此C庫使用的是哪一個JNI版本,如果你的庫里面沒有寫明JNI_OnLoad()函數,VM會默認該庫使用最老的JNI 1.1版本。由於最新版本的JNI做了很多擴充,也優化了一些內容,如果需要使用JNI新版本的功能,就必須在JNI_OnLoad()函數聲明JNI的版本。同時也可以在該函數中做一些初始化的動作,其實這個函數有點類似於Android
中的Activity
中的onCreate()
方法。該函數前面也有三個關鍵字分別是JNIEXPORT
,JNICALL
,jint
。其中JNIEXPORT
和JNICALL
是兩個宏定義,用於指定該函數時JNI函數。jint是JNI定義的數據類型,因為Java層和C/C++的數據類型或者對象不能直接相互的引用或者使用,JNI層定義了自己的數據類型,用於銜接Java層和JNI層,這塊前面已經介紹過了,我這里就不嘮叨了。
PS:與JNI_OnLoad()函數相對應的有JNI_OnUnload()函數,當虛擬機釋放的該C庫的時候,則會調用JNI_OnUnload()函數來進行善后清除工作。
該函數會有兩個參數,其中*jvm
為Java虛擬機實例,JavaVM
結構體定義一下函數:
DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv
下面我們就舉例說明
舉例說明,首先是加載so庫
public class JniDemo1{ static { System.loadLibrary("samplelib_jni"); } }
在jni中的實現
jint JNI_OnLoad(JavaVM* vm, void* reserved)
並且在這個函數里面去動態的注冊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
函數的實現,里面代碼很簡單,主要就是兩個代碼塊,一個是if語句,一個是jniRegisterNativeMethods函數的實現。那我們一個一個來分析。
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { return result ; }
這里調用了GetEnv函數時為了獲取JNIEnv結構體指針,其實JNIEnv結構體指向了一個函數表,該函數表指向了對應的JNI函數,我們通過這些JNI函數實現JNI編程。
然后就調用了jniRegisterNativeMethods
函數來實現注冊,這里面注意一個靜態變量gJni_Methods_table
。它其實代表了一個native方法的數組,如果你在一個Java類中有一個native方法,這里它的size就是1,如果是兩個native方法,它的size就是2,大家看下我這個gJni_Methods_table
變量的實現
static JNINativeMethod gJni_Methods_table[] = { {"sayHello", "(J)V", (void*)sayHello}, };
我們看到他的類型是JNINativeMethod ,那我們就來研究下JNINativeMethod
JNI允許我們提供一個函數映射表,注冊給Java虛擬機,這樣JVM就可以用函數映射表來調用相應的函數。這樣就可以不必通過函數名來查找需要調用的函數了。Java與JNI通過JNINativeMethod的結構來建立聯系,它被定義在jni.h中,其結構內容如下:
typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;
這里面有3個變量,那我們就依次來講解下:
- 第一個變量
name
,代表的是Java中的函數名- 第二個變量
signature
,代表的是Java中的參數和返回值- 第三個變量
fnPtr
,代表的是的指向C函數的函數指針
下面我們再來看下jniRegisterNativeMethods
函數內部的實現
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方法的類
然后通過調用RegisterNatives函數
將注冊函數的Java類,以及注冊函數的數組,以及個數注冊在一起,這樣就實現了綁定。
上面在講解JNINativeMethod
結構體的時候,提到一個概念,就是"signature"即簽名,這個是什么東西?我們下面就來講解下。
二、JNI中的簽名
(一)、為什么JNI中突然多出了一個概念叫"簽名"?
因為Java是支持函數重載的,也就是說,可以定義相同方法名,但是不同參數的方法,然后Java根據其不同的參數,找到其對應的實現的方法。這樣是很好,所以說JNI肯定要支持的,那JNI要怎么支持那,如果僅僅是根據函數名,沒有辦法找到重載的函數的,所以為了解決這個問題,JNI就衍生了一個概念——"簽名",即將參數類型和返回值類型的組合。如果擁有一個該函數的簽名信息和這個函數的函數名,我們就可以順序的找到對應的Java層中的函數了。
(二)、如果查看類中的方法的簽名
可以使用 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規范定義的函數簽名信息
具體格式如下:
(參數1類型標示;參數2類型標示;參數3類型標示...)返回值類型標示
當參數為引用類型的時候,參數類型的標示的根式為"L包名",其中包名的
.
(點)要換成"/",看我上面的例子就差不多,比如String
就是Ljava/lang/String
,Menu
為Landroid/view/Menu
。
類型標示 | Java類型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
如果是基本類類型,其簽名如下:
類型標示 | Java類型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
這個 其實很好記的,除了boolean和long,其他都是首字母大寫。
如果返回值是void,對應的簽名是V。
這里重點說1個特殊的類型,一個是數組及Array
類型標示 | Java類型 |
---|---|
[簽名 | 數組 |
[i | int[] |
[Ljava/lang/Object | String[] |
三、native代碼反調用Java層代碼
上面講解了如何從JNI中調用Java類中的方法,其實在jni.h中已經定義了一系列函數來實現這一目的,下面我們就以此舉例說明:
(一)、獲取Class
對象
為了能夠在C/C++中調用Java中的類,jni.h
的頭文件專門定義了jclass類型表示Java中Class類。JNIEnv中有3個函數可以獲取jclass。
- jclass FindClass(const char* clsName):
通過類的名稱(類的全名,這時候包名不是用'"."點號而是用"/"來區分的)來獲取jclass。比如:
jclass jcl_string=env->FindClass("java/lang/String");
來獲取Java中的String對象的class對象
- jclass GetObjectClass(jobject obj):
通過對象實例來獲取jclass,相當於Java中的getClass()函數- jclass getSuperClass(jclass obj):
通過jclass可以獲取其父類的jclass對象
(二)、獲取屬性方法
在Native本地代碼中訪問Java層的代碼,一個常用的常見的場景就是獲取Java類的屬性和方法。所以為了在C/C++獲取Java層的屬性和方法,JNI在jni.h頭文件中定義了jfieldID和jmethodID這兩種類型來分別代表Java端的屬性和方法。在訪問或者設置Java某個屬性的時候,首先就要現在本地代碼中取得代表該Java類的屬性的jfieldID,然后才能在本地代碼中進行Java屬性的操作,同樣,在需要調用Java類的某個方法時,也是需要取得代表該方法的jmethodID才能進行Java方法操作。
常見的調用Java層的方法如下:
一般是使用JNIEnv來進行操作
- GetFieldID/GetMethodID:獲取某個屬性/某個方法
- GetStaticFieldID/GetStaticMethodID:獲取某個靜態屬性/靜態方法
方法的具體實現如下:
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);
大家發現什么規律沒有?對了,我們發現他們都是4個入參,而且每個入參的都是*JNIEnv *env
,jclass clazz
,const char *name
,const char *sig
。關於JNIEnv
,前面我們已經講過了,這里我們就不詳細講解了,JNIEnv
代表一個JNI環境接口,jclass
上面也說了代表Java層中的"類",name
則代表方法名或者屬性名。那最后一個char *sig
代表什么?它其實代表了JNI中的一個特殊字段——簽名,上面已經講解過了。我們這里就不在冗余了。
(三)、構造一個對象
常用的JNI中創建對象的方法如下:
jobject NewObject(jclass clazz, jmethodID methodID, ...)
比如有我們知道Java類中可能有多個構造函數,當我們要指定調用某個構造函數的時候,會調用下面這個方法
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V"); obj = (*env)->NewObject(env, cls, mid);
即把指定的構造函數傳入進去即可。
現在我們來看下他上面的二個主要參數
- clazz:是需要創建的Java對象的Class對象
- methodID:是傳遞一個方法ID,想一想Java對象創建的時候,需要執行什么操作?就是執行構造函數。
有人會說這要走兩行代碼,有沒有一行代碼的,是有的,如下:
jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
這里多了一個參數,即jvalue *args
,這里是args
代表的是對應構造函數的所有參數的,我們可以應將傳遞給構造函數的所有參數放在jvalues類型的數組args中,該數組緊跟着放在methodID參數的后面。NewObject()收到數組中的這些參數后,將把它們傳給編程任索要調用的Java方法。
上面說到,參數是個數組,如果參數不是數組怎么處理,jni.h同樣也提供了一個方法,如下:
jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
這個方法和上面不同在於,這里將構造函數的所有參數放到在va_list類型的參數args中,該參數緊跟着放在methodID參數的后面。
上一篇文章Android JNI學習(二)——實戰JNI之“hello world”
下一篇文章Android JNI學習(四)——JNI的常用方法的中文API
作者:隔壁老李頭
鏈接:https://www.jianshu.com/p/b71aeb4ed13d
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。