目錄
用法解析
├── 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開發做鋪墊。
1、JNI函數
#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方法類似,同樣包含方法名、參數、返回類型,只不過多了一些修飾詞、特定參數類型而已。
1.1、extern "C"
作用:避免編繹器按照C++的方式去編繹C函數
該關鍵字可以刪掉嗎?
我們不妨動手測試一下:去掉extern “C” , 重新生成so,運行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++的函數重載差異有關系:
1、C不支持函數的重載,編譯之后函數名不變;
2、C++支持函數的重載(這點與Java一致),編譯之后函數名會改變;
原因:在C++中,存在函數的重載問題,函數的識別方式是通過:函數名,函數的返回類型,函數參數列表
三者組合來完成的。
所以,如果希望編譯后的函數名不變,應通知編譯器使用C的編譯方式編譯該函數(即:加上關鍵字:extern “C”)。
擴展:
如果即想去掉關鍵字 extern “C”,又希望方法能被正常調用,真的不能實現嗎?
非也,還是有解決辦法的:“函數的動態注冊”,這個后面再介紹吧!!!
1.2、JNIEXPORT、JNICALL
作用:
JNIEXPORT 用來表示該函數是否可導出(即:方法的可見性)
JNICALL 用來表示函數的調用規范(如:__stdcall)
我們通過JNIEXPORT、JNICALL關鍵字跳轉到jni.h中的定義,如下圖:
通過查看 jni.h 中的源碼,原來JNIEXPORT、JNICALL是兩個宏定義
對於安卓開發者來說,宏可這樣理解:
├── 宏 JNIEXPORT 代表的就是右側的表達式: __attribute__ ((visibility ("default")))
├── 或者也可以說: JNIEXPORT 是右側表達式的別名
宏可表達的內容很多,如:一個具體的數值、一個規則、一段邏輯代碼等;
attribute___((visibility ("default"))) 描述的是“可見性”屬性 visibility
1、default :表示外部可見,類似於public修飾符 (即:可以被外部調用)
2、hidden :表示隱藏,類似於private修飾符 (即:只能被內部調用)
3、其他 :略
如果,我們想使用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
1.3、函數名
看到.cpp中的函數"Java_com_qxc_testnativec_MainActivity_stringFromJNI",大部分開發人員都會有疑問:我們定義的native函數名stringFromJNI,為什么對應到cpp中函數名會變成這么長呢?
public native String stringFromJNI();
這跟JNI native函數的注冊方式有關
JNI Native函數有兩種注冊方式(后面會詳細介紹):
1、靜態注冊:按照JNI接口規范的命名規則注冊;
2、動態注冊:在.cpp的JNI_OnLoad方法里注冊;
JNI接口規范的命名規則:
Java_<PackageName>_<ClassName>_<MethodName>
當我們在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對象 |
New
|
創建類型為Type的數組對象 |
Get
|
獲得類型為Type的字段 |
Set
|
設置類型為Type的字段 |
GetStatic
|
獲得類型為Type的static的字段 |
SetStatic
|
設置類型為Type的static的字段 |
Call
|
調用返回值類型為Type的static方法 |
CallStatic
|
調用返回值類型為Type的static方法 |
具體用法,后面案例再進行演示。
1.5、jobject
jobject 代表了定義native函數的Java類 或 Java類的實例:
├── 如果native函數是static,則代表類Class對象
├── 如果native函數非static,則代表類的實例對象
我們可以通過jobject訪問定義該native方法的成員方法、成員變量等。
2、Java、JNI、C/C++基本類型映射關系
上面,已經介紹了.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類型對應的呢?如下所示:
Java 、C/C++中的常用數據類型的映射關系表(通過源碼跟蹤查找列出來的)
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* |
3、JNI描述符 (簽名)
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 此處是指的:
1、如果是字段,表示字段類型的描述符
2、如果是函數,表示函數結構的描述符,即:每個參數類型描述符 + 返回值類型描述符
舉例( 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 |
4、函數靜態注冊、動態注冊
JNI開發中,我們一般定義了Java native方法,又寫了對應的C方法實現。
那么,當我們在Java代碼中調用Java native方法時,虛擬機是怎么知道並調用SO庫的對應的C方法的呢?
Java native方法與C方法的對應關系,其實是通過注冊實現的,Java native方法的注冊形式有兩種,一種是靜態注冊,另一種是動態注冊:
靜態注冊:按照JNI規范書寫函數名:java_類路徑_方法名(路徑用下划線分隔)
動態注冊:JNI_OnLoad中指定Java Native函數與C函數的對應關系
兩種注冊方式的使用對比:
靜態注冊:
1、優缺點:
系統默認方式,使用簡單;
靈活性差(如果修改了java native函數所在類的包名或類名,需手動修改C函數名稱(頭文件、源文件));
2、實現方式:
1)函數名可以根據規則手寫
2)也可使用javah命令自動生成
3、示例:
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());
}
動態注冊:
1、優缺點:
函數名看着舒服一些,但是需要在C代碼中維護Java Native函數與C函數的對應關系;
靈活性稍高(如果修改了java native函數所在類的包名或類名,僅調整Java native函數的簽名信息)
2、實現方式
env->RegisterNatives(clazz, gMethods, numMethods)
3、示例:
Java類定義Native函數:
package com.qxc.testpage;
public class JNITools {
static {
System.loadLibrary("jnidemo");
}
//加法
public static native int add(int a,int b);
//減法
public static native int sub(int a,int b);
//乘法
public static native int mul(int a,int b);
//除法
public static native int div(int a,int b);
}
.cpp中動態注冊:
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;
}
上面,帶着大家了解了兩種注冊方式的基本知識。接下來,咱們再深入了解一下動態注冊和靜態注冊的底層差異、以及實現原理。
4.1、動態注冊原理
動態注冊是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個函數。當動態注冊時,需調用函數 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源碼,自行分析一下吧。
4.2、靜態注冊原理
靜態注冊是在首次調用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的函數,不會在
重新執行特定名稱查找了。
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函數時,不會再執行黃色部分的流程圖了)