Android JNI學習(三)——Java與Native相互調用


本系列文章如下:

  • 1、注冊native函數
  • 2、JNI中的簽名
  • 3、native代碼反調用Java層代碼

思維導圖如下:


image.png

前面兩篇文章簡單的介紹了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()方法。該函數前面也有三個關鍵字分別是JNIEXPORTJNICALLjint。其中JNIEXPORTJNICALL是兩個宏定義,用於指定該函數時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/StringMenuLandroid/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 *envjclass clazzconst char *nameconst 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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM