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

最近看到了很多關(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ā)我郵件!)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內(nèi)容