JNI(Java Native Interface)意爲JAVA本地調用,它容許Java代碼和其餘語言寫的代碼進行交互。一種在Java虛擬機控制下執行代碼的標準機制。java
組織結構: android
JNI函數表的組成就像C++的虛函數表,虛擬機能夠運行多張函數表。JNI接口指針僅在當前線程中起做用,指針不能從一個線程進入另外一個線程,但能夠在不一樣的線程中調用本地方法。 咱們在來看看JNI的頭文件:c++
JNI類型和數據結構:算法
引用類型:數組
屬性、方法、值類型:安全
jni.h 102行
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID* jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID* jmethodID; /* method IDs */
struct JNIInvokeInterface;
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
複製代碼
Type Signatures:服務器
觸類旁通:數據結構
String類型的域描述符爲 Ljava/lang/String;
[ + 其類型的域描述符 + ;
int[ ] 其描述符爲[I
float[ ] 其描述符爲[F
String[ ] 其描述符爲[Ljava/lang/String;
Object[ ]類型的域描述符爲[Ljava/lang/Object;
int [ ][ ] 其描述符爲[[I
float[ ][ ] 其描述符爲[[F
---------------------------------------------------------------------------------------------
Java層方法 JNI函數簽名
String test ( ) Ljava/lang/String;
int f (int i, Object object) (ILjava/lang/Object;)I void set (byte[ ] bytes) ([B)V 複製代碼
JNIEnv 概念 : 是一個線程相關的結構體, 該結構體表明瞭 Java 在本線程的運行環境 ;多線程
JNIEnv 與 JavaVM : 注意區分這兩個概念;app
JNIEnv 做用 :
***總結:***JNIEnv只在當前線程中有效。本地方法不能將JNIEnv從一個線程傳遞到另外一個線程中。相同的 Java 線程中對本地方法屢次調用時,傳遞給該本地方法的JNIEnv是相同的。可是,一個本地方法可被不一樣的 Java 線程所調用,所以能夠接受不一樣的 JNIEnv。
/packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterApp.java
其實要學習JNI的知識作好是Android的源碼,咱們知道藍牙、MediaPlayer都使用了JNI技術。你們注意在看源碼的時候必定要注意梳理調用流程。
static JNINativeMethod gMethods[] = {
······
{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
// 這邊是 native_setup : 第一個 是java函數名,第二個是簽名,第三個是 jni具體實現方法的指針
{"native_setup", "(Ljava/lang/Object;)V", (void *)android_media_MediaPlayer_native_setup},
{"native_finalize", "()V", (void *)android_media_MediaPlayer_native_finalize},
······
};
// jni具體實現方法的指針
static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this) {
ALOGV("native_setup");
sp<MediaPlayer> mp = new MediaPlayer();
if (mp == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
// create new listener and give it to MediaPlayer
sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this);
mp->setListener(listener);
// Stow our new C++ MediaPlayer in an opaque field in the Java object.
setMediaPlayer(env, thiz, mp);
}
// This function only registers the native methods
static int register_android_media_MediaPlayer(JNIEnv *env) {
// gMethods 在這邊被調用,系統能夠拿到AndroidRuntime:,咱們拿不到,只能分析,他註冊的時候作了什麼事情,
// 分析: env ,"android/media/MediaPlayer" 是MediaPlayer.java的包名+類名
// gMethods
// NELEM(gMethods)算這個結構體數組的佔多少個字節,將這個大小放進去(是個宏定義,便於複用)
// # define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
// registerNativeMethods 具體實如今AndroidRuntime.cpp 具體見下一段代碼
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
// 這邊重寫了jni.h聲明的 JNI_OnLoad方法,在JNI_OnLoad中進行註冊(register_android_media_MediaPlayer),在註冊過程當中,聲明瞭一個gMethods的結構體數組,這裏面寫好了方法映射。而JNI_OnLoad的調用處,就是System.loadLibrary 的時候會走到這裏,而後進行動態註冊
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
...
// register_android_media_MediaPlayer 在這邊被調用
if (register_android_media_MediaPlayer(env) < 0) {
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
...
/* success -- return valid version number */
result = JNI_VERSION_1_4;
bail:
return result;
}
複製代碼
https://android.googlesource.com/platform/libnativehelper/+/jb-mr1.1-dev-plus-aosp/JNIHelp.cpp#71
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
ALOGV("Registering %s's %d native methods...", className, numMethods);
scoped_local_ref<jclass> c(env, findClass(env, className));
if (c.get() == NULL) {
char* msg;
asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
e->FatalError(msg);
}
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* msg;
asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
e->FatalError(msg);
}
return 0;
}
複製代碼
函數 | Java 數組類型 | 本地類型 | 說明 |
---|---|---|---|
GetBooleanArrayElements | jbooleanArray | jboolean | ReleaseBooleanArrayElements 釋放 |
GetByteArrayElements | jbyteArray | jbyte | ReleaseByteArrayElements 釋放 |
GetCharArrayElements | jcharArray | jchar | ReleaseShortArrayElements 釋放 |
GetShortArrayElements | jshortArray | jshort | ReleaseBooleanArrayElements 釋放 |
GetIntArrayElements | jintArray | jint | ReleaseIntArrayElements 釋放 |
GetLongArrayElements | jlongArray | jlong | ReleaseLongArrayElements 釋放 |
GetFloatArrayElements | jfloatArray | jfloat | ReleaseFloatArrayElements 釋放 |
GetDoubleArrayElements | jdoubleArray | jdouble | ReleaseDoubleArrayElements 釋放 |
GetObjectArrayElement | 自定義對象 | object | |
SetObjectArrayElement | 自定義對象 | object | |
GetArrayLength | 獲取數組大小 | ||
NewArray | 建立一個指定長度的原始數據類型的數組 | ||
GetPrimitiveArrayCritical | 獲得指向原始數據類型內容的指針,該方法可能使垃圾回收不能執行,該方法可能返回數組的拷貝,所以必須釋放此資源。 | ||
ReleasePrimitiveArrayCritical | 釋放指向原始數據類型內容的指針,該方法可能使垃圾回收不能執行,該方法可能返回數組的拷貝,所以必須釋放此資源。 | ||
NewStringUTF | jstring類型的方法轉換 | ||
GetStringUTFChars | jstring類型的方法轉換 | ||
DefineClass | 從原始類數據的緩衝區中加載類 | ||
FindClass | 該函數用於加載本地定義的類。它將搜索由CLASSPATH 環境變量爲具備指定名稱的類所指定的目錄和 zip文件 | ||
GetObjectClass | 經過對象獲取這個類。該函數比較簡單,惟一注意的是對象不能爲NULL,不然獲取的class確定返回也爲NULL | ||
GetSuperclass | 獲取父類或者說超類 。 若是 clazz 表明類class而非類 object,則該函數返回由 clazz 所指定的類的超類。 若是 clazz指定類 object 或表明某個接口,則該函數返回NULL | ||
IsAssignableFrom | 肯定 clazz1 的對象是否可安全地強制轉換爲clazz2 | ||
Throw | 拋出 java.lang.Throwable 對象 | ||
ThrowNew | 利用指定類的消息(由 message 指定)構造異常對象並拋出該異常 | ||
ExceptionOccurred | 肯定是否某個異常正被拋出。在平臺相關代碼調用 ExceptionClear() 或 Java 代碼處理該異常前,異常將始終保持拋出狀態 | ||
ExceptionDescribe | 將異常及堆棧的回溯輸出到系統錯誤報告信道(例如 stderr)。該例程可便利調試操做 | ||
ExceptionClear | 清除當前拋出的任何異常。若是當前無異常,則此例程不產生任何效果 | ||
FatalError | 拋出致命錯誤而且不但願虛擬機進行修復。該函數無返回值 | ||
NewGlobalRef | 建立 obj 參數所引用對象的新全局引用。obj 參數既能夠是全局引用,也能夠是局部引用。全局引用經過調用DeleteGlobalRef() 來顯式撤消。 | ||
DeleteGlobalRef | 刪除 globalRef 所指向的全局引用 | ||
DeleteLocalRef | 刪除 localRef所指向的局部引用 | ||
AllocObject | 分配新 Java 對象而不調用該對象的任何構造函數。返回該對象的引用。clazz 參數務必不要引用數組類。 | ||
getObjectClass | 返回對象的類 | ||
IsSameObject | 測試兩個引用是否引用同一 Java 對象 | ||
NewString | 利用 Unicode 字符數組構造新的 java.lang.String 對象 | ||
GetStringLength | 返回 Java 字符串的長度(Unicode 字符數) | ||
GetStringChars | 返回指向字符串的 Unicode 字符數組的指針。該指針在調用 ReleaseStringchars() 前一直有效 | ||
ReleaseStringChars | 通知虛擬機平臺相關代碼無需再訪問 chars。參數chars 是一個指針,可經過 GetStringChars() 從 string 得到 | ||
NewStringUTF | 利用 UTF-8 字符數組構造新 java.lang.String 對象 | ||
GetStringUTFLength | 以字節爲單位返回字符串的 UTF-8 長度 | ||
GetStringUTFChars | 返回指向字符串的 UTF-8 字符數組的指針。該數組在被ReleaseStringUTFChars() 釋放前將一直有效 | ||
ReleaseStringUTFChars | 通知虛擬機平臺相關代碼無需再訪問 utf。utf 參數是一個指針,可利用 GetStringUTFChars() 得到 | ||
NewObjectArray | 構造新的數組,它將保存類 elementClass 中的對象。全部元素初始值均設爲 initialElement | ||
SetArrayRegion | 將基本類型數組的某一區域從緩衝區中複製回來的一組函數 | ||
GetFieldID | 返回類的實例(非靜態)域的屬性 ID。該域由其名稱及簽名指定。訪問器函數的 GetField 及 SetField系列使用域 ID 檢索對象域。GetFieldID() 不能用於獲取數組的長度域。應使用GetArrayLength()。 | ||
GetField | 該訪問器例程系列返回對象的實例(非靜態)域的值。要訪問的域由經過調用GetFieldID() 而獲得的域 ID 指定。 | ||
SetField | 該訪問器例程系列設置對象的實例(非靜態)屬性的值。要訪問的屬性由經過調用 SetFieldID() 而獲得的屬性 ID指定。 | ||
GetStaticFieldID GetStaticField SetStaticField | 同上,只不過是靜態屬性。 | ||
GetMethodID | 返回類或接口實例(非靜態)方法的方法 ID。方法可在某個 clazz 的超類中定義,也可從 clazz 繼承。該方法由其名稱和簽名決定。 GetMethodID() 可以使未初始化的類初始化。要得到構造函數的方法 ID,應將 做爲方法名,同時將void (V) 做爲返回類型。 | ||
CallVoidMethod | |||
CallObjectMethod | |||
CallBooleanMethod | |||
CallByteMethod | |||
CallCharMethod | |||
CallShortMethod | |||
CallIntMethod | |||
CallLongMethod | |||
CallFloatMethod | |||
CallDoubleMethod | |||
GetStaticMethodID | 調用靜態方法 | ||
CallMethod | |||
RegisterNatives | 向 clazz 參數指定的類註冊本地方法。methods 參數將指定 JNINativeMethod 結構的數組,其中包含本地方法的名稱、簽名和函數指針。nMethods 參數將指定數組中的本地方法數。 | ||
UnregisterNatives | 取消註冊類的本地方法。類將返回到連接或註冊了本地方法函數前的狀態。該函數不該在常規平臺相關代碼中使用。相反,它能夠爲某些程序提供一種從新加載和從新連接本地庫的途徑。 |
咱們來完成一個小的案列來講明知識點吧。咱們在Java層常用AES加密算法,服務器、移動端都須要作一套算法,那咱們就能夠利用JNI來實現一套,給多端調用。閒話很少說,直接上代碼。
下面的是JNIUtils類,主要完成so加載、AES算法初始化等等。關注幾個靜態native函數。
public class JNIUtils {
public static native String getStringFormC(); //演示從C++獲取字符串
public static native byte[] getKeyValue();//用於AES加密的密鑰也是從C++獲取字節數組
public static native byte[] getIv();//使用CBC模式,須要一個向量iv,可增長加密算法的強度
//-----演示字符串用法-------
public native static String sayHello(String text);
private static byte[]keyValue;
private static byte[]iv;
private static SecretKey key;
private static AlgorithmParameterSpec paramSpec;
private static Cipher ecipher;
static {
//加載動態庫
System.loadLibrary("native-lib");
//配置AES算法 初始化 這裏能夠在網上查閱下AES相關的資料
keyValue = getKeyValue();
iv = getIv();
if(null != keyValue && null !=iv) {
KeyGenerator kgen;
try {
kgen = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(keyValue);
kgen.init(128,random);
key =kgen.generateKey();
paramSpec =new IvParameterSpec(iv);
ecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
e.printStackTrace();
}
}
}
/**加密**/
public static String encode(String msg) {
String str ="";
try {
//用密鑰和一組算法參數初始化此 cipher
ecipher.init(Cipher.ENCRYPT_MODE,key,paramSpec);
//加密並轉換成16進制字符串
str = asHex(ecipher.doFinal(msg.getBytes()));
} catch (BadPaddingException | InvalidAlgorithmParameterException
| InvalidKeyException | IllegalBlockSizeException ignored) {
}
return str;
}
/**解密函數**/
public static String decode(String value) {
try {
ecipher.init(Cipher.DECRYPT_MODE,key,paramSpec);
return new String(ecipher.doFinal(asBin(value)));
} catch (BadPaddingException | InvalidKeyException | IllegalBlockSizeException
| InvalidAlgorithmParameterException ignored) {
}
return"";
}
/**轉16進制**/
private static String asHex(byte buf[]) {
StringBuffer strbuf =new StringBuffer(buf.length * 2);
int i;
for (i = 0;i <buf.length;i++) {
if (((int)buf[i] & 0xff) < 0x10)//小於十前面補零
strbuf.append("0");
strbuf.append(Long.toString((int)buf[i] & 0xff, 16));
}
return strbuf.toString();
}
/**轉2進制**/
private static byte[] asBin(String src) {
if (src.length() < 1)
return null;
byte[]encrypted =new byte[src.length() / 2];
for (int i = 0;i <src.length() / 2;i++) {
int high = Integer.parseInt(src.substring(i * 2, i * 2 + 1), 16);//取高位字節
int low = Integer.parseInt(src.substring(i * 2 + 1, i * 2 + 2), 16);//取低位字節
encrypted[i] = (byte) (high * 16 +low);
}
return encrypted;
}
複製代碼
咱們在來看下關鍵的C++核心代碼:
#include <jni.h>
#include <string>
#include<android/log.h>
#define LOG "ndk-jni" // 這個是自定義的LOG的標識
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG,__VA_ARGS__) // 定義LOGD類型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG,__VA_ARGS__) // 定義LOGI類型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG,__VA_ARGS__) // 定義LOGW類型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG,__VA_ARGS__) // 定義LOGE類型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) // 定義LOGF類型
const char keyValue[] = { //祕鑰key
21, 25, 21, 45, 25, 98, 55, 45, 10, 35, 45, 35,
26, 5, 25, 65, 78, 99, 85, 45, 5, 10, 0, 11,
35, 48, 98, 65, 32, 14, 67, 25, 36, 56, 45, 5,
12, 15, 35, 15, 25, 14, 62, 25, 33, 45, 55, 12, 8
};
const char iv[] = { //16bit 加強的iv向量,用於AES算法加強
33, 32, 25, 25, 35, 27, 55, 12, 15,32,
23, 45, 26, 32, 5,16
};
extern "C"
JNIEXPORT jstring JNICALL Java_ndk_jesson_com_ndkdemo_JNIUtils_getStringFormC(JNIEnv *env, jclass type) {
//NewStringUTF
return env->NewStringUTF("這是來自C++的原始字符串");
}
/*** * 獲取c++裏面的數組jbyte */
extern "C"
JNIEXPORT jbyteArray JNICALL Java_ndk_jesson_com_ndkdemo_JNIUtils_getKeyValue(JNIEnv *env, jclass type) {
jbyteArray kv = env->NewByteArray(sizeof(keyValue));
jbyte* bytes = env->GetByteArrayElements(kv,0);
int i=0;
for (i;i < sizeof(keyValue); ++i) {
bytes[i] = keyValue[i];
}
env->SetByteArrayRegion(kv,0, sizeof(keyValue),bytes);
env->ReleaseByteArrayElements(kv,bytes,0);
return kv;
}
extern "C"
JNIEXPORT jbyteArray JNICALL Java_ndk_jesson_com_ndkdemo_JNIUtils_getIv(JNIEnv *env, jclass type) {
jbyteArray ivArray = env->NewByteArray(sizeof(iv));
jbyte *bytes = env->GetByteArrayElements(ivArray, 0);
int i;
for (i = 0; i < sizeof(iv); i++){
bytes[i] = (jbyte)iv[i];
}
env->SetByteArrayRegion(ivArray, 0, sizeof(iv), bytes);
env->ReleaseByteArrayElements(ivArray,bytes,0);
return ivArray;
}
複製代碼
咱們在來重點看下下面幾個函數:
在jni.h裏面有數組定義,jsize代碼數組的大小(typedef jint jsize;)。那麼下面的幾個函數你必定要熟悉:
jbyteArray (*NewBooleanArray)(JNIEnv*, jsize); //初始化數組
jbyte* (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);//獲取數組元素
void (*SetCharArrayRegion)(JNIEnv*, jcharArray, jsize, jsize, const jchar*);//設置數組數據
void (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray, jbyte*, jint);//釋放數組佔用的內存資源
複製代碼
總結一下:在AES的例子中,實現AES是用的Java SDK API。我演示的C++的代碼主要是爲了演示數組的上面的幾個函數的使用。但願經過這篇文章,讓你真正的瞭解了JNI的一些初步的概念,後續我會繼續演示JNI的相關重點用法,包括字符串、異常、多線程。歡迎你們繼續關注個人NDK開發系列文章。