天天看點

安卓JNI精細化講解,讓你徹底了解JNI(二):用法解析

目錄

用法解析

├── 1、JNI函數

│ ├── 1.1、extern "C"

│ ├── 1.2、JNIEXPORT、JNICALL

│ ├── 1.3、函數名

│ ├── 1.4、JNIEnv

│ ├── 1.5、jobject

├── 2、Java、JNI、C/C++基本類型映射關系

├── 3、JNI描述符(簽名)

├── 4、函數靜态注冊、動态注冊

│ ├── 4.1、動态注冊原理

│ ├── 4.2、靜态注冊原理

│ ├── 4.3、Java調用native的流程

當通過AndroidStudio建立了Native C++工程後,首先面對的是*.cpp檔案,對于不熟悉C/C++的開發人員而言,往往是望“類”興歎,無從下手。為此,咱們系統的梳理一下JNI的用法,為後續Native開發做鋪墊。

通常,大家看到的JNI方法如上圖所示,方法結構與Java方法類似,同樣包含方法名、參數、傳回類型,隻不過多了一些修飾詞、特定參數類型而已。

1.1、extern "C"

作用:避免編繹器按照C++的方式去編繹C函數

該關鍵字可以删掉嗎?

我們不妨動手測試一下:去掉extern “C” , 重新生成so,運作app,結果直接閃退了:

咱們反編譯so檔案看一下,原來去掉extern “C” 後,函數名字竟然被修改了:

原因是什麼呢?

其實這跟C和C++的函數重載差異有關系:

是以,如果希望編譯後的函數名不變,應通知編譯器使用C的編譯方式編譯該函數(即:加上關鍵字:extern “C”)。

1.2、JNIEXPORT、JNICALL

作用: JNIEXPORT 用來表示該函數是否可導出(即:方法的可見性) JNICALL 用來表示函數的調用規範(如:__stdcall)

我們通過JNIEXPORT、JNICALL關鍵字跳轉到jni.h中的定義,如下圖:

通過檢視 jni.h 中的源碼,原來JNIEXPORT、JNICALL是兩個宏定義

attribute___((visibility ("default"))) 描述的是“可見性”屬性 visibility

如果,我們想使用hidden,隐藏我們寫的方法,可這麼寫:

重新編譯、運作,結果閃退了。

原因:函數Java_com_qxc_testnativec_MainActivity_stringFromJNI已被隐藏,而我們在java中調用該函數時,找不到該函數,是以抛出了異常,如下圖:

宏JNICALL 右邊是空的,說明隻是個空定義。上面講了,宏JNICALL代表的是右邊定義的内容,那麼,我們代碼也可直接使用右邊的内容(空)替換調JNICALL(即:去掉JNICALL關鍵字),編譯後運作,調用so仍然是正确的:

1.3、函數名

看到.cpp中的函數"Java_com_qxc_testnativec_MainActivity_stringFromJNI",大部分開發人員都會有疑問:我們定義的native函數名stringFromJNI,為什麼對應到cpp中函數名會變成這麼長呢?

這跟JNI native函數的注冊方式有關

JNI接口規範的命名規則:

當我們在Java中調用native方法時,JVM 也會根據這種命名規則來查找、調用native方法對應的 C 方法。

1.4、JNIEnv

JNIEnv 代表了Java環境,通過JNIEnv*就可以對Java端的代碼進行操作,如: ├──建立Java對象 ├──調用Java對象的方法 ├──擷取Java對象的屬性等

我們跳轉、檢視JNIEnv的源碼實作,如下圖:

JNIEnv指向_JNIEnv,而_JNIEnv是定義的一個C++結構體,裡面包含了很多通過JNI接口(JNINativeInterface)對象調用的方法。

那麼,我們通過JNIEnv操作Java端的代碼,主要使用哪些方法呢?

函數名稱

作用

NewObject

建立Java類中的對象

NewString

建立Java類中的String對象

NewArray

建立類型為Type的數組對象

GetField

獲得類型為Type的字段

SetField

設定類型為Type的字段

GetStaticField

獲得類型為Type的static的字段

SetStaticField

設定類型為Type的static的字段

CallMethod

調用傳回值類型為Type的static方法

CallStaticMethod

具體用法,後面案例再進行示範。

1.5、jobject

jobject 代表了定義native函數的Java類 或 Java類的執行個體: ├── 如果native函數是static,則代表類Class對象 ├── 如果native函數非static,則代表類的執行個體對象

我們可以通過jobject通路定義該native方法的成員方法、成員變量等。

上面,已經介紹了.cpp方法的基本結構、主要關鍵字。當我們定義了具體方法,寫C/C++方法實作時,會用到各種參數類型。那麼,在JNI開發中,這些類型應該是怎麼寫呢?

舉例:定義加、減、乘、除的方法

通過上面案例可以看到,幾個方法的後兩個參數、傳回值,類型都是 jint

我們先源碼跟蹤、看下jint的定義,jint 原來是 jni.h中 定義的 int32_t 的别名,如下圖:

根據 int32_t 查找,發現 int32_t 是 stdint.h中定義的 __int32_t的别名,如下圖:

再根據 __int32_t 查找,發現 __int32_t 是 stdint.h中定義的 int 的别名(這個也就是C/C++中的int類型了),如下圖:

Java 、C/C++都有一些常用的資料類型,分别是如何與JNI類型對應的呢?如下所示:

Java 、C/C++中的常用資料類型的映射關系表(通過源碼跟蹤查找列出來的)

JNI中定義的别名

Java類型

C/C++類型

jint / jsize

int

jshort

short

jlong

long

long / long long (__int64)

jbyte

byte

signed char

jboolean

boolean

unsigned char

jchar

char

unsigned short

jfloat

float

jdouble

double

jobject

Object

_jobject*

JNI開發時,我們除了寫本地C/C++實作,還可以通過 JNIEnv *env 調用Java層代碼,如獲得某個字段、擷取某個函數、執行某個函數等:

上面的函數與Java的反射比較類似,參數:

clazz : 類的class對象 name : 字段名、函數名 sig : 字段描述符(簽名)、函數描述符(簽名)

寫過反射的開發人員對clazz、name這兩個參數應該比較熟悉,對sig稍微陌生一些。

sig 此處是指的:

舉例( int 類型的描述符是 大寫的 I ):

由上面的示例可以看到,Java類中的字段類型、函數定義分别對應的描述符:

其他類型的描述符(簽名)如下表:

字段描述符(簽名)

備注

I

int的首字母、大寫

F

float的首字母、大寫

D

double的首字母、大寫

S

short的首字母、大寫

L

long的首字母、大寫

C

char的首字母、大寫

B

byte的首字母、大寫

Z

因B已被byte使用,是以JNI規定使用Z

object

L + /分隔完整類名

String 如: Ljava/lang/String

array

[ + 類型描述符

int[] 如:[I

Java函數

函數描述符(簽名)

void

V

無傳回值類型

Method

(參數字段描述符...)傳回值字段描述符

int add(int a,int b) 如:(II)I

JNI開發中,我們一般定義了Java native方法,又寫了對應的C方法實作。

那麼,當我們在Java代碼中調用Java native方法時,虛拟機是怎麼知道并調用SO庫的對應的C方法的呢?

Java native方法與C方法的對應關系,其實是通過注冊實作的,Java native方法的注冊形式有兩種,一種是靜态注冊,另一種是動态注冊:

靜态注冊:按照JNI規範書寫函數名:java_類路徑_方法名(路徑用下劃線分隔) 動态注冊:JNI_OnLoad中指定Java Native函數與C函數的對應關系

兩種注冊方式的使用對比:

靜态注冊:

動态注冊:

上面,帶着大家了解了兩種注冊方式的基本知識。接下來,咱們再深入了解一下動态注冊和靜态注冊的底層差異、以及實作原理。

4.1、動态注冊原理

動态注冊是Java代碼調用中System.loadLibray()時完成的

那麼,我們先了解一下System.loadLibray加載動态庫時,底層究竟做了哪些操作:

通過System.loadLibray的流程圖,不難看出,Java中加載.so動态庫時,最終會調用so中的JNI_OnLoad方法,這也是為什麼我們要在C的JNIEXPORT jint JNI_OnLoad(JavaVM vm, void* reserved)方法中注冊的原因。

接下來,咱們再深入了解一下動态注冊的具體流程:

如上圖所示:

我們再從源碼層面,重點分析一下動态注冊的流程3和流程4吧。

流程3:開發人員在JNI_OnLoad中寫的注冊方法,注冊對應的C函數

C函數的定義比較簡單,共加減乘除4個函數。當動态注冊時,需調用函數 RegisterNatives(env,jClassName,method, 4)(該方法有不同參數的多個方法重載),我們主要關注的參數:jclass clazz、JNINativeMethod* methods、jint nMethods

clazz 表示:定義Java Native方法的Java類; methods 表示:Java Native方法與C方法的對應關系; nMethods 表示:methods注冊方法的數量,一般設定成methods數組的長度;

JNINativeMethod如何表示Java Native方法與C方法的對應關系的呢?檢視其源碼定義:

了解了JNINativeMethod結構,那麼,JNINativeMethod對象是如何與虛拟機中的Method*對象對應的呢?這個有點複雜了,咱們通過流程圖簡單描述一下吧:

如果還希望更清晰的了解底層源碼的實作邏輯,可下載下傳Android源碼,自行分析一下吧。

4.2、靜态注冊原理

靜态注冊是在首次調用Java Native函數時完成的

4.3、Java調用native的流程

經過對動态注冊、靜态注冊的實作原理的梳理之後,再看Java代碼中調用Java native方法的流程圖,就比較簡單了:

1、如果是動态注冊的Java native函數,System.loadLibray時就已經設定好了Java native函數與C函數的對應關系,當Java代碼中調用Java native方法時,直接執行dvmCallJNIMethod橋函數即可(該函數中執行C函數)。

2、如果是靜态注冊的Java native函數,當Java代碼中調用Java native方法時,預設為Method.nativeFunc指派為dvmResolveNativeMethod,并按特定名稱查找C方法,重新指派Method*,最終仍然是執行dvmCallJNIMethod橋函數(隻不過Java代碼中第二次再調用靜态注冊的Java native函數時,不會再執行黃色部分的流程圖了)

下一篇: iftop

繼續閱讀