目錄java
用法解析
├── 一、JNI函數
│ ├── 1.一、extern "C"
│ ├── 1.二、JNIEXPORT、JNICALL
│ ├── 1.三、函數名
│ ├── 1.四、JNIEnv
│ ├── 1.五、jobject
├── 二、Java、JNI、C/C++基本類型映射關係
├── 三、JNI描述符(簽名)
├── 四、函數靜態註冊、動態註冊
│ ├── 4.一、動態註冊原理
│ ├── 4.二、靜態註冊原理
│ ├── 4.三、Java調用native的流程android
當經過AndroidStudio建立了Native C++工程後,首先面對的是*.cpp文件,對於不熟悉C/C++的開發人員而言,每每是望「類」興嘆,無從下手。爲此,我們系統的梳理一下JNI的用法,爲後續Native開發作鋪墊。windows
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_qxc_testnativec_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
一般,你們看到的JNI方法如上圖所示,方法結構與Java方法相似,一樣包含方法名、參數、返回類型,只不過多了一些修飾詞、特定參數類型而已。數組
做用:避免編繹器按照C++的方式去編繹C函數緩存
該關鍵字能夠刪掉嗎?
咱們不妨動手測試一下:去掉extern 「C」 , 從新生成so,運行app,結果直接閃退了:app
我們反編譯so文件看一下,原來去掉extern 「C」 後,函數名字居然被修改了:函數
//保留extern "C" 000000000000ea98 T Java_com_qxc_testnativec_MainActivity_stringFromJNI //去掉extern "C" 000000000000eab8 T _Z40Java_com_qxc_testnativec_MainActivity_stringFromJNIP7_JNIEnvP8_jobject
緣由是什麼呢?
其實這跟C和C++的函數重載差別有關係:源碼分析
一、C不支持函數的重載,編譯以後函數名不變; 二、C++支持函數的重載(這點與Java一致),編譯以後函數名會改變; 緣由:在C++中,存在函數的重載問題,函數的識別方式是經過:函數名,函數的返回類型,函數參數列表 三者組合來完成的。
因此,若是但願編譯後的函數名不變,應通知編譯器使用C的編譯方式編譯該函數(即:加上關鍵字:extern 「C」)。測試
擴展: 若是即想去掉關鍵字 extern 「C」,又但願方法能被正常調用,真的不能實現嗎? 非也,仍是有解決辦法的:「函數的動態註冊」,這個後面再介紹吧!!!
做用:
JNIEXPORT 用來表示該函數是否可導出(即:方法的可見性)
JNICALL 用來表示函數的調用規範(如:__stdcall)this
咱們經過JNIEXPORT、JNICALL關鍵字跳轉到jni.h中的定義,以下圖:
經過查看 jni.h 中的源碼,原來JNIEXPORT、JNICALL是兩個宏定義
對於安卓開發者來講,宏可這樣理解: ├── 宏 JNIEXPORT 表明的就是右側的表達式: __attribute__ ((visibility ("default"))) ├── 或者也能夠說: JNIEXPORT 是右側表達式的別名 宏可表達的內容不少,如:一個具體的數值、一個規則、一段邏輯代碼等;
attribute___((visibility ("default"))) 描述的是「可見性」屬性 visibility
一、default :表示外部可見,相似於public修飾符 (即:能夠被外部調用) 二、hidden :表示隱藏,相似於private修飾符 (即:只能被內部調用) 三、其餘 :略
若是,咱們想使用hidden,隱藏咱們寫的方法,可這麼寫:
#include <jni.h> #include <string> extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL Java_com_qxc_testnativec_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
從新編譯、運行,結果閃退了。
緣由:函數Java_com_qxc_testnativec_MainActivity_stringFromJNI已被隱藏,而咱們在java中調用該函數時,找不到該函數,因此拋出了異常,以下圖:
宏JNICALL 右邊是空的,說明只是個空定義。上面講了,宏JNICALL表明的是右邊定義的內容,那麼,咱們代碼也可直接使用右邊的內容(空)替換調JNICALL(即:去掉JNICALL關鍵字),編譯後運行,調用so仍然是正確的:
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring Java_com_qxc_testnativec_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
JNICALL 知識擴展: JNICALL的定義,並不是全部平臺都像Linux同樣是空的,如windows平臺: #ifndef _JAVASOFT_JNI_MD_H_ #define _JAVASOFT_JNI_MD_H_ #define JNIEXPORT __declspec(dllexport) #define JNIIMPORT __declspec(dllimport) #define JNICALL __stdcall typedef long jint; typedef __int64 jlong; typedef signed char jbyte; #endif
看到.cpp中的函數"Java_com_qxc_testnativec_MainActivity_stringFromJNI",大部分開發人員都會有疑問:咱們定義的native函數名stringFromJNI,爲何對應到cpp中函數名會變成這麼長呢?
public native String stringFromJNI();
這跟JNI native函數的註冊方式有關
JNI Native函數有兩種註冊方式(後面會詳細介紹): 一、靜態註冊:按照JNI接口規範的命名規則註冊; 二、動態註冊:在.cpp的JNI_OnLoad方法裏註冊;
JNI接口規範的命名規則:
通常是 Java_
JNIEnv 表明了Java環境,經過JNIEnv*就能夠對Java端的代碼進行操做,如:
├──建立Java對象
├──調用Java對象的方法
├──獲取Java對象的屬性等
咱們跳轉、查看JNIEnv的源碼實現,以下圖:
JNIEnv指向_JNIEnv,而_JNIEnv是定義的一個C++結構體,裏面包含了不少經過JNI接口(JNINativeInterface)對象調用的方法。
那麼,咱們經過JNIEnv操做Java端的代碼,主要使用哪些方法呢?
| 函數名稱 | 做用 |
|:-----------------:| :-----------------:|
| NewObject | 建立Java類中的對象 |
| NewString | 建立Java類中的String對象 |
| New
| Get
| Set
| GetStatic
| SetStatic
| Call
| CallStatic
具體用法,後面案例再進行演示。
jobject 表明了定義native函數的Java類 或 Java類的實例:
├── 若是native函數是static,則表明類Class對象
├── 若是native函數非static,則表明類的實例對象
咱們能夠經過jobject訪問定義該native方法的成員方法、成員變量等。
上面,已經介紹了.cpp方法的基本結構、主要關鍵字。當咱們定義了具體方法,寫C/C++方法實現時,會用到各類參數類型。那麼,在JNI開發中,這些類型應該是怎麼寫呢?
舉例:定義加、減、乘、除的方法
//加 jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a+b; } //減 jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a-b; } //乘 jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a*b; } //除 jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a/b; }
經過上面案例能夠看到,幾個方法的後兩個參數、返回值,類型都是 jint
jint 是JNI中定義的類型別名,對應的是Java、C++中的int類型
咱們先源碼跟蹤、看下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類型對應的呢?以下所示:
JNI中定義的別名 | Java類型 | C/C++類型 |
---|---|---|
jint / jsize | int | int |
jshort | short | short |
jlong | long | long / long long (__int64) |
jbyte | byte | signed char |
jboolean | boolean | unsigned char |
jchar | char | unsigned short |
jfloat | float | float |
jdouble | double | double |
jobject | Object | _jobject* |
JNI開發時,咱們除了寫本地C/C++實現,還能夠經過 JNIEnv *env 調用Java層代碼,如得到某個字段、獲取某個函數、執行某個函數等:
//得到某類中定義的字段id jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) { return functions->GetFieldID(this, clazz, name, sig); } //得到某類中定義的函數id jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) { return functions->GetMethodID(this, clazz, name, sig); }
上面的函數與Java的反射比較相似,參數:
clazz : 類的class對象
name : 字段名、函數名
sig : 字段描述符(簽名)、函數描述符(簽名)
寫過反射的開發人員對clazz、name這兩個參數應該比較熟悉,對sig稍微陌生一些。
sig 此處是指的:
一、若是是字段,表示字段類型的描述符 二、若是是函數,表示函數結構的描述符,即:每一個參數類型描述符 + 返回值類型描述符
舉例( int 類型的描述符是 大寫的 I ):
Java代碼: public class Hello{ public int property; public int fun(int param, int[] arr){ return 100; } }
JNI C/C++代碼: JNIEXPORT void Java_Hello_test(JNIEnv* env, jobject obj){ jclass myClazz = env->GetObjectClass(obj); jfieldId fieldId_prop = env -> GetFieldId(myClazz, "property", "I"); jmethodId methodId_fun = env -> GetMethodId(myClazz, "fun", "(I[I)I"); }
由上面的示例能夠看到,Java類中的字段類型、函數定義分別對應的描述符:
int 類型 對應的是 I fun 函數 對應的是 (I[I)I
其餘類型的描述符(簽名)以下表:
| Java類型 | 字段描述符(簽名) | 備註|
|:-----------------:| :-----------------:|:-----------------:|
| int | I |int的首字母、大寫|
| float | F |float的首字母、大寫|
| double | D |double的首字母、大寫|
| short | S |short的首字母、大寫|
| long | L |long的首字母、大寫|
| char | C |char的首字母、大寫|
| byte | B |byte的首字母、大寫|
| boolean | 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函數的對應關係
兩種註冊方式的使用對比:
一、優缺點: 系統默認方式,使用簡單; 靈活性差(若是修改了java native函數所在類的包名或類名,需手動修改C函數名稱(頭文件、源文件)); 二、實現方式: 1)函數名能夠根據規則手寫 2)也可以使用javah命令自動生成 三、示例: extern "C" JNIEXPORT jstring Java_com_qxc_testnativec_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
一、優缺點: 函數名看着舒服一些,可是須要在C代碼中維護Java Native函數與C函數的對應關係; 靈活性稍高(若是修改了java native函數所在類的包名或類名,僅調整Java native函數的簽名信息) 二、實現方式 env->RegisterNatives(clazz, gMethods, numMethods) 三、示例: JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ //打印日誌 __android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload"); JNIEnv* env = NULL; jint result = -1; // 判斷是否正確 if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){ return result; } // 定義函數映射關係(參數1:java native函數,參數2:函數描述符,參數3:C函數) const JNINativeMethod method[]={ {"add","(II)I",(void*)addNumber}, {"sub","(II)I",(void*)subNumber}, {"mul","(II)I",(void*)mulNumber}, {"div","(II)I",(void*)divNumber} }; //找到對應的JNITools類 jclass jClassName=(*env)->FindClass(env,"com/qxc/testpage/JNITools"); //開始註冊 jint ret = (*env)->RegisterNatives(env,jClassName,method, 4); //若是註冊失敗,打印日誌 if (ret != JNI_OK) { __android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error"); return -1; } return JNI_VERSION_1_6; } //加 jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a+b; } //減 jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a-b; } //乘 jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a*b; } //除 jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a/b; }
上面,帶着你們瞭解了兩種註冊方式的基本知識。接下來,我們再深刻了解一下動態註冊和靜態註冊的底層差別、以及實現原理。
動態註冊是Java代碼調用中System.loadLibray()時完成的
那麼,咱們先了解一下System.loadLibray加載動態庫時,底層究竟作了哪些操做:
底層源碼:/dalvik/vm/Native.cpp dvmLoadNativeCode() -> JNI_OnLoad() //省略的代碼...... //將pNewEntry保存到gDvm全局變量nativeLibs中,下次能夠直接經過緩存獲取 SharedLib* pActualEntry = addSharedLibEntry(pNewEntry); //省略的代碼...... //第一次加載so時,調用so中的JNI_OnLoad方法 vonLoad = dlsym(handle, "JNI_OnLoad");
經過System.loadLibray的流程圖,不難看出,Java中加載.so動態庫時,最終會調用so中的JNI_OnLoad方法,這也是爲何咱們要在C的JNIEXPORT jint JNI_OnLoad(JavaVM vm, void* reserved)方法中註冊的緣由。
接下來,我們再深刻了解一下動態註冊的具體流程:
如上圖所示:
流程1:是指執行 System.loadLibray函數; 流程2:是指底層默認調用so中的JNI_OnLoad函數; 流程3:是指開發人員在JNI_OnLoad中寫的註冊方法,例如: (*env)->RegisterNatives(env,.....) 流程4:須要重點講解一下: ├── 在Android中,不論是Java函數仍是Java Native函數,它在虛擬機中對應的都是一個Method*對象 ├── 若是是Java Native函數,那麼Method*對象的nativeFunc會指向一個bridge函數dvmCallJNIMethod ├── 當調用Java Native函數時,就會執行該bridge函數,bridge函數的做用是調用該Java Native方法對應的 JNI方法,即: method.insns 流程4的主要做用,如圖所示,爲Java Native函數對應的Method*對象,綁定屬性,創建對應關係: ├── nativeFunc 指向函數 dvmCallJNIMethod(一般狀況下) ├── insns 指向native層的C函數指針 (咱們寫的C函數)
咱們再從源碼層面,重點分析一下動態註冊的流程3和流程4吧。
流程3:開發人員在JNI_OnLoad中寫的註冊方法,註冊對應的C函數
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ //打印日誌 __android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload"); JNIEnv* env = NULL; jint result = -1; // 判斷是否正確 if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){ return result; } // 定義函數映射關係(參數1:java native函數,參數2:函數描述符,參數3:C函數) const JNINativeMethod method[]={ {"add","(II)I",(void*)addNumber}, {"sub","(II)I",(void*)subNumber}, {"mul","(II)I",(void*)mulNumber}, {"div","(II)I",(void*)divNumber} }; //找到對應的JNITools類 jclass jClassName=(*env)->FindClass(env,"com/qxc/testpage/JNITools"); //開始註冊 jint ret = (*env)->RegisterNatives(env,jClassName,method, 4); //若是註冊失敗,打印日誌 if (ret != JNI_OK) { __android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error"); return -1; } return JNI_VERSION_1_6; } //加 jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a+b; } //減 jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a-b; } //乘 jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a*b; } //除 jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a/b; }
C函數的定義比較簡單,共加減乘除4個函數。當動態註冊時,需調用函數 (env)->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方法的對應關係的呢?查看其源碼定義:
jni.h //結構體 typedef struct { const char* name; //Java 方法名稱 const char* signature; //Java 方法描述符(簽名) void* fnPtr; //C/C++方法實現 } JNINativeMethod;
瞭解了JNINativeMethod結構,那麼,JNINativeMethod對象是如何與虛擬機中的Method*對象對應的呢?這個有點複雜了,我們經過流程圖簡單描述一下吧:
若是還但願更清晰的瞭解底層源碼的實現邏輯,可下載Android源碼,自行分析一下吧。
靜態註冊是在首次調用Java Native函數時完成的
如上圖所示:
流程1:Java代碼中調用Java Native函數; 流程2:得到Method*對象,默認爲該函數的Method*設置nativeFunc(dvmResolveNativeMethod); 流程3:dvmResolveNativeMethod函數中按照特定名稱查找對應的C方法; 流程4:若是找到了對應的C方法,從新爲該方法設置Method*屬性; 注意:當Java代碼中第二次再調用Java Native函數時,Method*的nativeFunc已經有值了 (即:dvmCallJNIMethod,可參考動態註冊流程內容),會直接執行Method*的nativeFunc的函數,不會在 從新執行特定名稱查找了。
通過對動態註冊、靜態註冊的實現原理的梳理以後,再看Java代碼中調用Java native方法的流程圖,就比較簡單了:
一、若是是動態註冊的Java native函數,System.loadLibray時就已經設置好了Java native函數與C函數的對應關係,當Java代碼中調用Java native方法時,直接執行dvmCallJNIMethod橋函數便可(該函數中執行C函數)。
二、若是是靜態註冊的Java native函數,當Java代碼中調用Java native方法時,默認爲Method.nativeFunc賦值爲dvmResolveNativeMethod,並按特定名稱查找C方法,從新賦值Method*,最終仍然是執行dvmCallJNIMethod橋函數(只不過Java代碼中第二次再調用靜態註冊的Java native函數時,不會再執行黃色部分的流程圖了)