【源碼解讀】JNI的實現原理

JNI是Java Native Interface的縮寫,它為java提供了調用C和C++代碼的能力。java.lang包下的很多類都用到了native方法,比如Class、String、Thread、System,可以說JNI是java語言的基礎。了解了JNI的實現原理,可以讓我們對java虛擬機有更深的認識。本文主要從源碼的角度,分析java虛擬機是如何實現對JNI的支持的。

1. native方法的調用

首先來看一下虛擬機是如何調用一個native方法的。

//dalvik2/vm/interp/Stack.cpp
Object* dvmInvokeMethod(Object* obj, const Method* method,
    ArrayObject* argList, ArrayObject* params, ClassObject* returnType,
    bool noAccessCheck)
{
 //省略次要流程 ...
  if (dvmIsNativeMethod(method)) {
        TRACE_METHOD_ENTER(self, method);
        (*method->nativeFunc)((u4*)self->interpSave.curFrame, &retval,
                              method, self);
        TRACE_METHOD_EXIT(self, method);
    } else {
        dvmInterpret(self, method, &retval);
    }

上面的代碼就是dalvik虛擬機調用一個方法的實現,它發現如果這是一個native方法,則使用Method對象中的nativeFunc函數指針對象調用。那么nativeFunc函數指針指向哪個函數呢?

2. nativeFunc函數指針的賦值

nativeFunc主要有兩種情況

  • 默認情況下,nativeFunc會指向一個解析方法;
  • 也可以通過JNIEnv提供的注冊方法,手動建立native方法與實現方法的關系,此時nativeFunc就指向了實現方法。

2.1 本地方法解析resolveNativeMethod

// dalvik2/vm/oo/Class.cpp
findClassNoInit
  loadClassFromDex
     loadClassFromDex0
      loadMethodFromDex

static void loadMethodFromDex(ClassObject* clazz, const DexMethod* pDexMethod, Method* meth) {
  ...
  if (dvmIsNativeMethod(meth)) {
    meth->nativeFunc = dvmResolveNativeMethod;
    meth->jniArgInfo = computeJniArgInfo(&meth->prototype);
  }
}

第一次從dex中加載類時,會加載類中的所有方法,對于native方法,會將nativeFunc賦值為dvmResolveNativeMethod,dvmResolveNativeMethod方法如下:

// dalvik2/vm/Native.cpp
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
    const Method* method, Thread* self) {
  ClassObject* clazz = method->clazz;

   //對于靜態方法,要確保所屬的class已經初始化
  if (dvmIsStaticMethod(method)) {
    if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
      assert(dvmCheckException(dvmThreadSelf()));
      return;
    }
  } else {
    assert(dvmIsClassInitialized(clazz) ||
      dvmIsClassInitializing(clazz));
  }
  //在內部本地方法表中查詢
  DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
  if (infunc != NULL) {
    DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
    dvmSetNativeFunc((Method*) method, dfunc, NULL);
    dfunc(args, pResult, method, self);
    return;
  }
  //在動態鏈接庫表中查詢
  void* func = lookupSharedLibMethod(method);
  if (func != NULL) {
    /* found it, point it at the JNI bridge and then call it */
    dvmUseJNIBridge((Method*) method, func);
    (*method->nativeFunc)(args, pResult, method, self);
    return;
  }
}

主要流程如下:

  1. 如果是靜態方法,首先確保所屬的class已經初始化
  2. 在內部靜態方法表中查詢
  3. 在動態鏈接庫中查詢

這個方法很巧妙,第一次調用native方法時,由于不知道其具體實現,所以就用resolveNativeMethod去查找,等找到后,就將nativeFunc替換為找到的函數,這樣下次再用的時候就省去了查找的過程。

2.1.1 內部本地方法的查詢

內部本地方法表是一個集合,這個集合里包含了java語言和虛擬機本身用到的native方法,如下所示:

// dalvik2/vm/native/InternalNative.cpp
static DalvikNativeClass gDvmNativeMethodSet[] = {
    { "Ljava/lang/Object;",               dvm_java_lang_Object, 0 },
    { "Ljava/lang/Class;",                dvm_java_lang_Class, 0 },
    ...
    { "Ljava/lang/String;",               dvm_java_lang_String, 0 },
    { "Ljava/lang/System;",               dvm_java_lang_System, 0 },
    { "Ljava/lang/Throwable;",            dvm_java_lang_Throwable, 0 },
    { "Ljava/lang/VMClassLoader;",        dvm_java_lang_VMClassLoader, 0 },
    { "Ljava/lang/VMThread;",             dvm_java_lang_VMThread, 0 },
    { "Ldalvik/system/DexFile;",          dvm_dalvik_system_DexFile, 0 },
    { "Ldalvik/system/VMRuntime;",        dvm_dalvik_system_VMRuntime, 0 },
    { "Ldalvik/system/Zygote;",           dvm_dalvik_system_Zygote, 0 },
    { "Ldalvik/system/VMStack;",          dvm_dalvik_system_VMStack, 0 },
    ...
    { NULL, NULL, 0 },
};

DalvikNativeClass結構的定義如下:

// dalvik2/vm/Native.h
struct DalvikNativeClass {
    const char* classDescriptor; //類描述符
    const DalvikNativeMethod* methodInfo; //native方法數組
    u4          classDescriptorHash;   // 類描述符的hash值
};

類描述符的hash值,會在虛擬機啟動的時候進行計算,使用hash值的目的,是為了加快查找過程。
methodInfo是一個以NULL結尾的本地方法數組。DalvikNativeMethod定義如下:

struct DalvikNativeMethod {
    const char* name; //方法名
    const char* signature; // 方法簽名
    DalvikNativeFunc  fnPtr; //native函數
};

上面的dvm_java_lang_String就是String類中用到的所有native方法的數組,具體如下:

// dalvik2/vm/native/String.cpp
const DalvikNativeMethod dvm_java_lang_String[] = {
    { "charAt",      "(I)C",                  String_charAt },
    { "compareTo",   "(Ljava/lang/String;)I", String_compareTo },
    { "equals",      "(Ljava/lang/Object;)Z", String_equals },
    { "fastIndexOf", "(II)I",                 String_fastIndexOf },
    { "intern",      "()Ljava/lang/String;",  String_intern },
    { "isEmpty",     "()Z",                   String_isEmpty },
    { "length",      "()I",                   String_length },
    { NULL, NULL, NULL },
};

2.1.2 動態鏈接庫的查詢

如果沒有在內部本地方法表中找到對應的native方法,則要繼續在動態鏈接庫中查找。這個動態鏈接庫就是通過System.loadLibrary加載的so,虛擬機會將所有的so存入一個表中,下面看看具體的代碼。

// dalvik2/vm/Native.cpp
static void* lookupSharedLibMethod(const Method* method)
{
    return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib,
        (void*) method);
}

gDvm.nativeLibs是一個HashTable,其中存的是動態鏈接庫的信息。其中的數據是通過System.loadLibrary添加進去的,java層的調用關系如下:

System.loadLibrary
  Runtime.loadLibrary0
    Runtime.doLoad()
      Rumtime.nativeLoad(name, loader, librarySearchPath)

Runtime.nativeLoad是個native方法,對應的native方法如下:

// dalvik2/vm/native/java_lang_Runtime.cpp
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
    JValue* pResult) {
    StringObject* fileNameObj = (StringObject*) args[0];
    Object* classLoader = (Object*) args[1];
    StringObject* ldLibraryPathObj = (StringObject*) args[2];
    ...
    bool success = dvmLoadNativeCode(fileName, classLoader, &reason);
}

// dalvik2/vm/Native.cpp
bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
        char** detail) {
  SharedLib* pEntry;
  void* handle;
  bool verbose;
  ...
  //先檢查是否已經加載過
  pEntry = findSharedLibEntry(pathName);
  if (pEntry != NULL) {
    // 檢查classLoader是否相同,如果不同會報錯,同一個so只能由一個classLoader加載
    if (pEntry->classLoader != classLoader) {
           ALOGW("Shared lib '%s' already opened by CL %p; can't open in %p",
                pathName, pEntry->classLoader, classLoader);
          return false;
    }
    if (verbose) {
         ALOGD("Shared lib '%s' already loaded in same CL %p",
                pathName, classLoader);
    }
    if (!checkOnLoadResult(pEntry))
        return false;
    return true;
  }
  // 打開so文件
  handle = dlopen(pathName, RTLD_LAZY);
  // 創建一個新的ShareLib對象
  SharedLib* pNewEntry;
  pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
  pNewEntry->pathName = strdup(pathName);
  pNewEntry->handle = handle;
  pNewEntry->classLoader = classLoader;
  // 將新的ShareLib加到hash表中
  SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
  if (pNewEntry != pActualEntry) {
        ALOGI("WOW: we lost a race to add a shared lib (%s CL=%p)",
            pathName, classLoader);
      freeSharedLibEntry(pNewEntry);
      return checkOnLoadResult(pActualEntry);
   } else {
      vonLoad = dlsym(handle, "JNI_OnLoad");
      if (vonLoad == NULL) {
            ALOGD("No JNI_OnLoad found in %s %p, skipping init",
                pathName, classLoader);
      } else {
          OnLoadFunc func = (OnLoadFunc)vonLoad;
          Object* prevOverride = self->classLoaderOverride;
          self->classLoaderOverride = classLoader;
          //調用JNI_OnLoad方法
          version = (*func)(gDvmJni.jniVm, NULL);
      }
    }
  }
}

上面的主要流程就是

  1. 檢查是否已經加載過該so,如果是還需要判斷加載so的classLoader是否相同,不同的話也會報錯,
  2. 如果沒有加載過,則使用dlopen打開so,然后創建一個ShareLib加入到gDvm.nativeLibs哈希表中。
  3. 如果so中有JNI_OnLoad方法,則執行該方法。我們可以在該方法中做一些初始化工作,還可以手動建立java類中的native方法和so中的native方法的對應關系。

findMethodInLib的主要流程是,首先根據規則,生成對應的native方法名,然后使用dlsym查找so中是否有該方法。
java層的native方法和so中的對應關系為:Java_類全名_方法名。

2.2 手動注冊native方法

上面提到System.loadLibrary加載so時,會執行其中的JNI_OnLoad方法,我們可以在該方法中手動注冊native方法。

static jint RegisterNatives(JNIEnv* env, jclass jclazz,
    const JNINativeMethod* methods, jint nMethods)
{
    ScopedJniThreadState ts(env);

    ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);

    if (gDvm.verboseJni) {
        ALOGI("[Registering JNI native methods for class %s]",
            clazz->descriptor);
    }

    for (int i = 0; i < nMethods; i++) {
        if (!dvmRegisterJNIMethod(clazz, methods[i].name,
                methods[i].signature, methods[i].fnPtr))
        {
            return JNI_ERR;
        }
    }
    return JNI_OK;
}

針對每個JNINativeMethod,調用dvmRegiseterJNIMethod,該方法會修改Method對象的nativeFunc指針,從而建立和native實現方法的對應關系。

總結

本文分析了native方法的注冊與調用流程。
native方法既可以手動注冊,也可以按規則動態解析。
手動注冊,需要在JNI_OnLoad中使用JNIEnv提供的RegisterNatives接口注冊。
動態解析只在第一次調用時進行。

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

推薦閱讀更多精彩內容