JNI的实现原理

作者:ladder_builder https://www.jianshu.com/p/0b7976fbf81b

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

主要流程如下:

  • 如果是静态方法,首先确保所属的class已经初始化
  • 在内部静态方法表中查询
  • 在动态链接库中查询

这个方法很巧妙,第一次调用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接口注册。 动态解析只在第一次调用时进行。

本文分享自微信公众号 - 刘望舒(liuwangshuAndroid)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-09-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏jouypub

MySQL中utf8和utf8mb4的区别

MySQL在5.5.3之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。好在utf8mb4是ut...

20780
来自专栏雪胖纸的玩蛇日常

第二章 基本数据结构

21880
来自专栏灯塔大数据

干货|Kotlin入门第一课:从对比Java开始

1.介绍 今年初,甲骨文再次对谷歌所谓的安卓侵权使用Java提起诉讼,要求后者赔偿高达90亿美元。随后便传出谷歌因此计划将主力语言切换到苹果主导的Swift,不...

398110
来自专栏Java与Android技术栈

使用Kotlin高效地开发Android App(五)完结篇总结

使用 Java 来编写单例模式的话,可以写出好几种。同样,使用 Kotlin 也可以写出多种单例模式。在这里介绍的是一种使用委托属性的方式来实现单例的写法。

11120
来自专栏博客园

WebAPI返回JSON

web api写api接口时默认返回的是把你的对象序列化后以XML形式返回,那么怎样才能让其返回为json呢,下面就介绍两种方法:  方法一:(改配置法)  ...

97820
来自专栏Java技术栈

跟我学 Java 8 新特性之 Stream 流(二)关键知识点

我们的第一篇文章,主要是通过一个Demo,让大家体验了一下使用流API的那种酣畅淋漓的感觉。如果你没有实践,我还是再次呼吁你动手敲一敲,自己实实在跑一遍上一篇的...

15440
来自专栏JavaEdge

设计模式实战-迭代器模式

迭代器是为容器服务的,那什么是容器呢? 能容纳对象的所有类型都可以称之为容器,例如Collection集合类型、Set类型等,迭代器模式就是为解决遍历这些容器中...

37820
来自专栏FreeBuf

CTF小技巧:文本解密工具 Text Decoder Toolkit

欢迎来到文本解码挑战赛! T{4 G=C 9<=E B63 3<3;G /<2 9<=E G=C@A3:4^ G=C <332 <=B 43/@ B63 @3A...

323100
来自专栏Flutter入门到实战

谈谈模板方法设计模式的使用

在项目中经常会遇到一个类的某些方法和另一个类的某些方法功能是相同的,只有部分方法是不同的。这个时候就可以使用模板方法来操作了。其实这种情况很常见:比如我们项目里...

10320
来自专栏CSDN技术头条

【问底】静行:FastJSON实现详解

还记得电影《功夫》中火云邪神的一句话:天下功夫,无坚不破,唯快不破。在程序员的世界中,“快”一直是大家苦苦修炼,竞相追逐的终极目标之一,甚至到了“不择手段”、“...

33770

扫码关注云+社区

领取腾讯云代金券