一、什么是JNI
1、jni的含義
JNI即為java native interface Java本地接口;JNI是用來調(diào)用本地方法的技術(shù),用來使Java和C/C++進(jìn)行通信。
簡單來說,Java運行一個程序需要和不同的系統(tǒng)進(jìn)行交互,在windows里就要和windows底層平臺交互,mac里就要和mac的底層平臺交互, JVM就是通過大量的JNI技術(shù)能夠使Java運行在不同的系統(tǒng)平臺當(dāng)中,與不同的系統(tǒng)平臺底層進(jìn)行交互。
2、Java與JNI類型對照表
基本類型對照

引用類型對照

3、什么是動態(tài)庫 靜態(tài)庫
Android下的動態(tài)庫和靜態(tài)庫就是Linux的靜態(tài)庫和動態(tài)庫;Linux平臺靜態(tài)庫以.a結(jié)尾,動態(tài)庫以.so結(jié)尾
3.1、靜態(tài)庫
Linux下以.a結(jié)尾,靜態(tài)庫會將整個函數(shù)庫的所有數(shù)據(jù)都整合到目標(biāo)代碼中,優(yōu)點在于后期程序執(zhí)行時不需要外部的函數(shù)庫支持,移植方便但體量較大。
3.2、動態(tài)庫
Linux下以.so即為,動態(tài)庫不會在編譯時將整個函數(shù)庫都整合到目標(biāo)代碼中,而是在運行時執(zhí)行到哪個函數(shù)才會從動態(tài)庫中加載哪個函數(shù),好處在于體量小,升級方便。
靜態(tài)庫的代碼在編譯過程中已經(jīng)被載入可執(zhí)行程序,因此體積比較大;動態(tài)庫(共享庫)的代碼在可執(zhí)行程序運行時才載
入內(nèi)存,在編譯過程中僅簡單的引用,因此代碼體積比較小。
3.3、什么是交叉編譯
交叉編譯就是只在A系統(tǒng)中編譯出在B系統(tǒng)中能用的庫,例如我們Android開發(fā)中,在Windows或者在Mac上能編譯出適合Android執(zhí)行使用的靜態(tài)庫或者動態(tài)庫。
3.4、AndroidStudio CMakeList文件關(guān)于靜態(tài)庫和動態(tài)庫的配置
詳細(xì)的CMake文件的配置后面會有一小節(jié)去專門的講,這里只需要看一下CMakeList中是如何區(qū)分靜態(tài)庫和動態(tài)庫的。
在CMakelist中的add_library中進(jìn)行配置,SHARED為動態(tài)庫配置選項,STATIC為靜態(tài)庫配置選項

二、靜態(tài)注冊和動態(tài)注冊
1、靜態(tài)注冊
靜態(tài)注冊的步驟:
(1)編寫Java類,例如JniTest.java
(2)通過命令行輸入javac JniTest.java 生成JniTest.class文件
(3)在Jni.class目錄下 通過javah xxx.JniTest(全類名)生成xxx_JniTest.h頭文件
(4)編寫xxx_JniTest.c文件,并拷貝xxx_JniTest.h中的函數(shù) 實現(xiàn)這些函數(shù),且在.c文件上方引入jni.h頭文件
(5)編寫CMakeList.tet文件 ,編譯生成對應(yīng)的靜態(tài)庫/動態(tài)庫
上面的步驟很繁瑣,但其實使用熟練后,可以不用通過命令行生成.h文件;只需要自己創(chuàng)建.c文件并引入jni.h頭文件后,其中的C函數(shù)嚴(yán)格對應(yīng)Java中的方法(全類名加方法名,例如:com_test_JniTest_test),函數(shù)當(dāng)中的方法和返回值也要嚴(yán)格對應(yīng)上就可以了。
例如:
Java代碼:
package com.hzf.test;
public class JniTest {
static {
System.loadLibrary("helloworldLib");
}
public static native String test();
}
對應(yīng)的C代碼:
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_hzf_test_JniTest_test(JNIEnv *env,jclass jclass){
return (*env)->NewStringUTF(env,"this is from HelloWorld.c");
}
cmakelist.txt中添加
add_library(helloworldLib SHARED HelloWorld.c)
其中如果Java方法是static的,jni函數(shù)中要傳入jclass(因為static函數(shù)是類的);如果不是static的,jni函數(shù)要傳入jobject
2、動態(tài)注冊
動態(tài)注冊顧名思義,就是動態(tài)的將C函數(shù)去匹配Java函數(shù)的過程,不需要像靜態(tài)注冊一樣,完全對應(yīng)上,方法名稱不需要那么長。
例如:
java:
public static native void jniTestFunc1();
public static native String jniTestFunc2();
對應(yīng)的C函數(shù)
#include <jni.h>
void func1(JNIEnv *env,jclass jclass1){
}
jstring func2(JNIEnv *env,jclass jclass2){
return (*env)->NewStringUTF(env,"this is from dynamicTest.c");
}
static const char * mClazzname = "com/hzf/test/DynamicJniTest";
static const JNINativeMethod methods[] = {
{"jniTestFunc1","()V",(void*)func1},
{"jniTestFunc2","()Ljava/lang/String;",(void*)func2},
};
//通過JNI_OnLoad函數(shù)動態(tài)的匹配到mClassname中的methods函數(shù)
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved){
JNIEnv* env = NULL;
//獲得env
int r = (*vm)->GetEnv(vm,(void**) &env,JNI_VERSION_1_4);
if (r != JNI_OK){
return -1;
}
jclass dynamicClazz = (*env)->FindClass(env,mClazzname);
r = (*env)->RegisterNatives(env,dynamicClazz,methods,2);
if (r != JNI_OK){
return -1;
}
return JNI_VERSION_1_4;
}
3、Jni_onLoad
當(dāng)執(zhí)行到System.loadLibary時,會自動執(zhí)行JNI_OnLoad函數(shù),主要作用有兩個:
(1)告訴VM C組件使用哪一個JNI版本,如果沒有指定,會默認(rèn)指定最老的JNI1.1的版本
(2)當(dāng)執(zhí)行到system.loadLibary時會自動調(diào)用JNI_OnLoad方法,因此可以做一些初始化的相關(guān)工作
4、Sytem.load和System.loadLibaray的區(qū)別
System.load,指定的庫文件路徑是絕對路徑,例如System.load("C:\test\test.so")
System.loadLibary指定是庫文件的名字不包括擴(kuò)展名,例如System.loadLibary("Test.so");
Android中常用的是System.loadLibary
三、JNIEnv類型jobject類型的解釋
JNIEXPORT jstring JNICALL Java_com_hzf_test_JniTest_test(JNIEnv *env,jclass jclass){
return (*env)->NewStringUTF(env,"this is from HelloWorld.c");
}
JNIEXPORT與JNICALL是JNI的關(guān)鍵字,表示此函數(shù)為JNI所調(diào)用
JNIEnv表示Java環(huán)境,通過*env指針就可以對Java函數(shù)進(jìn)行操作,創(chuàng)建Java類中的對象,調(diào)用對象中的方法等。JNIEnv的指針會被JNI傳入到本地方法的實現(xiàn)函數(shù)中對Java代碼進(jìn)行操作。
JNI函數(shù)的方法參數(shù)中都有固定的jclass或者jobject,那么這兩個代表什么含義呢?
(1)如果Java的本地方法不是static的,JNI對應(yīng)的函數(shù)就需要寫jobject,代表native的實例
(2)如果Java的本地方法是static的,JNI對應(yīng)的函數(shù)就需要寫jclass,代表native方法的類的class實例
四、C/C++代碼調(diào)用Java代碼
1、初步使用
JNI還有一個非常重要的作用,也是在開發(fā)中經(jīng)常用到的 就是通過C或者C++去調(diào)用Java本地的代碼,獲取Java的屬性和函數(shù)等;jni.h中定義了jfiledId以及jmethodid分別代表Java端的屬性和方法。
同樣的根據(jù)屬性和方法是否是static的 也對應(yīng)了需要使用不同的filedid以及methodID
GetFiledId/GetMethodId
GetStaticFiledId/GetStaticMethodId
//GetFiledId的定義
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
一共4個參數(shù)
第一個參數(shù)為 JNIEnv指針
第二個參數(shù)為 clazz對象
第三個參數(shù)為 屬性名稱
第四個參數(shù)為 該字段的簽名
例如:
java函數(shù):
static {
System.loadLibrary("ccalljavalib");
}
public int property = 100;
public native void accessProperty();
C函數(shù):
#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#ifndef LOG_TAG
#define LOG_TAG "hzfTag"
#define logUtils(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#endif
//JNI獲取Java層非靜態(tài)的變量
JNIEXPORT void JNICALL Java_com_hzf_test_CCallJavaTest_accessProperty(JNIEnv *env,jobject object){
jclass clazz = (*env)->GetObjectClass(env,object);
jfieldID jfieldId = (*env)->GetFieldID(env,clazz,"property","I");
jint var = (*env)->GetIntField(env,object,jfieldId);
logUtils("vat = %d",var);
//設(shè)置Java層當(dāng)中的值
(*env)->SetIntField(env,object,jfieldId,var + 1000);
};
2、簽名問題
1中提到了獲取字段時,第四個參數(shù)為字段的簽名,JNI與Java基本類型的簽名對照如下:

其他類型:
void對應(yīng)的簽名為V
object對應(yīng)的簽名以L開頭并以/分割包的形式 最后再加上分號; 比如String的簽名為Ljava/lang/String;
Array對應(yīng)的簽名以[開頭加數(shù)組類型對應(yīng)的簽名,例如int[]的簽名為[I,int[][]的簽名為[[I,Object[]的簽名為[Ljava/lang/Object;
例如一個Java方法
public int function(int fu,Date date,int[] array){}
對應(yīng)的JNI中GetMethodID(env,clazz,"function",(ILjava/util/Date;[I)I)
首先方法的參數(shù)在括號中,括號外是方法的返回值,因此先寫成()I
其次的對應(yīng)方式為 int fu對應(yīng)的為I
Date date 對應(yīng)的為 Ljava/util/Date;
int[] array 對應(yīng)的為 [I
3、JNI訪問字符串
由于JNI C/C++ Java三者的String編碼方式各不相同,因此JNI訪問Java的字符串時不能直接用jstring進(jìn)行處理。
Java內(nèi)部是utf-16
Jni是utf-8 Unicode編碼方式,英文是1個字節(jié),中文是3個字節(jié)
C/C++ 使用ASCII碼 中文是GB2312的編碼方式,中文2個字節(jié)
Java代碼
public static native String sayHello(String str);
JNI代碼
JNIEXPORT jstring JNICALL
Java_com_hzf_test_JniTest_sayHello(JNIEnv *env, jclass jclass, jstring str) {
const char *c_str = NULL;
char buf[128] = {0};
jboolean isCopy;
c_str = (*env)->GetStringUTFChars(env, str, isCopy);
//需要做非空判斷 防止崩潰
if (c_str == NULL) {
return NULL;
}
sprintf(buf, "Hello 收到你的信息了 : %s", c_str);
//GetStringUTF會讓JVM開辟一段新內(nèi)存,因此資源要及時釋放
(*env)->ReleaseStringChars(env, str, c_str);
return (*env)->NewStringUTF(env, buf);
}
4、JNI訪問Java的函數(shù)
4.1、JNI訪問Java的構(gòu)造函數(shù)
Java代碼
//無參構(gòu)造函數(shù)
public CCallJavaTest() {
}
//JNI調(diào)用 生成CCallJavaTest對象
public static native CCallJavaTest getInstance();
JNI代碼
JNIEXPORT jobject JNICALL Java_com_hzf_test_CCallJavaTest_getInstance(JNIEnv *env, jclass clazz) {
jclass jclazz = (*env)->FindClass(env, "com/hzf/test/CCallJavaTest");
//獲取類的無參構(gòu)造函數(shù);注意這里構(gòu)造函數(shù)統(tǒng)一寫成 "<init>"
jmethodID jmethodId = (*env)->GetMethodID(env, jclazz, "<init>", "()V");
//創(chuàng)建一個新對象
jobject instance = (*env)->NewObject(env, jclazz, jmethodId);
return instance;
};
調(diào)用代碼
Log.d("hzfTag", "instance : " + CCallJavaTest.getInstance());
輸出:
2020-04-13 19:30:19.919 12386-12386/? D/hzfTag: instance : com.hzf.test.CCallJavaTest@b50fcc2
4.2、JNI訪問Java的靜態(tài)函數(shù)
java代碼
public static void staticTest(String str, int i) {
Log.d("hzfTag", "str : " + str + ",i : " + i);
}
/**
* 調(diào)用static的代碼
*/
public static native void cCallJavaStaticMethod();
JNI函數(shù)
JNIEXPORT void JNICALL
Java_com_hzf_test_CCallJavaTest_cCallJavaStaticMethod(JNIEnv *env, jclass clazz) {
jclass jclazz = (*env)->FindClass(env, "com/hzf/test/CCallJavaTest");
if (jclazz == NULL) {
return;
}
jmethodID jmethodId = (*env)->GetStaticMethodID(env, jclazz, "staticTest",
"(Ljava/lang/String;I)V");
if (jmethodId == NULL) {
return;
}
jstring str = (*env)->NewStringUTF(env, "靜態(tài)類");
(*env)->CallStaticVoidMethod(env, jclazz, jmethodId, str, 1000);
(*env)->DeleteLocalRef(env, jclazz);
(*env)->DeleteLocalRef(env, str);
}
4.3、JNI訪問Java的非靜態(tài)函數(shù)
非靜態(tài)函數(shù)與上面的靜態(tài)函數(shù)類似,只是獲取Method時會用的時GetMethodID
5、引用類型
1、局部引用
通過NewLocalRef和各種JNI接口創(chuàng)建(FindClass、NewObject、GetObjectClass和NewCharArray等)。不能跨函數(shù) 跨線程使用。函數(shù)返回或者結(jié)束后VM會自動回收。
釋放一個局部引用有兩種方式
1、本地方法執(zhí)行結(jié)束后VM會自動釋放
2、通過DeleteLocalRef手動釋放
之所以手動釋放是因為局部引用會影響它所創(chuàng)建的對象被GC回收
2、全局引用
調(diào)用NewGlobalRef創(chuàng)建,可以跨方法,跨線程使用。JVM不會自動釋放,必須通過DeleteGlobalRef釋放
3、弱全局引用
調(diào)用NewWeakGlobalRef基于局部引用或者全局引用創(chuàng)建,不會阻止GC回收所引用的對象,可以跨方法,跨線程。引用不會自動釋放,當(dāng)JVM認(rèn)為回收的時候(比如內(nèi)存緊張時),進(jìn)而回收而被釋放,或調(diào)用DeleteWeakGlobalRef手動釋放