戳上面的蓝字关注我哦!
序言
因为在接下来的源码分析中将涉及大量的 Java 和 Native 的互相调用.当然对于我们的代码分析没有什么影响,但是,这样一个黑盒子摆在面前,对于其实现原理还是充满了好奇心.本篇将从 JNI 最基本的概念到简单的代码实例和其实现原理逐步展开.
1.JNI
JNI(Java Native Interface,Java 本地接口)是一种编程框架使得 Java 虚拟机中的 Java 程序可以调用本地应用 / 或库, 也可以被其他程序调用. 本地程序一般是用其它语言 C,C++ 或汇编语言编写的, 并且被编译为基于本机硬件和操作系统的程序.在 Android 平台,为了更方便开发者的使用和增强其功能性,Android 提供了 NDK 来更方便开发者的开发.
2. 为什么要有 JNI?
JNI 允许程序员用其他编程语言来解决用纯粹的 Java 代码不好处理的情况, 例如, Java 标准库不支持的平台相关功能或者程序库.也用于改造已存在的用其它语言写的程序, 供 Java 程序调用.许多基于 JNI 的标准库提供了很多功能给程序员使用, 例如文件 I/O,音频相关的功能.当然,也有各种高性能的程序,以及平台相关的 API 实现, 允许所有 Java 应用程序安全并且平台独立地使用这些功能.Java 层可以用来负责 UI 功能实现,而 C++ 负责进行计算操作.
JNI 框架允许 Native 方法调用 Java 对象,就像 Java 程序访问 Native 对象一样方便.Native 方法可以创建 Java 对象,读取这些对象, 并调用 Java 对象执行某些方法.当然 Native 方法也可以读取由 Java 程序自身创建的对象, 并调用这些对象的方法.
3.Hello World
这里,我们先通过一个简单的 Hello World 实例来对 JNI 的调用流程有一个直观的印象,然后针对其中的实现原理和细节做分析.
在 Java 文件中定义 native 函数
在此方法声明中,使用 native 关键字的作用是告诉虚拟机,函数位于共享库中(即在原生端实现).
private native String helloWorld();
利用 Javah 生成头文件
对于 native 方法的命名规则,函数名根据以下规则构建:
在名称前面加上 Java_.
描述与顶级源目录相关的文件路径.
使用下划线代替正斜杠.
删掉 .java 文件扩展名.
在最后一个下划线后,附加函数名.
按照这些规则,此示例使用的函数名为
Java_com_example_hellojni_HelloJni_stringFromJNI
. 此名称描述
hellojni/src/com/example/hellojni/HelloJni.java
中一个名为 stringFromJNI() 的 Java 函数.我们想通过更简单的方式,让写 native 函数如同和写 java 函数没有这一步的转化,那么可以通过 javah 来实现.
javah -d ../jni -jni com.chenjensen.myapplication.MainActivity
d :头文件输出目录
jni:生成 jni 文件
根据 Javah 生成的头文件,实现相应的 native 函数
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld (JNIEnv *, jobject);
头文件中生成了我们的 java 文件中定义的 native 方法,也做好了类型转化,我们只需要新建一个 cpp 文件来实现相应的方法即可.
cpp 文件
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld (JNIEnv *env, jobject){ char *str = "Hello world"; return (*env).NewStringUTF(str);}
build 文件中编译支持指定的平台(arm,x86 等)
ndk { moduleName "hello" abiFilters "armeabi", "armeabi-v7a", "x86" }
这里指定了生成 so 文件的 name 之后,编译系统就会从 JNI 目录下去寻找相应的 c/cpp 文件,来生成相应的 so 文件.
执行
在 Java 代码中,native 方法的执行之前,要提前加载相应的动态库,然后才可以执行,一般会在该类中通过静态代码块的方式来加载.应用启动时,调用此函数以加载 .so 文件.
static { System.loadLibrary("hello");}
这个时候,我们在 Java 代码中调用相应的 native 代码就会生效了.
那么在 C/C++ 文件中如何调用 Java 呢,这里的调用方式和 Java 中通过反射查找一个类的调用相似.核心函数为以下几个.
FindClass(), NewObject(), GetStaticMethodID(), GetMethodID(), CallStaticObjectMethod(), CallVoidMethod()
找到相应的类,相应的方法,调用相应的类和方法.这里不在给出具体的代码示例.可参考文章末尾给出的相应链接.
4. 如何调用
通过上述 6 个步骤,我们便实现了 Java 调用 native 函数,借助了相应的工具,我们可以很快的实现其互相调用,但是,工具也屏蔽掉了大量的实现细节,让这个过程变成黑盒,不了解其实现.这个过程中,当 JVM 调用这些函数,传递了一个 JNIEnv 指针,一个 jobject 的指针,任何在 Java 方法中声明的 Java 参数.
一个 JNI 函数看起来类似这样:
JNIEXPORT void JNICALL Java_ClassName_MethodName (JNIEnv *env, jobject obj){}
Java 和 C++ 之间的调用,Java 的执行需要在 JVM 上,因此在调用的时候,JVM 必须知道要调用那一个本地函数,本地函数调用 Java 的时候,也必须要知道应用对象和具体的函数.
JNI 中 C++ 和 Java 的执行是在同一个线程,但是其线程值是不相同的.JNIEnv 是 JNI 的使用环境,JNIEnv 对象是和线程绑定在一起的,在进行调用的时候,会传递一个 JavaVM 的指针作为参数,然后通过 JavaVM 的 getEnv 函数得到 JNIEnv 对象的指针.在 Java 中每次创建一个线程,都会生成新的 JNIEnv 对象.
在分析系统源码的时候,我们可以看到很多的 java 对于 native 的调用,通过对于源码的分析,我们发现在系统开机之后,就会有许多的 Service 进程被启动,这个时候,而其很多实现都是通过 native 来实现的,这个时候如何调用,让我们回归到系统的启动过程中.在 Zygote 进程中首先会调用启动 VM.
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));}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 的方法对应表中去查找相应的方法,然后执行.
来源: https://juejin.im/entry/5a72f7d4f265da4e896a9c06