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实现原理初探

相关文章
相关标签/搜索