Android系統源碼分析-JNI

序言

由於在接下來的源碼分析中將涉及大量的Java和Native的互相調用。固然對於咱們的代碼分析沒有什麼影響,可是,這樣一個黑盒子擺在面前,對於其實現原理仍是充滿了好奇心。本篇將從JNI最基本的概念到簡單的代碼實例和其實現原理逐步展開。java

JNI

JNI(Java Native Interface,Java本地接口)是一種編程框架使得Java虛擬機中的Java程序能夠調用本地應用/或庫,也能夠被其餘程序調用。 本地程序通常是用其它語言C,C++或彙編語言編寫的, 而且被編譯爲基於本機硬件和操做系統的程序。在Android平臺,爲了更方便開發者的使用和加強其功能性,Android提供了NDK來更方便開發者的開發。android

JNI工做

爲何要有JNI?

JNI容許程序員用其餘編程語言來解決用純粹的Java代碼很差處理的狀況, 例如, Java標準庫不支持的平臺相關功能或者程序庫。也用於改造已存在的用其它語言寫的程序, 供Java程序調用。許多基於JNI的標準庫提供了不少功能給程序員使用, 例如文件I/O、音頻相關的功能。固然,也有各類高性能的程序,以及平臺相關的API實現, 容許全部Java應用程序安全而且平臺獨立地使用這些功能。Java層能夠用來負責UI功能實現,而C++負責進行計算操做。git

JNI框架容許Native方法調用Java對象,就像Java程序訪問Native對象同樣方便。Native方法能夠建立Java對象,讀取這些對象, 並調用Java對象執行某些方法。固然Native方法也能夠讀取由Java程序自身建立的對象,並調用這些對象的方法。程序員

Hello World

這裏,咱們先經過一個簡單的Hello World實例來對JNI的調用流程有一個直觀的印象,而後針對其中的實現原理和細節作分析。編程

1. 在Java文件中定義native函數

在此方法聲明中,使用 native 關鍵字的做用是告訴虛擬機,函數位於共享庫中(即在原生端實現)。數組

private native String helloWorld();
複製代碼

2.利用Javah生成頭文件

對於native方法的命名規則,函數名根據如下規則構建:安全

  • 在名稱前面加上 Java_。
  • 描述與頂級源目錄相關的文件路徑。
  • 使用下劃線代替正斜槓。
  • 刪掉 .java 文件擴展名。
  • 在最後一個下劃線後,附加函數名。

按照這些規則,此示例使用的函數名爲 Java_com_example_hellojni_HelloJni_stringFromJNI。 此名稱描述 hellojni/src/com/example/hellojni/HelloJni.java 中一個名爲 stringFromJNI()的 Java 函數。咱們想經過更簡單的方式,讓寫native函數如同和寫java函數沒有這一步的轉化,那麼能夠經過javah來實現。bash

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

3.根據Javah生成的頭文件,實現相應的native函數

JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
  (JNIEnv *, jobject);
複製代碼

頭文件中生成了咱們的java文件中定義的native方法,也作好了類型轉化,咱們只須要新建一個cpp文件來實現相應的方法便可。app

4.cpp文件

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

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

ndk {
     moduleName "hello"       //生成的so文件名字,調用C程序的代碼中會用到該名字
     abiFilters "armeabi", "armeabi-v7a", "x86" //輸出指定三種平臺下的so庫
}
複製代碼

這裏指定了生成so文件的name以後,編譯系統就會從JNI目錄下去尋找相應的c/cpp文件,來生成相應的so文件。框架

6.執行

在Java代碼中,native方法的執行以前,要提早加載相應的動態庫,而後才能夠執行,通常會在該類中經過靜態代碼塊的方式來加載。應用啓動時,調用此函數以加載 .so 文件。

static {
   System.loadLibrary("hello");
}
複製代碼

這個時候,咱們在Java代碼中調用相應的native代碼就會生效了。

那麼在C/C++文件中如何調用Java呢,這裏的調用方式和Java中經過反射查找一個類的調用類似。核心函數爲如下幾個。

FindClass(), NewObject(), GetStaticMethodID(), 
GetMethodID(), CallStaticObjectMethod(), CallVoidMethod()
複製代碼

找到相應的類,相應的方法,調用相應的類和方法。這裏不在給出具體的代碼示例。可參考文章末尾給出的相應連接。

如何調用

經過上述6個步驟,咱們便實現了Java調用native函數,藉助了相應的工具,咱們能夠很快的實現其互相調用,可是,工具也屏蔽掉了大量的實現細節,讓這個過程變成黑盒,不瞭解其實現。這個過程當中, 當JVM調用這些函數,傳遞了一個JNIEnv指針,一個jobject的指針,任何在Java方法中聲明的Java參數。

一個JNI函數看起來相似這樣:

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

Java和C++之間的調用,Java的執行須要在JVM上,所以在調用的時候,JVM必須知道要調用那一個本地函數,本地函數調用Java的時候,也必需要知道應用對象和具體的函數。

JNI中C++和Java的執行是在同一個線程,可是其線程值是不相同的。 JNIEnv是JNI的使用環境,JNIEnv對象是和線程綁定在一塊兒的,在進行調用的時候,會傳遞一個JavaVM的指針做爲參數,而後經過JavaVM的getEnv函數獲得JNIEnv對象的指針。在Java中每次建立一個線程,都會生成新的JNIEnv對象。

在分析系統源碼的時候,咱們能夠看到不少的java對於native的調用,經過對於源碼的分析,咱們發如今系統開機以後,就會有許多的Service進程被啓動,這個時候,而其不少實現都是經過native來實現的,這個時候如何調用,讓咱們迴歸到系統的啓動過程當中。在Zygote進程中首先會調用啓動VM。

系統啓動JNI註冊流程

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數組, 該數組有100多個成員。其中每一項成員都是經過REG_JNI宏定義。

#define REG_JNI(name) { name }
複製代碼
struct RegJNIRec {
        int (*mProc)(JNIEnv*);
 };
複製代碼

調用mProc,就等價於調用其參數名所指向的函數。 例如REG_JNI(register_com_android_internal_os_RuntimeInit).mProc也就是指進入register_com_android_internal_os_RuntimeInit方法,進入這些方法以後,就會是對於該類中的一些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層的方法的一一映射關係
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 },
};
複製代碼

至此就完成了對於native方法和Java方法的映射關聯。

  • 另外一種加載方式

對於JNI方法的註冊無非是經過兩種方式一個是上述啓動過程當中的註冊,一個是在程序中經過System.loadLibrary的方式進行註冊,這裏,咱們以System.loadLibrary來分析其註冊過程。

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);
}
複製代碼

通過層層調用以後來到了nativeLoad方法,這裏對於這段代碼的分析,目的是爲了瞭解,整個JNI的註冊過程和調用的時候,JVM是如何找到相應的native方法的。

對於nativeLoad執行的內容,會轉交到classLoader,最終會轉化爲系統的調用,調用dlopen和dlsym函數。

  • 調用dlopen函數,打開一個so文件並建立一個handle;
  • 調用dlsym()函數,查看相應so文件的JNI_OnLoad()函數指針,並執行相應函數。

簡單的說,dlopen、dlsym提供一種動態轉載庫到內存的機制,在須要的時候,能夠調用庫中的方法。

在Java字節碼中,普通的方法是直接把字節碼放到code屬性表中,而native方法,與普通的方法經過一個標誌「ACC_NATIVE」區分開來。java在執行普通的方法調用的時候,能夠經過找方法表,再找到相應的code屬性表,最終解釋執行代碼。

在將動態庫load進來的時候,首先要作的第一步就是執行該動態庫的JNI_OnLoad方法,咱們須要在該方法中聲明好native和java的關聯,系統中的相關類由於沒有提供該方法,所以須要手動調用了各自相應的註冊方法。而在咱們寫的demo中,編譯器則爲咱們作了這個操做,也不須要咱們來作。寫好映射關係以後,調用registerNativeMethods方法來將這些方法進行註冊。具體的函數映射和註冊方式如上Runtime所示。

在編譯成的java代碼中,普通的Java方法會直接指向方法表中具體的方法,而對於native方法則是作了特殊的標記,在執行到native方法時,就會根據咱們以前加載進來的native的方法對應表中去查找相應的方法,而後執行。

參考文章

Android JNI原理分析 Native調用Java Java JNI實現原理初探

相關文章
相關標籤/搜索