Android JNI編程—JNI基礎(chǔ)

最近看到了很多關(guān)于熱補的開源項目——Depoxed(阿里)、AnFix(阿里)、DynamicAPK(攜程)等,它們都用到了JNI編程,并且JNI編程也貫穿了Android系統(tǒng),學(xué)會JNI編程對于我們學(xué)習(xí)研究Android源碼、Android安全以及Android安全加固等都是有所幫助的。但是對于我們這些寫Android應(yīng)用的,大部分時間都是在使用Java編程,很少使用C/C++編程,對于JNI編程也了解的比較少,那么我們就來簡單的了解一下JNI編程的基礎(chǔ)吧。

什么是JNI,怎么使用

JNI——Java Native Interface,它是Java平臺的一個特性(并不是Android系統(tǒng)特有的)。其實主要是定義了一些JNI函數(shù),讓開發(fā)者可以通過調(diào)用這些函數(shù)實現(xiàn)Java代碼調(diào)用C/C++的代碼,C/C++的代碼也可以調(diào)用Java的代碼,這樣就可以發(fā)揮各個語言的特點了。那么怎么使用JNI呢,一般情況下我們首先是將寫好的C/C++代碼編譯成對應(yīng)平臺的動態(tài)庫(windows一般是dll文件,linux一般是so文件等),這里我們是針對Android平臺,所以只討論so庫。由于JNI編程支持C和C++編程,這里我們的栗子都是使用C++,對于C的版本可能會有些差異,但是主要的內(nèi)容還是一致的,大家可以觸類旁通。

我的好基友程序亦非猿表示看不懂,所以害怕我的小伙伴們都有一樣的困惑,這里補充一下這篇文章的主要內(nèi)容:

1.Java的native方法怎么與C/C++中的函數(shù)鏈接起來

2.JNI定義了與Java對應(yīng)的數(shù)據(jù)類型,用于JNI編程。

3.描述符-用于描述類名或者數(shù)據(jù)類型,我們在C/C++層為了獲取Java層的對象、變量以及描述Java的方法,需要用字符串來描述需要獲取對象的類名、變量類型以及方法。
文章主要從上面三個方面做了簡單介紹,等下篇文章介紹NDK實踐的時候,回來再看看相信會有更好的理解。

從一個栗子說起

這里還是直接從代碼說起,這樣更加形象和直觀,也便于理解。今天使用的Java代碼如下:

public class AndroidJni {

    static{
        System.loadLibrary("main");
    }

    public native void dynamicLog();

    public native void staticLog();

}

這里我們定義了兩個聲明為native的方法,并聲明了一塊靜態(tài)區(qū)域,在該靜態(tài)區(qū)域類加載名為libmain.so的庫,這里我們說是libmain.so庫,但是加載的時候卻只寫了“main”,其實大家只要知道這是約定的就可以了。

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;
}

這里引用了兩個頭文件,jni.h和mylog.h,其中jni.h是定義了很多我們使用的JNI函數(shù)和結(jié)構(gòu)體,mylog.h是我自己定義的打印Android log的函數(shù)(功能和Java的Log類相同)。
這里暫時不討論怎么編譯成so庫以及so庫的一些規(guī)范,會在下一篇文章中介紹。這里假定將上面的C++程序編譯成了一個叫l(wèi)ibmain.so的文件。在Java層使用System.loadLibarary("main")方法將該so庫加載起來,使得dynamicLog()、staticLog()和對應(yīng)的Java_com_github_songnick_jni_AndroidJni_staticLog()、nativeDynamicLog()兩個native方法鏈接起來,當(dāng)然這部分工作都是由Java虛擬機完成的,那么具體是怎么完成的,接下來將根據(jù)上面的代碼進行分析。

靜態(tài)注冊native方法

在上面的代碼中看到了JNIEXPORT和JNICALL關(guān)鍵字,這兩個關(guān)鍵字是兩個宏定義,他主要的作用就是說明該函數(shù)為JNI函數(shù),在Java虛擬機加載的時候會鏈接對應(yīng)的native方法,在AndroidJni.java的類中聲明了staticLog()為native方法,他對應(yīng)的JNI函數(shù)就是Java_com_github_songnick_jni_AndroidJni_staticLog(),那么是怎么鏈接的呢,在Java虛擬機加載so庫時,如果發(fā)現(xiàn)含有上面兩個宏定義的函數(shù)時就會鏈接到對應(yīng)Java層的native方法,那么怎么知道對應(yīng)Java中的哪個類的哪個native方法呢,我們仔細(xì)觀察JNI函數(shù)名的構(gòu)成其實是:Java_PkgName_ClassName_NativeMethodName,以Java為前綴,并且用“_”下劃線將包名、類名以及native方法名連接起來就是對應(yīng)的JNI函數(shù)了。一般情況下我們可以自己手動的去按照這個規(guī)則寫,但是如果native方法特別多,那么還是有一定的工作量,并且在寫的過程中不小心就有可能寫錯,其實Java給我們提供了javah的工具幫助生成相應(yīng)的頭文件。在生成的頭文件中就是按照上面說的規(guī)則生成了對應(yīng)的JNI函數(shù),我們在開發(fā)的時候直接copy過去就可以了。這里上面的代碼為例,在AndroidStudio中編譯后,進入項目的目錄app/build/intermediates/classes/debug下,運行如下命令:

javah -d jni com.github.songnick.jni.AndroidJni

這里-d指定生成.h文件存放的目錄(如果沒有就會自動創(chuàng)建),com.github.songnick.jni.AndroidJni表示指定目錄下的class文件。這里簡單介紹一下生成的JNI函數(shù)包含兩個固定的參數(shù)變量,分別是JNIEnv和jobject,其中JNIEnv后面會介紹,jobject就是當(dāng)前與之鏈接的native方法隸屬的類對象(類似于Java中的this)。這兩個變量都是Java虛擬機生成并在調(diào)用時傳遞進來的。

動態(tài)注冊

上面我們介紹了靜態(tài)注冊native方法的過程,就是Java層聲明的native方法和JNI函數(shù)是一一對應(yīng)的,那么有沒有方法讓Java層的native方法和任意的JNI函數(shù)鏈接起來,當(dāng)然是可以的,這就得使用動態(tài)注冊的方法。接下來就看看如何實現(xiàn)動態(tài)注冊的。

JNI_OnLoad函數(shù)

當(dāng)我們使用System.loadLibarary()方法加載so庫的時候,Java虛擬機就會找到這個函數(shù)并調(diào)用該函數(shù),因此可以在該函數(shù)中做一些初始化的動作,其實這個函數(shù)就是相當(dāng)于Activity中的onCreate()方法。該函數(shù)前面有三個關(guān)鍵字,分別是JNIEXPORT、JNICALL和jint,其中JNIEXPORT和JNICALL是兩個宏定義,用于指定該函數(shù)是JNI函數(shù)。jint是JNI定義的數(shù)據(jù)類型,因為Java層和C/C++的數(shù)據(jù)類型或者對象不能直接相互的引用或者使用,JNI層定義了自己的數(shù)據(jù)類型,用于銜接Java層和JNI層,至于這些數(shù)據(jù)類型我們在后面介紹。這里的jint對應(yīng)Java的int數(shù)據(jù)類型,該函數(shù)返回的int表示當(dāng)前使用的JNI的版本,其實類似于Android系統(tǒng)的API版本一樣,不同的JNI版本中定義的一些不同的JNI函數(shù)。該函數(shù)會有兩個參數(shù),其中*jvm為Java虛擬機實例,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)體指針,其實JNIEnv結(jié)構(gòu)體是指向一個函數(shù)表的,該函數(shù)表指向了對應(yīng)的JNI函數(shù),我們通過調(diào)用這些JNI函數(shù)實現(xiàn)JNI編程,在后面我們還會對其進行介紹。

獲取Java對象,完成動態(tài)注冊

上面介紹了如何獲取JNIEnv結(jié)構(gòu)體指針,得到這個結(jié)構(gòu)體指針后我們就可以調(diào)用JNIEnv中的RegisterNatives函數(shù)完成動態(tài)注冊native方法了。該方法如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

第一個參數(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");

第二個參數(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;

第三個參數(shù)為注冊native方法的數(shù)量。一般會動態(tài)注冊多個native方法,首先會定義一個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這個結(jié)構(gòu)體,它就老厲害了,指向一個函數(shù)表,該函數(shù)表指向一系列的JNI函數(shù),我們通過調(diào)用這些JNI函數(shù)可以實現(xiàn)與Java層的交互,這里簡單的看看幾個定義的函數(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, ...)
..........

這里簡單的看看上面的四個函數(shù),GetFieldID()函數(shù)是獲取Java對象中某個變量的ID,GetBooleanField()函數(shù)是根據(jù)變量的ID獲取數(shù)據(jù)類型為Boolean的變量。GetMethodID()函數(shù)是獲取Java對象中對應(yīng)方法的ID,CallVoidMethod()根據(jù)methodID調(diào)用對應(yīng)對象中的方法,并且該方法的返回值為Void類型。通過這些函數(shù)我們可以實現(xiàn)調(diào)用Java層的代碼。更多的函數(shù)大家還是看看API文檔吧!

JNI數(shù)據(jù)類型

上面我們提到JNI定義了一些自己的數(shù)據(jù)類型。這些數(shù)據(jù)類型是銜接Java層和C/C++層的,如果有一個對象傳遞下來,那么對于C/C++來說是沒辦法識別這個對象的,同樣的如果C/C++的指針對于Java層來說它也是沒辦法識別的,那么就需要JNI進行匹配,所以需要定義一些自己的數(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中的某個方法的時候我們首先要獲取它的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ù)只有一個字符串參數(shù),我們發(fā)現(xiàn)該字符串如下所示:

com/github/songnick/jni/AndroidJni

其實這個就是JNI定義了對類的描述符,它的規(guī)則就是將"com.github.songnick.jni.AndroidJni"中的“.”用“/”代替。

2.方法描述符
?前面我們動態(tài)注冊native方法的時候結(jié)構(gòu)體JNINativeMethod中含有方法描述符,就是確定native方法的參數(shù)和返回值,我們這里定義的dynamicLog()方法沒有參數(shù),返回值為空所以對應(yīng)的描述符為:"()V",括號類為參數(shù),V表示返回值為空。下面還是看看幾個栗子吧:

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ù)類型,對于這些類型的描述符在下個部分介紹。這里數(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é)

上面的部分我們通過一個栗子簡單的對JNI編程進行了分析,這里只是對簡單的進行了介紹,只是JNI編程的一部分,我相信任何一門技術(shù)或者技術(shù)點都不能通過一篇文章達到精通,更多的還是靠實踐,只有在實踐的過程中發(fā)現(xiàn)問題-解決問題,才能對知識更好的理解和認(rèn)識,從而達到精通。所以希望通過這邊文章你可以對JNI編程有一個初步的認(rèn)識,不會感覺JNI編程很難。大家可以多看看JNI的API文檔,我這里也有一份JNI的教程,大家下載下來看看吧,這樣會對JNI有更多的了解。這里還要說一下對于Android的JNI編程還是有點區(qū)別的,大家可以多看看Google官方文檔對于JNI編程的一些指導(dǎo)和Demo程序。下一篇文章將介紹Android NDK相關(guān)的內(nèi)容,將JNI編程運用到Android開發(fā)中。

希望在Android學(xué)習(xí)的路上,大家共同成長!
(如果有人有對這個Android職位感興趣,我可以幫忙內(nèi)推,簡歷發(fā)我郵件?。?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容