Android JNI 原理分析

引言:分析Android源码6.0的过程,一定离不开Java与C/C++代码直接的来回跳转,那么就很有必要掌握JNI,这是链接Java层和Native层的桥梁,本文涉及相关源码:

  • frameworks/base/core/jni/AndroidRuntime.cpp
  • libcore/luni/src/main/java/java/lang/System.java
  • libcore/luni/src/main/java/java/lang/Runtime.java
  • libnativehelper/JNIHelp.cpp
  • libnativehelper/include/nativehelper/jni.h
  • frameworks/base/core/java/android/os/MessageQueue.java
  • frameworks/base/core/jni/android_os_MessageQueue.cpp
  • frameworks/base/core/java/android/os/Binder.java
  • frameworks/base/core/jni/android_util_Binder.cpp
  • frameworks/base/media/java/android/media/MediaPlayer.java
  • frameworks/base/media/jni/android_media_MediaPlayer.cpp

一、JNI概述

JNI(Java Native Interface,Java本地接口),用于打通Java层与Native(C/C++)层。这不是Android系统所独有的,而是Java所有。众所周知,Java语言是跨平台的语言,而这跨平台的背后都是依靠Java虚拟机,虚拟机采用C/C++编写,适配各个系统,通过JNI为上层Java提供各种服务,保证跨平台性。

相信不少经常使用Java的程序员,享受着其跨平台性,可能全然不知JNI的存在。在Android平台,让JNI大放异彩,为更多的程序员所熟知,往往为了提供效率或者其他功能需求,就需要NDK开发。上一篇文章Linux系统调用(syscall)原理,介绍了打通android上层与底层kernel的枢纽syscall,那么本文的目的则是介绍打通android上层中Java层与Native的纽带JNI。

二、JNI查找方式

Android系统在启动启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会AndroidRuntime.cpp中的startVm创建虚拟机,VM创建完成后,紧接着调用startReg完成虚拟机中的JNI方法注册。

2.1 startReg

[–>AndroidRuntime.cpp]

  • int AndroidRuntime::startReg(JNIEnv* env)
  • {
  • //设置线程创建方法为javaCreateThreadEtc
  • androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
  • env->PushLocalFrame(200);
  • //进程NI方法的注册
  • if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
  • env->PopLocalFrame(NULL);
  • return -1;
  • }
  • env->PopLocalFrame(NULL);
  • return 0;
  • }

register_jni_procs(gRegJNI, NELEM(gRegJNI), env)这行代码的作用就是就是循环调用gRegJNI数组成员所对应的方法。

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

gRegJNI数组,有100多个成员变量,定义在AndroidRuntime.cpp

  • static const RegJNIRec gRegJNI[] = {
  • REG_JNI(register_android_os_MessageQueue),
  • REG_JNI(register_android_os_Binder),
  • ...
  • };

该数组的每个成员都代表一个类文件的jni映射,其中REG_JNI是一个宏定义,在Zygote中介绍过,该宏的作用就是调用相应的方法。

2.2 如何查找native方法

当大家在看framework层代码时,经常会看到native方法,这是往往需要查看所对应的C++方法在哪个文件,对应哪个方法?下面从一个实例出发带大家如何查看java层方法所对应的native方法位置。

2.2.1 实例(一)

当分析Android消息机制源码,遇到MessageQueue.java中有多个native方法,比如:

  • private native void nativePollOnce(long ptr, int timeoutMillis);

步骤1:
MessageQueue.java的全限定名为android.os.MessageQueue.java,方法名:android.os.MessageQueue.nativePollOnce(),而相对应的native层方法名只是将点号替换为下划线,可得android_os_MessageQueue_nativePollOnce()
Tips: nativePollOnce ==> android_os_MessageQueue_nativePollOnce()

步骤2:
有了native方法,那么接下来需要知道该native方法所在那个文件。前面已经介绍过Android系统启动时就已经注册了大量的JNI方法,见AndroidRuntime.cpp的gRegJNI数组。这些注册方法命令方式:

  • register_[包名]_[类名]

那么MessageQueue.java所定义的jni注册方法名应该是register_android_os_MessageQueue,的确存在于gRegJNI数组,说明这次JNI注册过程是有开机过程完成的。 该方法在AndroidRuntime.cpp申明为extern方法:

  • extern int register_android_os_MessageQueue(JNIEnv* env);

这些extern方法绝大多数位于/framework/base/core/jni/目录,大多数情况下native文件命名方式:

  • [包名]_[类名].cpp
  • [包名]_[类名].h

Tips: MessageQueue.java ==> android_os_MessageQueue.cpp

打开android_os_MessageQueue.cpp文件,搜索android_os_MessageQueue_nativePollOnce方法,这便找到了目标方法:

  • static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
  • jlong ptr, jint timeoutMillis) {
  • NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
  • nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
  • }

到这里完成了一次从Java层方法搜索到所对应的C++方法的过程。

2.2.2 实例(二)

对于native文件命名方式,有时并非[包名]_[类名].cpp,比如Binder.java

Binder.java所对应的native文件:android_util_Binder.cpp

  • public static final native int getCallingPid();

根据实例(一)方式,找到getCallingPid ==> android_os_Binder_getCallingPid(),并且在AndroidRuntime.cpp中的gRegJNI数组中找到register_android_os_Binder

按实例(一)方式则native文名应该为android_os_Binder.cpp,可是在/framework/base/core/jni/目录下找不到该文件,这是例外的情况。其实真正的文件名为android_util_Binder.cpp,这就是例外,这一点有些费劲,不明白为何google要如此打破规律的命名。

  • static jint android_os_Binder_getCallingPid(JNIEnv* env, jobject clazz)
  • {
  • return IPCThreadState::self()->getCallingPid();
  • }

有人可能好奇,既然如何遇到打破常规的文件命令,怎么办?这个并不难,首先,可以尝试在/framework/base/core/jni/中搜索,对于binder.java,可以直接搜索binder关键字,其他也类似。如果这里也找不到,可以通过grep全局搜索android_os_Binder_getCallingPid这个方法在哪个文件。

jni存在的常见目录:

  • /framework/base/core/jni/
  • /framework/base/services/core/jni/
  • /framework/base/media/jni/

2.2.3 实例(三)

前面两种都是在Android系统启动之初,便已经注册过JNI所对应的方法。 那么如果程序自己定义的jni方法,该如何查看jni方法所在位置呢?下面以MediaPlayer.java为例,其包名为android.media:

  • public class MediaPlayer{
  • static {
  • System.loadLibrary("media_jni");
  • native_init();
  • }
  • private static native final void native_init();
  • ...
  • }

通过static静态代码块中System.loadLibrary方法来加载动态库,库名为media_jni, Android平台则会自动扩展成所对应的libmedia_jni.so库。 接着通过关键字native加在native_init方法之前,便可以在java层直接使用native层方法。

接下来便要查看libmedia_jni.so库定义所在文件,一般都是通过Android.mk文件定义LOCAL_MODULE:= libmedia_jni,可以采用grep或者mgrep来搜索包含libmedia_jni字段的Android.mk所在路径。

搜索可知,libmedia_jni.so位于/frameworks/base/media/jni/Android.mk。用前面实例(一)中的知识来查看相应的文件和方法名分别为:

  • android_media_MediaPlayer.cpp
  • android_media_MediaPlayer_native_init()

再然后,你会发现果然在该Android.mk所在目录/frameworks/base/media/jni/中找到android_media_MediaPlayer.cpp文件,并在文件中存在相应的方法:

  • static void
  • android_media_MediaPlayer_native_init(JNIEnv *env)
  • {
  • jclass clazz;
  • clazz = env->FindClass("android/media/MediaPlayer");
  • fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
  • ...
  • }

Tips:MediaPlayer.java中的native_init方法所对应的native方法位于/frameworks/base/media/jni/目录下的android_media_MediaPlayer.cpp文件中的android_media_MediaPlayer_native_init方法。

2.3 小结

JNI作为连接Java世界和C/C++世界的桥梁,很有必要掌握。看完本文,至少能掌握在分析Android源码过程中如何查找native方法。首先要明白native方法名和文件名的命名规律,其次要懂得该如何去搜索代码。 JNI方式注册无非是Android系统启动过程中Zygote注册以及通过System.loadLibrary方式注册,对于系统启动过程注册的,可以通过查询AndroidRuntime.cpp中的gRegJNI是否存在对应的register方法,如果不存在,则大多数情况下是通过LoadLibrary方式来注册。

三、 JNI原理分析

再进一步来分析,Java层与native层方法是如何注册并映射的,继续以MediaPlayer为例。

在文件MediaPlayer.java中调用System.loadLibrary("media_jni")把libmedia_jni.so动态库加载到内存。接下来,以loadLibrary为起点展开JNI注册流程的过程分析。

3.1 loadLibrary

[System.java]
```java
public static void loadLibrary(String libName) {
//接下来调用Runtime方法
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

[Runtime.java]

  • void loadLibrary(String libraryName, ClassLoader loader) {
  • //loader不会空,则进入该分支
  • if (loader != null) {
  • //查找库所在路径
  • String filename = loader.findLibrary(libraryName);
  • if (filename == null) {
  • throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
  • System.mapLibraryName(libraryName) + "\"");
  • }
  • //加载库
  • String error = doLoad(filename, loader);
  • if (error != null) {
  • throw new UnsatisfiedLinkError(error);
  • }
  • return;
  • }
  • //loader为空,则会进入该分支
  • String filename = System.mapLibraryName(libraryName);
  • List<String> candidates = new ArrayList<String>();
  • String lastError = null;
  • for (String directory : mLibPaths) {
  • String candidate = directory + filename;
top Created with Sketch.