Android系統(tǒng)源碼分析-JNI

序言

因?yàn)樵诮酉聛淼脑创a分析中將涉及大量的Java和Native的互相調(diào)用。當(dāng)然對(duì)于我們的代碼分析沒有什么影響,但是,這樣一個(gè)黑盒子擺在面前,對(duì)于其實(shí)現(xiàn)原理還是充滿了好奇心。本篇將從JNI最基本的概念到簡(jiǎn)單的代碼實(shí)例和其實(shí)現(xiàn)原理逐步展開。

JNI

JNI(Java Native Interface,Java本地接口)是一種編程框架使得Java虛擬機(jī)中的Java程序可以調(diào)用本地應(yīng)用/或庫,也可以被其他程序調(diào)用。 本地程序一般是用其它語言C,C++或匯編語言編寫的, 并且被編譯為基于本機(jī)硬件和操作系統(tǒng)的程序。在Android平臺(tái),為了更方便開發(fā)者的使用和增強(qiáng)其功能性,Android提供了NDK來更方便開發(fā)者的開發(fā)。

JNI工作

為什么要有JNI?

JNI允許程序員用其他編程語言來解決用純粹的Java代碼不好處理的情況, 例如, Java標(biāo)準(zhǔn)庫不支持的平臺(tái)相關(guān)功能或者程序庫。也用于改造已存在的用其它語言寫的程序, 供Java程序調(diào)用。許多基于JNI的標(biāo)準(zhǔn)庫提供了很多功能給程序員使用, 例如文件I/O、音頻相關(guān)的功能。當(dāng)然,也有各種高性能的程序,以及平臺(tái)相關(guān)的API實(shí)現(xiàn), 允許所有Java應(yīng)用程序安全并且平臺(tái)獨(dú)立地使用這些功能。Java層可以用來負(fù)責(zé)UI功能實(shí)現(xiàn),而C++負(fù)責(zé)進(jìn)行計(jì)算操作。

JNI框架允許Native方法調(diào)用Java對(duì)象,就像Java程序訪問Native對(duì)象一樣方便。Native方法可以創(chuàng)建Java對(duì)象,讀取這些對(duì)象, 并調(diào)用Java對(duì)象執(zhí)行某些方法。當(dāng)然Native方法也可以讀取由Java程序自身創(chuàng)建的對(duì)象,并調(diào)用這些對(duì)象的方法。

Hello World

這里,我們先通過一個(gè)簡(jiǎn)單的Hello World實(shí)例來對(duì)JNI的調(diào)用流程有一個(gè)直觀的印象,然后針對(duì)其中的實(shí)現(xiàn)原理和細(xì)節(jié)做分析。

1. 在Java文件中定義native函數(shù)

在此方法聲明中,使用 native 關(guān)鍵字的作用是告訴虛擬機(jī),函數(shù)位于共享庫中(即在原生端實(shí)現(xiàn))。

private native String helloWorld();

2.利用Javah生成頭文件

對(duì)于native方法的命名規(guī)則,函數(shù)名根據(jù)以下規(guī)則構(gòu)建:

  • 在名稱前面加上 Java_。
  • 描述與頂級(jí)源目錄相關(guān)的文件路徑。
  • 使用下劃線代替正斜杠。
  • 刪掉 .java 文件擴(kuò)展名。
  • 在最后一個(gè)下劃線后,附加函數(shù)名。

按照這些規(guī)則,此示例使用的函數(shù)名為 Java_com_example_hellojni_HelloJni_stringFromJNI。 此名稱描述 hellojni/src/com/example/hellojni/HelloJni.java 中一個(gè)名為 stringFromJNI()的 Java 函數(shù)。我們想通過更簡(jiǎn)單的方式,讓寫native函數(shù)如同和寫java函數(shù)沒有這一步的轉(zhuǎn)化,那么可以通過javah來實(shí)現(xiàn)。

javah -d ../jni -jni com.chenjensen.myapplication.MainActivity
  • d :頭文件輸出目錄
  • jni:生成jni文件

3.根據(jù)Javah生成的頭文件,實(shí)現(xiàn)相應(yīng)的native函數(shù)

JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
  (JNIEnv *, jobject);

頭文件中生成了我們的java文件中定義的native方法,也做好了類型轉(zhuǎn)化,我們只需要新建一個(gè)cpp文件來實(shí)現(xiàn)相應(yīng)的方法即可。

4.cpp文件

JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
        (JNIEnv *env, jobject)
{
    char *str = "Hello world";
    return (*env).NewStringUTF(str);
}

5.build文件中編譯支持指定的平臺(tái)(arm,x86等)

ndk {
     moduleName "hello"       //生成的so文件名字,調(diào)用C程序的代碼中會(huì)用到該名字
     abiFilters "armeabi", "armeabi-v7a", "x86" //輸出指定三種平臺(tái)下的so庫
}

這里指定了生成so文件的name之后,編譯系統(tǒng)就會(huì)從JNI目錄下去尋找相應(yīng)的c/cpp文件,來生成相應(yīng)的so文件。

6.執(zhí)行

在Java代碼中,native方法的執(zhí)行之前,要提前加載相應(yīng)的動(dòng)態(tài)庫,然后才可以執(zhí)行,一般會(huì)在該類中通過靜態(tài)代碼塊的方式來加載。應(yīng)用啟動(dòng)時(shí),調(diào)用此函數(shù)以加載 .so 文件。

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

這個(gè)時(shí)候,我們?cè)贘ava代碼中調(diào)用相應(yīng)的native代碼就會(huì)生效了。

那么在C/C++文件中如何調(diào)用Java呢,這里的調(diào)用方式和Java中通過反射查找一個(gè)類的調(diào)用相似。核心函數(shù)為以下幾個(gè)。

FindClass(), NewObject(), GetStaticMethodID(), 
GetMethodID(), CallStaticObjectMethod(), CallVoidMethod()

找到相應(yīng)的類,相應(yīng)的方法,調(diào)用相應(yīng)的類和方法。這里不在給出具體的代碼示例。可參考文章末尾給出的相應(yīng)鏈接。

如何調(diào)用

通過上述6個(gè)步驟,我們便實(shí)現(xiàn)了Java調(diào)用native函數(shù),借助了相應(yīng)的工具,我們可以很快的實(shí)現(xiàn)其互相調(diào)用,但是,工具也屏蔽掉了大量的實(shí)現(xiàn)細(xì)節(jié),讓這個(gè)過程變成黑盒,不了解其實(shí)現(xiàn)。這個(gè)過程中,
當(dāng)JVM調(diào)用這些函數(shù),傳遞了一個(gè)JNIEnv指針,一個(gè)jobject的指針,任何在Java方法中聲明的Java參數(shù)。

一個(gè)JNI函數(shù)看起來類似這樣:

JNIEXPORT void JNICALL Java_ClassName_MethodName
  (JNIEnv *env, jobject obj)
{
    /*Implement Native Method Here*/
}

Java和C++之間的調(diào)用,Java的執(zhí)行需要在JVM上,因此在調(diào)用的時(shí)候,JVM必須知道要調(diào)用那一個(gè)本地函數(shù),本地函數(shù)調(diào)用Java的時(shí)候,也必須要知道應(yīng)用對(duì)象和具體的函數(shù)。

JNI中C++和Java的執(zhí)行是在同一個(gè)線程,但是其線程值是不相同的。
JNIEnv是JNI的使用環(huán)境,JNIEnv對(duì)象是和線程綁定在一起的,在進(jìn)行調(diào)用的時(shí)候,會(huì)傳遞一個(gè)JavaVM的指針作為參數(shù),然后通過JavaVM的getEnv函數(shù)得到JNIEnv對(duì)象的指針。在Java中每次創(chuàng)建一個(gè)線程,都會(huì)生成新的JNIEnv對(duì)象。

在分析系統(tǒng)源碼的時(shí)候,我們可以看到很多的java對(duì)于native的調(diào)用,通過對(duì)于源碼的分析,我們發(fā)現(xiàn)在系統(tǒng)開機(jī)之后,就會(huì)有許多的Service進(jìn)程被啟動(dòng),這個(gè)時(shí)候,而其很多實(shí)現(xiàn)都是通過native來實(shí)現(xiàn)的,這個(gè)時(shí)候如何調(diào)用,讓我們回歸到系統(tǒng)的啟動(dòng)過程中。在Zygote進(jìn)程中首先會(huì)調(diào)用啟動(dòng)VM。

系統(tǒng)啟動(dòng)JNI注冊(cè)流程
if (startVm(&mJavaVM, &env, zygote) != 0) {
   return;
}

onVmCreated(env);

if (startReg(env) < 0) {
  return;
}
int AndroidRuntime::startReg(JNIEnv* env)
{
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    ....
    return 0;
}
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
    for (size_t i = 0; i < count; i++) {
        if (array[i].mProc(env) < 0) {
            return -1;
        }
    }
    return 0;
}
static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    REG_JNI(register_android_util_Log),
    .....
}

array[i]是指gRegJNI數(shù)組, 該數(shù)組有100多個(gè)成員。其中每一項(xiàng)成員都是通過REG_JNI宏定義。

 #define REG_JNI(name)      { name }
struct RegJNIRec {
        int (*mProc)(JNIEnv*);
 };

調(diào)用mProc,就等價(jià)于調(diào)用其參數(shù)名所指向的函數(shù)。 例如REG_JNI(register_com_android_internal_os_RuntimeInit).mProc也就是指進(jìn)入register_com_android_internal_os_RuntimeInit方法,進(jìn)入這些方法之后,就會(huì)是對(duì)于該類中的一些native方法和java方法的映射。

int register_com_android_internal_os_RuntimeInit(JNIEnv* env) {
    return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
        gMethods, NELEM(gMethods));
}
//gMethods:java層方法名與jni層的方法的一一映射關(guān)系
static JNINativeMethod gMethods[] = {
    { "nativeFinishInit", "()V",
        (void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
    { "nativeZygoteInit", "()V",
        (void*) com_android_internal_os_RuntimeInit_nativeZygoteInit },
    { "nativeSetExitWithoutCleanup", "(Z)V",
        (void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
};

至此就完成了對(duì)于native方法和Java方法的映射關(guān)聯(lián)。

  • 另一種加載方式

對(duì)于JNI方法的注冊(cè)無非是通過兩種方式一個(gè)是上述啟動(dòng)過程中的注冊(cè),一個(gè)是在程序中通過System.loadLibrary的方式進(jìn)行注冊(cè),這里,我們以System.loadLibrary來分析其注冊(cè)過程。

public static void loadLibrary(String libname) {
  Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
public static Runtime getRuntime() {
   return currentRuntime;
}
synchronized void load0(Class fromClass, String filename) {
    if (!(new File(filename).isAbsolute())) {
        throw new UnsatisfiedLinkError(
            "Expecting an absolute path of the library: " + filename);
    }
    if (filename == null) {
        throw new NullPointerException("filename == null");
    }
    String error = doLoad(filename, fromClass.getClassLoader());
    if (error != null) {
        throw new UnsatisfiedLinkError(error);
    }
}
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
    BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
    librarySearchPath = dexClassLoader.getLdLibraryPath();
}
        synchronized (this) {
    return nativeLoad(name, loader, librarySearchPath);
}

經(jīng)過層層調(diào)用之后來到了nativeLoad方法,這里對(duì)于這段代碼的分析,目的是為了了解,整個(gè)JNI的注冊(cè)過程和調(diào)用的時(shí)候,JVM是如何找到相應(yīng)的native方法的。

對(duì)于nativeLoad執(zhí)行的內(nèi)容,會(huì)轉(zhuǎn)交到classLoader,最終會(huì)轉(zhuǎn)化為系統(tǒng)的調(diào)用,調(diào)用dlopen和dlsym函數(shù)。

  • 調(diào)用dlopen函數(shù),打開一個(gè)so文件并創(chuàng)建一個(gè)handle;
  • 調(diào)用dlsym()函數(shù),查看相應(yīng)so文件的JNI_OnLoad()函數(shù)指針,并執(zhí)行相應(yīng)函數(shù)。

簡(jiǎn)單的說,dlopen、dlsym提供一種動(dòng)態(tài)轉(zhuǎn)載庫到內(nèi)存的機(jī)制,在需要的時(shí)候,可以調(diào)用庫中的方法。

在Java字節(jié)碼中,普通的方法是直接把字節(jié)碼放到code屬性表中,而native方法,與普通的方法通過一個(gè)標(biāo)志“ACC_NATIVE”區(qū)分開來。java在執(zhí)行普通的方法調(diào)用的時(shí)候,可以通過找方法表,再找到相應(yīng)的code屬性表,最終解釋執(zhí)行代碼。

在將動(dòng)態(tài)庫load進(jìn)來的時(shí)候,首先要做的第一步就是執(zhí)行該動(dòng)態(tài)庫的JNI_OnLoad方法,我們需要在該方法中聲明好native和java的關(guān)聯(lián),系統(tǒng)中的相關(guān)類因?yàn)闆]有提供該方法,因此需要手動(dòng)調(diào)用了各自相應(yīng)的注冊(cè)方法。而在我們寫的demo中,編譯器則為我們做了這個(gè)操作,也不需要我們來做。寫好映射關(guān)系之后,調(diào)用registerNativeMethods方法來將這些方法進(jìn)行注冊(cè)。具體的函數(shù)映射和注冊(cè)方式如上Runtime所示。

在編譯成的java代碼中,普通的Java方法會(huì)直接指向方法表中具體的方法,而對(duì)于native方法則是做了特殊的標(biāo)記,在執(zhí)行到native方法時(shí),就會(huì)根據(jù)我們之前加載進(jìn)來的native的方法對(duì)應(yīng)表中去查找相應(yīng)的方法,然后執(zhí)行。

參考文章

Android JNI原理分析
Native調(diào)用Java
Java JNI實(shí)現(xiàn)原理初探

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

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

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