JNI開發系列目錄
- Android Studio 4.0.+NDK專案開發詳細教學
- Android NDK與JNI的區別有何不同?
- Android Studio 4.0.+NDK .so庫生成打包
- Android JNI的深度進階學習
- Android Studio 4.0.+NDK開發 This files is not part of the project
JNI的深入學習
- 前言
- JNI原理
- JNI函式創建
- Java與C/C++互相呼叫
- JNI開發細則
博客創建時間:2020.11.01
博客更新時間:2020.11.02
以Android studio 4.0.2來分析講解,gradle=6.1.1,如圖文和網上其他資料不一致,可能是別的資料版本較低而已
前言
JNI的全稱是Java Native Interface,即本地Java介面,采用JNI特性可以增強 Java 與本地代碼互動的能力,使Java和其他型別的語言如C++/C能夠互相呼叫,

JNI原理
Java語言的執行環境是Java虛擬機(JVM),JVM其實是主機環境中的一個行程,每個JVM虛擬機都在本地環境中有一個JavaVM結構體,該結構體在創建JVM虛擬機時被回傳,JNI全域僅僅有一個,JavaVM是Java虛擬機在JNI層的代表,一個JVM對應一個JavaVM結構,
一個JVM中可能創建多個Java執行緒,每個執行緒對應一個JNIEnv結構,它們保存在執行緒本地存盤TLS中,JNIEnv是一個執行緒相關的函式表結構體,該結構體代表了Java在本執行緒的執行環境,
不同的執行緒的JNIEnv是不同,也不能相互共享使用,在本地代碼中通過JNIEnv的函式表來操作Java資料或者呼叫Java方法,
JNI函式創建
在AS中如申明一個native方法,AS可以自動幫.cpp檔案中創建一個native函式

#include <jni.h>
extern "C"
JNIEXPORT jint JNICALL
Java_com_xuanyuan_ndktest_MainActivity_add(JNIEnv *env, jobject thiz, jint a, jint b) {
// TODO: implement add()
}
對于如上的JNI函式我們一項項分析
extern "C"
指定以"C"的方式來實作native函式,當然你也可以選擇用extern "C++",兩種方式大致一樣,主要是對env的操作方式略有區別
extern "C++" JNIEXPORT jstring JNICALL Java_com_szysky_note_androiddevseek_114_JNITest_get(JNIEnv *env, jobject thiz){
printf("執行在c++檔案中 get方法\n");
return env->NewStringUTF("Hello from JNI .");
}
extern "C" JNIEXPORT jstring JNICALL Java_com_szysky_note_androiddevseek_114_JNITest_get(JNIEnv *env, jobject thiz){
printf("執行在c檔案中 get方法\n");
return (*env)->NewStringUTF("Hello from JNI .");
}
//區別:
C++: env->ReleaseStringUTFChars(string, str);
C: (*env)->ReleaseStringUTFChars(env, string, str);
JNIEXPORT
宏定義,用于指定該函式是JNI函式,表示此函式可以被外部呼叫,在Android開發中不可省略
JNICALL
宏定義,用于指定該函式是JNI函式,,無實際意義,但是不可省略
JNIEnv env
JNIEnv 代表了JNI的環境,只要在本地代碼中拿到了JNIEnv和jobject,JNI層實作的方法都是通過JNIEnv 指標呼叫JNI層的方法訪問Java虛擬機,進而操作Java物件,這樣就能呼叫Java代碼了,
jobject thiz
在AS中自動為我們生成的JNI方法宣告都會帶一個這樣的引數,這個instance就代表Java中native方法宣告所在的類,比如上面add(int a,int b)方法宣告在MainActivity中,這里的instance就表示MainActivity實體,
Java與C/C++互相呼叫
java和C/C++是可以互相呼叫,下面我們分別分析兩種情況
// Java代碼
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(new MyNdkTest().getData());
}
}
***************************************************************
// C代碼
extern "C"
JNIEXPORT jstring JNICALL
Java_com_xuanyuan_ndktest_MyNdkTest_getData(JNIEnv *env, jobject thiz) {
std::string hello = "我還會回來的!";
return env->NewStringUTF(hello.c_str());
}
1. Java呼叫C/C++函式
呼叫流程: Java層呼叫某個函式時,會從對應的JNI層中尋找該函式,根據java函式的包名、方法名、引數串列等多方面來確定函式是否存在,如果沒有就會報錯,如果存在就會就會建立一個關聯關系,以后再呼叫時會直接使用這個函式,這部分的操作由虛擬機完成,

例如在MainActivity 中呼叫MyNdkTest類的native getData()方法,程式會自動在JNI層查找Java_com_xuanyuan_ndktest_MyNdkTest_getData函式介面,如未找到則報錯,如找到,則會呼叫native庫中的對應函式,
2. C/C++函式呼叫Java
在JNI函式中總會有一個引數jobject thiz,它代表著呼叫該JNI函式的類的實體,這里是MainActivity的實體,通過JNIEnv env和jobject thiz就呼叫MainActivity中的函式和欄位,
JNI開發細則
1. JNI函式命名規則:
- 本地代碼函式如果后綴為.h,則方法要由
extern "C" { }包裹,.cpp檔案不用 -
JNIEXPORT jstring JNICALL中的JNIEXPORT和JNICALL不能省,且jstring是JNI的一種資料型別,相當于Java中的String - 如果在Java中宣告的方法是"靜態的",則native方法也是static,
- JNI函式的命名規則為:Java_包名_類名_方法名,包名里的
.要改成_,_要改成_1 - 如果你的JNI的native方法不是通過靜態注冊方式來實作的,則不需要符合上面的這些規范,可以格局自己習慣隨意命名
2. 資料型別
因為Java層和C/C++的資料型別或者物件不能直接相互的參考或者使用,JNI層定義了自己的資料型別,用于銜接Java層和JNI層,其一 一對應關系如下,
| Java型別 | JNI型別 | Java型別 | JNI型別 |
|---|---|---|---|
| boolean | jboolean | boolean[] | jbooleanArray |
| byte | jbyte | byte[] | jbyteArray |
| char | jchar | char[] | jcharArray |
| short | jshort | short[] | jshortArray |
| int | jint | int[] | jintArray |
| long | jlong | long[] | jlongArray |
| float | jfloat | float[] | jfloatArray |
| double | jdouble | double[] | jdoubleArray |
| Object | jobject | Object[] | jobjectArray |
| Class | jclass | ||
| String | jstring |
3. 常用方法
1. NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...)
創建一個物件
2. string NewString(JNIEnv *env, const jchar *unicodeChars,jsize len)
創建一個新的String物件
3. ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length)
各種型別的陣列
4.jobjectArray NewObjectArray(JNIEnv *env, jsize length,jclass elementClass, jobject initialElement)
創建型別為elementClass的物件陣列,其陣列值初始化為initialElement
5. jobject GetObjectArrayElement(JNIEnv *env,jobjectArray array, jsize index)
從指定陣列中獲得其中某個位置的元素
6. jsize GetArrayLength(JNIEnv *env, jarray array)
獲取array陣列的長度
7. 獲取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”); -
class GetObjectClass(jobject obj):
通過物件實體來獲取jclass,相當于Java中的getClass()函式 -
jclass getSuperClass(jclass obj):
通過jclass可以獲取其父類的jclass物件
8. 獲取屬性及方法
在Native本地代碼中訪問Java層的代碼,一個常用的常見的場景就是獲取Java類的屬性和方法,所以為了在C/C++獲取Java層的屬性和方法,JNI在jni.h頭檔案中定義了jfieldID和jmethodID這兩種型別來分別代表Java端的屬性和方法,
在訪問或者設定Java某個屬性的時候,首先就要現在本地代碼中取得代表該Java類的屬性的jfieldID,然后才能在本地代碼中進行Java屬性的操作,同樣,在需要呼叫Java類的某個方法時,也是需要取得代表該方法的jmethodID才能進行Java方法操作,
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);
JNIEnv代表一個JNI環境介面,jclass上面也說了代表Java層中的"類",name則代表方法名或者屬性名,那最后一個char *sig代表了JNI中的一個特殊欄位——簽名,
4. JNI參考型別
從java 虛擬機中創建的物件傳到C/C++代碼中會產生參考,根據Java的垃圾回識訓制,只要有參考存在就不會觸發該參考所指向Java物件的垃圾回收,所以在JNI呼叫引數需要做一些額外處理,
在JNI規范中定義了三種參考:區域參考(Local Reference)、全域參考(Global Reference)、弱全域參考(Weak Global Reference),
1.區域參考(Local Reference)
區域參考,也稱本地參考,通常是在函式中創建并使用,會阻止GC回收所有參考物件,在函式中產生的區域參考,都會在函式回傳的時候自動釋放(freed),也可以使用DeleteLocalRef函式手動釋放該應用,
2.全域參考(Global Reference)
全域參考可以跨方法、跨執行緒使用,直到被開發者顯式釋放,一個全域參考在被釋放前保證參考物件不被GC回收,和區域應用不同的是,能創建全域參考的函式只有NewGlobalRef,而釋放它需要使用ReleaseGlobalRef函式
3. 弱全域參考(Weak Global Reference)
與全域參考類似,創建跟洗掉都需要由編程人員來進行,這種參考與全域參考一樣可以在多個地方有效,通過使用NewWeakGlobalRef、ReleaseWeakGlobalRef來產生和解除參考,
注意:和全域參考不一樣的是,弱參考將不會阻止垃圾回收器回收這個參考所指向的物件,所以在使用時需要多加小心,它所參考的物件可能是不存在的或者已經被回收,
5. 注冊native函式
當Java代碼中執行Native的代碼的時候,首先是通過一定的方法來找到這些native方法,而注冊native函式的具體方法不同,會導致系統在運行時采用不同的方式來尋找這些native方法,JNI有如下兩種注冊native方法的途徑:靜態注冊與動態注冊
1. 靜態注冊
先由Java得到本地方法的宣告,然后再通過JNI實作該宣告方法,
靜態注冊就是根據函式名來遍歷Java和JNI函式之間的關聯,而且要求JNI層函式的名字必須遵循特定的格式,具體的實作很簡單,首先在Java代碼中宣告native函式,然后通過javah來生成native函式的具體形式,接下來在JNI代碼中實作這些函式即可,
2. 動態注冊
先通過JNI多載JNI_OnLoad()實作本地方法,然后直接在Java中呼叫本地方法,
通過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()方法,
與JNI_OnLoad()函式相對應的有JNI_OnUnload()函式,當虛擬機釋放該C庫的時候,則會呼叫JNI_OnUnload()函式來進行善后清除作業,
相關鏈接:
- Android Studio 4.0.+NDK專案開發詳細教學
- Android NDK與JNI的區別有何不同?
- Android Studio 4.0.+NDK .so庫生成打包
- Android JNI的深度進階學習
- Android Studio 4.0.+NDK開發 This files is not part of the project
博客書寫不易,您的點贊收藏是我前進的動力,千萬別忘記點贊、 收藏 ^ _ ^ !
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/203174.html
標籤:其他
上一篇:IPC機制之了解AIDL(一)
