最近看到了很多關(guān)于熱補(bǔ)的開源項(xiàng)目——Depoxed(阿里)、AnFix(阿里)、DynamicAPK(攜程)等,它們都用到了JNI編程,并且JNI編程也貫穿了Android系統(tǒng),學(xué)會JNI編程對于我們學(xué)習(xí)研究Android源碼、Android安全以及Android安全加固等都是有所幫助的。但是對于我們這些寫Android應(yīng)用的,大部分時(shí)間都是在使用Java編程,很少使用C/C++編程,對于JNI編程也了解的比較少,那么我們就來簡單的了解一下JNI編程的基礎(chǔ)吧。
什么是JNI,怎么使用
JNI——Java Native Interface,它是Java平臺的一個(gè)特性(并不是Android系統(tǒng)特有的)。其實(shí)主要是定義了一些JNI函數(shù),讓開發(fā)者可以通過調(diào)用這些函數(shù)實(shí)現(xiàn)Java代碼調(diào)用C/C++的代碼,C/C++的代碼也可以調(diào)用Java的代碼,這樣就可以發(fā)揮各個(gè)語言的特點(diǎn)了。那么怎么使用JNI呢,一般情況下我們首先是將寫好的C/C++代碼編譯成對應(yīng)平臺的動態(tài)庫(windows一般是dll文件,linux一般是so文件等),這里我們是針對Android平臺,所以只討論so庫。由于JNI編程支持C和C++編程,這里我們的栗子都是使用C++,對于C的版本可能會有些差異,但是主要的內(nèi)容還是一致的,大家可以觸類旁通。
我的好基友程序亦非猿表示看不懂,所以害怕我的小伙伴們都有一樣的困惑,這里補(bǔ)充一下這篇文章的主要內(nèi)容:
1.Java的native方法怎么與C/C++中的函數(shù)鏈接起來
2.JNI定義了與Java對應(yīng)的數(shù)據(jù)類型,用于JNI編程。
3.描述符-用于描述類名或者數(shù)據(jù)類型,我們在C/C++層為了獲取Java層的對象、變量以及描述Java的方法,需要用字符串來描述需要獲取對象的類名、變量類型以及方法。
文章主要從上面三個(gè)方面做了簡單介紹,等下篇文章介紹NDK實(shí)踐的時(shí)候,回來再看看相信會有更好的理解。
從一個(gè)栗子說起
這里還是直接從代碼說起,這樣更加形象和直觀,也便于理解。今天使用的Java代碼如下:
public class AndroidJni {
static{
System.loadLibrary("main");
}
public native void dynamicLog();
public native void staticLog();
}
這里我們定義了兩個(gè)聲明為native的方法,并聲明了一塊靜態(tài)區(qū)域,在該靜態(tài)區(qū)域類加載名為libmain.so的庫,這里我們說是libmain.so庫,但是加載的時(shí)候卻只寫了“main”,其實(shí)大家只要知道這是約定的就可以了。
C++代碼如下:
#include <jni.h>
#define LOG_TAG "main.cpp"
#include "mylog.h"
static void nativeDynamicLog(JNIEnv *evn, jobject obj){
LOGE("hell main");
}
JNIEXPORT void JNICALL Java_com_github_songnick_jni_AndroidJni_staticLog (JNIEnv *env, jobject obj)
{
LOGE("static register log ");
}
JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog},};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
LOGE("JNI_OnLoad comming");
jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
return JNI_VERSION_1_4;
}
這里引用了兩個(gè)頭文件,jni.h和mylog.h,其中jni.h是定義了很多我們使用的JNI函數(shù)和結(jié)構(gòu)體,mylog.h是我自己定義的打印Android log的函數(shù)(功能和Java的Log類相同)。
這里暫時(shí)不討論怎么編譯成so庫以及so庫的一些規(guī)范,會在下一篇文章中介紹。這里假定將上面的C++程序編譯成了一個(gè)叫l(wèi)ibmain.so的文件。在Java層使用System.loadLibarary("main")方法將該so庫加載起來,使得dynamicLog()、staticLog()和對應(yīng)的Java_com_github_songnick_jni_AndroidJni_staticLog()、nativeDynamicLog()兩個(gè)native方法鏈接起來,當(dāng)然這部分工作都是由Java虛擬機(jī)完成的,那么具體是怎么完成的,接下來將根據(jù)上面的代碼進(jìn)行分析。
靜態(tài)注冊native方法
在上面的代碼中看到了JNIEXPORT和JNICALL關(guān)鍵字,這兩個(gè)關(guān)鍵字是兩個(gè)宏定義,他主要的作用就是說明該函數(shù)為JNI函數(shù),在Java虛擬機(jī)加載的時(shí)候會鏈接對應(yīng)的native方法,在AndroidJni.java的類中聲明了staticLog()為native方法,他對應(yīng)的JNI函數(shù)就是Java_com_github_songnick_jni_AndroidJni_staticLog(),那么是怎么鏈接的呢,在Java虛擬機(jī)加載so庫時(shí),如果發(fā)現(xiàn)含有上面兩個(gè)宏定義的函數(shù)時(shí)就會鏈接到對應(yīng)Java層的native方法,那么怎么知道對應(yīng)Java中的哪個(gè)類的哪個(gè)native方法呢,我們仔細(xì)觀察JNI函數(shù)名的構(gòu)成其實(shí)是:Java_PkgName_ClassName_NativeMethodName,以Java為前綴,并且用“_”下劃線將包名、類名以及native方法名連接起來就是對應(yīng)的JNI函數(shù)了。一般情況下我們可以自己手動的去按照這個(gè)規(guī)則寫,但是如果native方法特別多,那么還是有一定的工作量,并且在寫的過程中不小心就有可能寫錯(cuò),其實(shí)Java給我們提供了javah的工具幫助生成相應(yīng)的頭文件。在生成的頭文件中就是按照上面說的規(guī)則生成了對應(yīng)的JNI函數(shù),我們在開發(fā)的時(shí)候直接copy過去就可以了。這里上面的代碼為例,在AndroidStudio中編譯后,進(jìn)入項(xiàng)目的目錄app/build/intermediates/classes/debug下,運(yùn)行如下命令:
javah -d jni com.github.songnick.jni.AndroidJni
這里-d指定生成.h文件存放的目錄(如果沒有就會自動創(chuàng)建),com.github.songnick.jni.AndroidJni表示指定目錄下的class文件。這里簡單介紹一下生成的JNI函數(shù)包含兩個(gè)固定的參數(shù)變量,分別是JNIEnv和jobject,其中JNIEnv后面會介紹,jobject就是當(dāng)前與之鏈接的native方法隸屬的類對象(類似于Java中的this)。這兩個(gè)變量都是Java虛擬機(jī)生成并在調(diào)用時(shí)傳遞進(jìn)來的。
動態(tài)注冊
上面我們介紹了靜態(tài)注冊native方法的過程,就是Java層聲明的native方法和JNI函數(shù)是一一對應(yīng)的,那么有沒有方法讓Java層的native方法和任意的JNI函數(shù)鏈接起來,當(dāng)然是可以的,這就得使用動態(tài)注冊的方法。接下來就看看如何實(shí)現(xiàn)動態(tài)注冊的。
JNI_OnLoad函數(shù)
當(dāng)我們使用System.loadLibarary()方法加載so庫的時(shí)候,Java虛擬機(jī)就會找到這個(gè)函數(shù)并調(diào)用該函數(shù),因此可以在該函數(shù)中做一些初始化的動作,其實(shí)這個(gè)函數(shù)就是相當(dāng)于Activity中的onCreate()方法。該函數(shù)前面有三個(gè)關(guān)鍵字,分別是JNIEXPORT、JNICALL和jint,其中JNIEXPORT和JNICALL是兩個(gè)宏定義,用于指定該函數(shù)是JNI函數(shù)。jint是JNI定義的數(shù)據(jù)類型,因?yàn)镴ava層和C/C++的數(shù)據(jù)類型或者對象不能直接相互的引用或者使用,JNI層定義了自己的數(shù)據(jù)類型,用于銜接Java層和JNI層,至于這些數(shù)據(jù)類型我們在后面介紹。這里的jint對應(yīng)Java的int數(shù)據(jù)類型,該函數(shù)返回的int表示當(dāng)前使用的JNI的版本,其實(shí)類似于Android系統(tǒng)的API版本一樣,不同的JNI版本中定義的一些不同的JNI函數(shù)。該函數(shù)會有兩個(gè)參數(shù),其中*jvm為Java虛擬機(jī)實(shí)例,JavaVM結(jié)構(gòu)體定義了以下函數(shù):
DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv
這里我們使用了GetEnv函數(shù)獲取JNIEnv變量,上面的JNI_OnLoad函數(shù)中有如下代碼:
JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
這里調(diào)用了GetEnv函數(shù)獲取JNIEnv結(jié)構(gòu)體指針,其實(shí)JNIEnv結(jié)構(gòu)體是指向一個(gè)函數(shù)表的,該函數(shù)表指向了對應(yīng)的JNI函數(shù),我們通過調(diào)用這些JNI函數(shù)實(shí)現(xiàn)JNI編程,在后面我們還會對其進(jìn)行介紹。
獲取Java對象,完成動態(tài)注冊
上面介紹了如何獲取JNIEnv結(jié)構(gòu)體指針,得到這個(gè)結(jié)構(gòu)體指針后我們就可以調(diào)用JNIEnv中的RegisterNatives函數(shù)完成動態(tài)注冊native方法了。該方法如下:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
第一個(gè)參數(shù)是Java層對應(yīng)包含native方法的對象(這里就是AndroidJni對象),通過調(diào)用JNIEnv對應(yīng)的函數(shù)獲取class對象(FindClass函數(shù)的參數(shù)為需要獲取class對象的類描述符):
jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");
第二個(gè)參數(shù)是JNINativeMethod結(jié)構(gòu)體指針,這里的JNINativeMethod結(jié)構(gòu)體是描述Java層native方法的,它的定義如下:
typedef struct {
const char* name;//Java層native方法的名字
const char* signature;//Java層native方法的描述符
void* fnPtr;//對應(yīng)JNI函數(shù)的指針
} JNINativeMethod;
第三個(gè)參數(shù)為注冊native方法的數(shù)量。一般會動態(tài)注冊多個(gè)native方法,首先會定義一個(gè)JNINativeMethod數(shù)組,然后將該數(shù)組指針作為RegisterNative函數(shù)的參數(shù)傳入,所以這里定義了如下的JNINativeMethod數(shù)組:
JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog}};
最后調(diào)用RegisterNative函數(shù)完成動態(tài)注冊:
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
JNIEnv結(jié)構(gòu)體
上面提到JNIEnv這個(gè)結(jié)構(gòu)體,它就老厲害了,指向一個(gè)函數(shù)表,該函數(shù)表指向一系列的JNI函數(shù),我們通過調(diào)用這些JNI函數(shù)可以實(shí)現(xiàn)與Java層的交互,這里簡單的看看幾個(gè)定義的函數(shù):
..........
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jboolean GetBooleanField(jobject obj, jfieldID fieldID)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
CallVoidMethod(jobject obj, jmethodID methodID, ...)
CallBooleanMethod(jobject obj, jmethodID methodID, ...)
..........
這里簡單的看看上面的四個(gè)函數(shù),GetFieldID()函數(shù)是獲取Java對象中某個(gè)變量的ID,GetBooleanField()函數(shù)是根據(jù)變量的ID獲取數(shù)據(jù)類型為Boolean的變量。GetMethodID()函數(shù)是獲取Java對象中對應(yīng)方法的ID,CallVoidMethod()根據(jù)methodID調(diào)用對應(yīng)對象中的方法,并且該方法的返回值為Void類型。通過這些函數(shù)我們可以實(shí)現(xiàn)調(diào)用Java層的代碼。更多的函數(shù)大家還是看看API文檔吧!
JNI數(shù)據(jù)類型
上面我們提到JNI定義了一些自己的數(shù)據(jù)類型。這些數(shù)據(jù)類型是銜接Java層和C/C++層的,如果有一個(gè)對象傳遞下來,那么對于C/C++來說是沒辦法識別這個(gè)對象的,同樣的如果C/C++的指針對于Java層來說它也是沒辦法識別的,那么就需要JNI進(jìn)行匹配,所以需要定義一些自己的數(shù)據(jù)類型。
1.原始數(shù)據(jù)類型
Java Type | Native Typ | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
2.引用類型
前面我們在獲取AndroidJni對象的使用通過定義jclass引用,然后調(diào)用FindClass函數(shù)獲取了該對象,所以JNI也定義了一些引用類型以便JNI層調(diào)用,具體的引用類型如下:
jobject (all Java objects)
|
|-- jclass (java.lang.Class objects)
|-- jstring (java.lang.String objects)
|-- jarray (array)
| |--jobjectArray (object arrays)
| |--jbooleanArray (boolean arrays)
| |--jbyteArray (byte arrays)
| |--jcharArray (char arrays)
| |--jshortArray (short arrays)
| |--jintArray (int arrays)
| |--jlongArray (long arrays)
| |--jfloatArray (float arrays)
| |--jdoubleArray (double arrays)
|
|--jthrowable
3.方法和變量的ID
?當(dāng)需要調(diào)用Java中的某個(gè)方法的時(shí)候我們首先要獲取它的ID,根據(jù)ID調(diào)用JNI函數(shù)獲取該方法,變量的獲取過程也是同樣的過程,這些ID的結(jié)構(gòu)體定義如下:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method IDs */
描述符
1.類描述符
?前面為了獲取Java的AndroidJni對象,是通過調(diào)用FindClass()函數(shù)獲取的,該函數(shù)參數(shù)只有一個(gè)字符串參數(shù),我們發(fā)現(xiàn)該字符串如下所示:
com/github/songnick/jni/AndroidJni
其實(shí)這個(gè)就是JNI定義了對類的描述符,它的規(guī)則就是將"com.github.songnick.jni.AndroidJni"中的“.”用“/”代替。
2.方法描述符
?前面我們動態(tài)注冊native方法的時(shí)候結(jié)構(gòu)體JNINativeMethod中含有方法描述符,就是確定native方法的參數(shù)和返回值,我們這里定義的dynamicLog()方法沒有參數(shù),返回值為空所以對應(yīng)的描述符為:"()V",括號類為參數(shù),V表示返回值為空。下面還是看看幾個(gè)栗子吧:
Method Descriptor | Java Language Type |
---|---|
"()Ljava/lang/String;" | String f(); |
"(ILjava/lang/Class;)J" | long f(int i, Class c); |
"([B)V" | String(byte[] bytes); |
上面的栗子我們看到方法的返回類型和方法參數(shù)有引用類型以及boolean、int等基本數(shù)據(jù)類型,對于這些類型的描述符在下個(gè)部分介紹。這里數(shù)組的描述符以"["和對應(yīng)的類型描述符來表述。對于二維數(shù)組以及三維數(shù)組則以"[["和"[[["表示:
Descriptor | Java Langauage Type |
---|---|
"[[I" | int[][] |
"[[[D" | double[][][] |
3.數(shù)據(jù)類型描述符
?前面我們說了方法的描述符,那么針對boolean、int等數(shù)據(jù)類型描述符是怎樣的呢,JNI對基本數(shù)據(jù)類型的描述符定義如下:
Field Desciptor | Java Language Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | floa |
D | double |
對于引用類型描述符是以"L"開頭";"結(jié)尾,示例如下所示:
Field Desciptor | Java Language Type |
---|---|
"Ljava/lang/String;" | String |
"[Ljava/lang/Object;" | Object[] |
總結(jié)
上面的部分我們通過一個(gè)栗子簡單的對JNI編程進(jìn)行了分析,這里只是對簡單的進(jìn)行了介紹,只是JNI編程的一部分,我相信任何一門技術(shù)或者技術(shù)點(diǎn)都不能通過一篇文章達(dá)到精通,更多的還是靠實(shí)踐,只有在實(shí)踐的過程中發(fā)現(xiàn)問題-解決問題,才能對知識更好的理解和認(rèn)識,從而達(dá)到精通。所以希望通過這邊文章你可以對JNI編程有一個(gè)初步的認(rèn)識,不會感覺JNI編程很難。大家可以多看看JNI的API文檔,我這里也有一份JNI的教程,大家下載下來看看吧,這樣會對JNI有更多的了解。這里還要說一下對于Android的JNI編程還是有點(diǎn)區(qū)別的,大家可以多看看Google官方文檔對于JNI編程的一些指導(dǎo)和Demo程序。下一篇文章將介紹Android NDK相關(guān)的內(nèi)容,將JNI編程運(yùn)用到Android開發(fā)中。
希望在Android學(xué)習(xí)的路上,大家共同成長!
(如果有人有對這個(gè)Android職位感興趣,我可以幫忙內(nèi)推,簡歷發(fā)我郵件!)