安卓實戰開發之JNI從小白到偽老白深入瞭解JNI動態註冊native方法及JNI資料使用
前言
或許你知道了jni的簡單呼叫,其實不算什麼百度谷歌一大把,雖然這些jni絕大多數情況下都不會讓我們安卓工程師來弄,畢竟還是有點難,但是我們還是得打破砂鍋知道為什麼這樣幹吧,至少也讓我們知道呼叫流程和資料型別以及處理方法,或許你會有不一樣的發現。
其實總的來說從java的角度來看.h檔案就是java中的interface(插座),然後.c/.cpp檔案呢就是實現類罷了,然後資料型別和java還是有點出入我們還是得了解下(媽蛋,天氣真熱不適合生存了)。
今天也給出一個JNI動態註冊native方法的例子,如圖:
JNI實現步驟
JNI 開發流程主要分為以下步驟:
- 編寫聲明瞭 native 方法的 Java 類
- 將 Java 原始碼編譯成 class 位元組碼檔案
- 用 javah -jni 命令生成.h標頭檔案(javah 是 jdk 自帶的一個命令,-jni 引數表示將 class 中用native 宣告的函式生成 JNI 規則的函式)
- 用原生代碼(c/c++)實現.h標頭檔案中的函式
- 將(c/c++)檔案編譯成動態庫(Windows:*.dll,linux/unix:*.so,mac os x:*.jnilib)
- 拷貝動態庫至本地庫目錄下,並執行 Java 程式(System.loadLibrary(“xxx”))
我們安卓開發工程師顯然只需要編寫native的java類,然後clean下編譯器知道把我們的java編譯成了class檔案,但是我們必須知道是呼叫了javac命令,javah jni命令我們還是得執行,其他的工作就差不多了,不管是什麼編譯器,反正jni步驟就這樣。
JVM 查詢 native 方法
JVM 查詢 native 方法有兩種方式:
- 按照 JNI 規範的命名規則
- 呼叫 JNI 提供的 RegisterNatives 函式,將本地函式註冊到 JVM 中。
是不是感到特別的意外,jni還能夠利用RegisterNatives 函式查詢native方法,其實我也才剛剛知道有這方法,因為要根據包名類名方法名的規範來寫是很傻逼的,哈哈,有的人或許覺得這樣很直觀。
嚴格按照命名規則實現native方法的呼叫
我們還是按步驟來說吧,先來解讀JNI規範的命名規則:
* 我們先來看下.h檔案 *
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_losileeya_jnimaster_JNIUtils */
#ifndef _Included_com_losileeya_jnimaster_JNIUtils
#define _Included_com_losileeya_jnimaster_JNIUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_losileeya_jnimaster_JNIUtils
* Method: say
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_losileeya_jnimaster_JNIUtils_say
(JNIEnv *, jclass,jstring);
#ifdef __cplusplus
}
#endif
#endif
我們再來看下Linux 下jni_md.h標頭檔案內容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
#endif
從上面我們可以看出檔案以#ifndef開始然後#endif 結尾,不會C的話是不是看起來有點蛋疼,#號呢代表巨集,這裡來普及一下巨集的使用和定義。
#define 識別符號 字串
其中,#表示這是一條預處理命令;#define為巨集定義命令;“識別符號”為巨集定義的巨集名;“字串”可以上常數、表示式、格式串等。
舉例如下:
#define PI 3.14 // 對3.14進行巨集定義,巨集名為PI
void main()
{
printf("%f", PI); // 輸出3.14
}
條件編譯的命令
#ifndef def
語句1
# else
語句2
# endif
表示如果def在前面進行了巨集定義那麼就編譯語句1(語句2不編譯),否則編譯語句2(語句1不編譯)
再看我們.h檔案並沒有else,所以我們就編譯巨集定義的本地方法類(com_losileeya_jnimaster_JNIUtils),你突然就會發現我們的巨集是我們的native類,然後把包名點類名的點改成了下劃線,然後你會發現多了_Included不要多想,就是included關鍵字加個下劃線,這樣我們就給本地類進行了巨集定義。然後
#ifdef __cplusplus
extern “C” {#endif
這是說明如果巨集定義了c++,並且裡面有c我們還是支援c的,並且c程式碼寫extern “C” {}裡面。可以看出#endif對應上面的#ifdef-cplusplus,#ifdef-cplusplus對應最後的#endif, #ifdef與#endif總是一一對應的,表明條件編譯開始和結束。
JNIEXPORT 和 JNICALL 的作用
因為安卓是跑在 Linux 下的,所以從 Linux 下的jni_md.h標頭檔案可以看出來,JNIEXPORT 和 JNICALL 是一個空定義,所以在 Linux 下 JNI 函式宣告可以省略這兩個巨集。
再來看我們的方法:
JNIEXPORT jstring JNICALL Java_com_losileeya_jnimaster_JNIUtils_say
(JNIEnv *, jclass,jstring);
函式命名規則為:Java_類全路徑_方法名。
如:Java_com_losileeya_jnimaster_JNIUtils_say,其中Java_是函式的字首,com_losileeya_jnimaster_JNIUtils是類名,say是方法名,它們之間用 _(下劃線) 連線。
- 第一個引數:JNIEnv* 是定義任意 native 函式的第一個引數(包括呼叫 JNI 的 RegisterNatives 函式註冊的函式),指向 JVM 函式表的指標,函式表中的每一個入口指向一個 JNI 函式,每個函式用於訪問 JVM 中特定的資料結構。
- 第二個引數:呼叫 Java 中 native 方法的例項或 Class 物件,如果這個 native 方法是例項方法,則該引數是 jobject,如果是靜態方法,則是 jclass。
- 第三個引數:Java 對應 JNI 中的資料型別,Java 中 String 型別對應 JNI 的 jstring 型別。(後面會詳細介紹 JAVA 與 JNI 資料型別的對映關係)。
函式返回值型別:夾在 JNIEXPORT 和 JNICALL 巨集中間的 jstring,表示函式的返回值型別,對應 Java 的String 型別。
如果你需要裝逼的話你就可以自己去寫.h檔案,然後就可以拋棄javah -jni 命令,只需要按照函式命名規則編寫相應的函式原型和實現即可(逼就是這麼裝出來的)
RegisterNatives動態獲取本地方法
是不是感覺一個方法的名字太長非常的蛋疼,然後我們呢直接使用,RegisterNatives來自己命名呼叫native方法,這樣是不是感覺好多了。
要實現呢,我們必須重寫JNI_OnLoad()方法這樣就會當呼叫 System.loadLibrary(“XXXX”)方法的時候直接來呼叫JNI_OnLoad(),這樣就達到了動態註冊實現native方法的作用。
/*
* System.loadLibrary("lib")時呼叫
* 如果成功返回JNI版本, 失敗返回-1
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) {//註冊
return -1;
}
//成功
result = JNI_VERSION_1_4;
return result;
}
並且我們需要為類註冊本地方法,那樣就能方便我們去呼叫,不多說看方法:
/*
* 為所有類註冊本地方法
*/
static int registerNatives(JNIEnv* env) {
return registerNativeMethods(env, JNIREG_CLASS, gMethods,sizeof(gMethods) / sizeof(gMethods[0]));
}
也可以為某一個類註冊本地方法
/*
* 為某一個類註冊本地方法
*/
static int registerNativeMethods(JNIEnv* env
, const char* className
, JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNINativeMethod 結構體的官方定義
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
- 第一個變數name是Java中函式的名字。
- 第二個變數signature,用字串是描述了Java中函式的引數和返回值
第三個變數fnPtr是函式指標,指向native函式。前面都要接 (void *)
第一個變數與第三個變數是對應的,一個是java層方法名,對應著第三個引數的native方法名字(不明白請看後面程式碼就會清楚了)。
哈哈最後我們就把native方法繫結到JNINativeMethod上我們來看下事例:
static JNINativeMethod gMethods[] = {
{"setDataSource", "(Ljava/lang/String;)V", (void *)com_media_ffmpeg_FFMpegPlayer_setDataSource},
{"_setVideoSurface", "(Landroid/view/Surface;)V", (void *)com_media_ffmpeg_FFMpegPlayer_setVideoSurface},
{"prepare", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_prepare},
{"_start", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_start},
{"_stop", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_stop},
{"getVideoWidth", "()I", (void *)com_media_ffmpeg_FFMpegPlayer_getVideoWidth},
{"getVideoHeight", "()I", (void *)com_media_ffmpeg_FFMpegPlayer_getVideoHeight},
{"seekTo", "(I)V", (void *)com_media_ffmpeg_FFMpegPlayer_seekTo},
{"_pause", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_pause},
{"isPlaying", "()Z", (void *)com_media_ffmpeg_FFMpegPlayer_isPlaying},
{"getCurrentPosition", "()I", (void *)com_media_ffmpeg_FFMpegPlayer_getCurrentPosition},
{"getDuration", "()I", (void *)com_media_ffmpeg_FFMpegPlayer_getDuration},
{"_release", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_release},
{"_reset", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_reset},
{"setAudioStreamType", "(I)V", (void *)com_media_ffmpeg_FFMpegPlayer_setAudioStreamType},
{"native_init", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_native_init},
{"native_setup", "(Ljava/lang/Object;)V", (void *)com_media_ffmpeg_FFMpegPlayer_native_setup},
{"native_finalize", "()V", (void *)com_media_ffmpeg_FFMpegPlayer_native_finalize},
{"native_suspend_resume", "(Z)I", (void *)com_media_ffmpeg_FFMpegPlayer_native_suspend_resume},
};
第一個引數就是我們寫的方法,第三個就是.h檔案裡面的方法,第二個引數顯得有點難度,這裡會主要去講。
主要是第二個引數比較複雜:
括號裡面表示引數的型別,括號後面表示返回值。
- “()” 中的字元表示引數,後面的則代表返回值。例如”()V” 就表示void * Fun();
- “(II)V” 表示 void Fun(int a, int b);
- “(II)I” 表示 int sum(int a, int b);
這些字元與函式的引數型別的對映表如下:
字元 Java型別 C型別
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
陣列則以”[“開始,用兩個字元表示
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
[Z jbooleanArray boolean[]
如圖:
- 物件型別:以”L”開頭,以”;”結尾,中間是用”/” 隔開。如上表第1個
- 陣列型別:以”[“開始。如上表第2個(n維陣列的話,則是前面多少個”[“而已,如”[[[D”表示“double[][][]”)
如果Java函式的引數是class,則以”L”開頭,以”;”結尾中間是用”/” 隔開的包及類名。而其對應的C函式名的引數則為jobject. 一個例外是String類,其對應的類為jstring
Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject
如果JAVA函式位於一個嵌入類,則用作為類名間的分隔符。例如“(Ljava/lang/String;Landroid/os/FileUtils FileStatus;)Z”
好了,所有 的介紹也完了,那麼我們就來實現我們的程式碼:(果斷把h檔案刪除,看效果)
JNIUtil.c:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>
#define JNIREG_CLASS "com/losileeya/registernatives/JNIUtil"//指定要註冊的類
jstring call(JNIEnv* env, jobject thiz)
{
return (*env)->NewStringUTF(env, "動態註冊JNI,居然可以把標頭檔案刪掉也不會影響結果,牛逼不咯");
}
jint sum(JNIEnv* env, jobject jobj,jint num1,jint num2){
return num1+num2;
}
/**
* 方法對應表
*/
static JNINativeMethod gMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*)call},
{"sum", "(II)I", (void*)sum},
};
/*
* 為某一個類註冊本地方法
*/
static int registerNativeMethods(JNIEnv* env
, const char* className
, JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* 為所有類註冊本地方法
*/
static int registerNatives(JNIEnv* env) {
return registerNativeMethods(env, JNIREG_CLASS, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
}
/*
* System.loadLibrary("lib")時呼叫
* 如果成功返回JNI版本, 失敗返回-1
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) {//註冊
return -1;
}
//成功
result = JNI_VERSION_1_4;
return result;
}
程式碼寫完了,主要也是利用findClass來獲取方法,從而實現方法的呼叫。效果重現:
JNI資料型別及常用方法(JNI安全手冊)
基本型別和本地等效型別表:
引用型別:
介面函式表:
const struct JNINativeInterface ... = {
NULL,
NULL,
NULL,
NULL,
GetVersion,
DefineClass,
FindClass,
NULL,
NULL,
NULL,
GetSuperclass,
IsAssignableFrom,
NULL,
Throw,
ThrowNew,
ExceptionOccurred,
ExceptionDescribe,
ExceptionClear,
FatalError,
NULL,
NULL,
NewGlobalRef,
DeleteGlobalRef,
DeleteLocalRef,
IsSameObject,
NULL,
NULL,
AllocObject,
NewObject,
NewObjectV,
NewObjectA,
GetObjectClass,
IsInstanceOf,
GetMethodID,
CallObjectMethod,
CallObjectMethodV,
CallObjectMethodA,
CallBooleanMethod,
CallBooleanMethodV,
CallBooleanMethodA,
CallByteMethod,
CallByteMethodV,
CallByteMethodA,
CallCharMethod,
CallCharMethodV,
CallCharMethodA,
CallShortMethod,
CallShortMethodV,
CallShortMethodA,
CallIntMethod,
CallIntMethodV,
CallIntMethodA,
CallLongMethod,
CallLongMethodV,
CallLongMethodA,
CallFloatMethod,
CallFloatMethodV,
CallFloatMethodA,
CallDoubleMethod,
CallDoubleMethodV,
CallDoubleMethodA,
CallVoidMethod,
CallVoidMethodV,
CallVoidMethodA,
CallNonvirtualObjectMethod,
CallNonvirtualObjectMethodV,
CallNonvirtualObjectMethodA,
CallNonvirtualBooleanMethod,
CallNonvirtualBooleanMethodV,
CallNonvirtualBooleanMethodA,
CallNonvirtualByteMethod,
CallNonvirtualByteMethodV,
CallNonvirtualByteMethodA,
CallNonvirtualCharMethod,
CallNonvirtualCharMethodV,
CallNonvirtualCharMethodA,
CallNonvirtualShortMethod,
CallNonvirtualShortMethodV,
CallNonvirtualShortMethodA,
CallNonvirtualIntMethod,
CallNonvirtualIntMethodV,
CallNonvirtualIntMethodA,
CallNonvirtualLongMethod,
CallNonvirtualLongMethodV,
CallNonvirtualLongMethodA,
CallNonvirtualFloatMethod,
CallNonvirtualFloatMethodV,
CallNonvirtualFloatMethodA,
CallNonvirtualDoubleMethod,
CallNonvirtualDoubleMethodV,
CallNonvirtualDoubleMethodA,
CallNonvirtualVoidMethod,
CallNonvirtualVoidMethodV,
CallNonvirtualVoidMethodA,
GetFieldID,
GetObjectField,
GetBooleanField,
GetByteField,
GetCharField,
GetShortField,
GetIntField,
GetLongField,
GetFloatField,
GetDoubleField,
SetObjectField,
SetBooleanField,
SetByteField,
SetCharField,
SetShortField,
SetIntField,
SetLongField,
SetFloatField,
SetDoubleField,
GetStaticMethodID,
CallStaticObjectMethod,
CallStaticObjectMethodV,
CallStaticObjectMethodA,
CallStaticBooleanMethod,
CallStaticBooleanMethodV,
CallStaticBooleanMethodA,
CallStaticByteMethod,
CallStaticByteMethodV,
CallStaticByteMethodA,
CallStaticCharMethod,
CallStaticCharMethodV,
CallStaticCharMethodA,
CallStaticShortMethod,
CallStaticShortMethodV,
CallStaticShortMethodA,
CallStaticIntMethod,
CallStaticIntMethodV,
CallStaticIntMethodA,
CallStaticLongMethod,
CallStaticLongMethodV,
CallStaticLongMethodA,
CallStaticFloatMethod,
CallStaticFloatMethodV,
CallStaticFloatMethodA,
CallStaticDoubleMethod,
CallStaticDoubleMethodV,
CallStaticDoubleMethodA,
CallStaticVoidMethod,
CallStaticVoidMethodV,
CallStaticVoidMethodA,
GetStaticFieldID,
GetStaticObjectField,
GetStaticBooleanField,
GetStaticByteField,
GetStaticCharField,
GetStaticShortField,
GetStaticIntField,
GetStaticLongField,
GetStaticFloatField,
GetStaticDoubleField,
SetStaticObjectField,
SetStaticBooleanField,
SetStaticByteField,
SetStaticCharField,
SetStaticShortField,
SetStaticIntField,
SetStaticLongField,
SetStaticFloatField,
SetStaticDoubleField,
NewString,
GetStringLength,
GetStringChars,
ReleaseStringChars,
NewStringUTF,
GetStringUTFLength,
GetStringUTFChars,
ReleaseStringUTFChars,
GetArrayLength,
NewObjectArray,
GetObjectArrayElement,
SetObjectArrayElement,
NewBooleanArray,
NewByteArray,
NewCharArray,
NewShortArray,
NewIntArray,
NewLongArray,
NewFloatArray,
NewDoubleArray,
GetBooleanArrayElements,
GetByteArrayElements,
GetCharArrayElements,
GetShortArrayElements,
GetIntArrayElements,
GetLongArrayElements,
GetFloatArrayElements,
GetDoubleArrayElements,
ReleaseBooleanArrayElements,
ReleaseByteArrayElements,
ReleaseCharArrayElements,
ReleaseShortArrayElements,
ReleaseIntArrayElements,
ReleaseLongArrayElements,
ReleaseFloatArrayElements,
ReleaseDoubleArrayElements,
GetBooleanArrayRegion,
GetByteArrayRegion,
GetCharArrayRegion,
GetShortArrayRegion,
GetIntArrayRegion,
GetLongArrayRegion,
GetFloatArrayRegion,
GetDoubleArrayRegion,
SetBooleanArrayRegion,
SetByteArrayRegion,
SetCharArrayRegion,
SetShortArrayRegion,
SetIntArrayRegion,
SetLongArrayRegion,
SetFloatArrayRegion,
SetDoubleArrayRegion,
RegisterNatives,
UnregisterNatives,
MonitorEnter,
MonitorExit,
GetJavaVM,
};
基本上jni的資料和方法都差不多放這裡了,你就可以隨便開發了。這個你也可以去看
jni完全手冊
JNI與C/C++資料型別的轉換(效率開發)
字元陣列與jbyteArray
- jbyteArray轉字元陣列
int byteSize = (int) env->GetArrayLength(jbyteArrayData); //jbyteArrayData是jbyteArray型別的資料
unsigned char* data = new unsigned char[byteSize + 1];
env->GetByteArrayRegion(jbyteArrayData, 0, byteSize, reinterpret_cast<jbyte*>(data));
data[byteSize] = '\0';
- 字元陣列轉jbyteArray
jbyte *jb = (jbyte*) data; //data是字元陣列型別
jbyteArray jarray = env->NewByteArray(byteSize); //byteSize是字元陣列大小
env->SetByteArrayRegion(jarray, 0, byteSize, jb);
字元陣列與jstring
- jstring轉字元陣列
char* JstringToChar(JNIEnv* env, jstring jstr) {
if(jstr == NULL) {
return NULL;
}
char* rtn = NULL;
jclass clsstring = env->FindClass("java/lang/String");
jstring strencode = env->NewStringUTF("utf-8");
jmethodID mid = env->GetMethodID(clsstring, "getBytes",
"(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid, strencode);
jsize alen = env->GetArrayLength(barr);
jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
rtn = (char*) malloc(alen + 1);
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);
return rtn;
}
- 字元陣列轉jstring
jstring StrtoJstring(JNIEnv* env, const char* pat)
{
jclass strClass = env->FindClass("java/lang/String");
jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
jbyteArray bytes = env->NewByteArray(strlen(pat));
env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat);
jstring encoding = env->NewStringUTF("utf-8");
return (jstring)env->NewObject(strClass, ctorID, bytes, encoding);
}
特麼最簡單的可以直接使用
jstring jstr = env->NewStringUTF(str);
jint與int的互轉都可以直接使用強轉,如:
jint i = (jint) 1024;
上面的程式碼你看見了嗎,都是env的一級指標來做的,所以是cpp的使用方法,如果你要轉成c的那麼就把env替換為(*env)好了,具體的方法可能有點小改動(請自行去參考jni手冊),報錯的地方請自行引入相關的.h檔案,估計對你瞭解jni有更深的瞭解。
總結
本篇主要介紹了JNI動態註冊native方法,並且順便截了幾個jni的圖,以及使用的基本資料轉換處理,至於實際應用中比如java 呼叫c,c呼叫java以及混合呼叫等我們都需要實踐中去處理問題。
至於學習過程中可能用到反射或其他更多的東西,還值得我們去挖掘,難道你就不想知道美圖秀秀哪些對圖片處理是怎麼利用jni來實現的?
come on.enjoy it !