JNI原理

216 阅读8分钟

JNI原理

初识:

what:

JNI(Java Native Interface)是一个编程框架,允许 Java 代码与其他语言(如 C、C++ 和汇编语言)编写的本地应用程序和库进行交互。JNI 提供了一种机制,使得 Java 程序能够调用本地方法(native methods),并且能够与本地代码进行数据交换。

image.png

why:

● 游戏

● 视频解码

● 复用C/C++函数

● 加快运行速度

how:

doc.weixin.qq.com/flowchart-a…

在Java中调用C++:

通过“hello world”写一个简单的示例:

1.  在Java文件中定义native函数:

JVM查找Native方法有两种方式:

1、按照JNI规范的命名规则进行查找,这种方式叫静态注册。

2、调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中,这种方式叫动态注册。

静态注册:

1.  在Java文件中声明native函数,使用System.loadLibrary("<C语言文件名>")。

2.  使用javac <>将编译Java文件编译成.class。

3.  使用javac -h <C/C++目的文件目录> <Java文件目录> 生成C/Cpp的头文件。

这么做是直接生成对应名字文件,不用自己命名,

对于native方法的命名规则,函数名根据以下规则构建:

● 在名称前面加上 Java_。

● 描述与顶级源目录相关的文件路径。

● 使用下划线代替正斜杠。

● 删掉 .java 文件扩展名。

● 在最后一个下划线后,附加函数名。

4.  实现头文件,并且将文件命名为前面的<C语言文件名>,android studio中提供CMake直接注册,注册之后CMake会帮忙编译, 否则使用gcc自己将c/cpp和头文件编译为.so编译为动态链接库。

5.  在Java中调用。

动态注册:

静态注册和动态注册本质区别: 静态注册在编译器,使用CMake等手段将.so动态链接库,预先注册好了

动态注册是在运行时候,在动态连接库中才回去注册本地方法

在C++中调用Java:

这里的调用方式和Java中通过反射查找一个类的调用相似。核心函数为以下几个。

FindClass(), NewObject(), GetStaticMethodID(), 
GetMethodID(), CallStaticObjectMethod(), CallVoidMethod()
//*********************************************************
非静态:
//通过jobject对象调用普通方法
(*jniEnv)->CallVoidMethod(jniEnv, mTestProvider, sayHello,jstrMSG);

//通过jclass类调用静态方法
(*jniEnv)->CallStaticObjectMethod(jniEnv, TestProvider, getTime);

Chromium中是如何处理C++和Java之间调用的?

chromium为方便JNI的开发, 写了一个关键脚本: jni_generator.py,

在编译前扫描所有的java文件, 对java文件中有@CalledByNative注解的方法和native关键字修饰的方法,

在out/release/gen/目录下生成和java文件对应的.h文件,

命名规则是: 类名_jni.h, 例如: Tab.java对应Tab_jni.h, TraceEvent.java对应TraceEvent_jni.h以Tab.java为例:

public class Tab {
    //内核获得用户输入的url
    @CalledByNative
    public String getUrl() {
        String url = getWebContents() != null && !getWebContents().isDestroyed() ? getWebContents().getUrl() : "";
        if (getContentViewCore() != null || getNativePage() != null || !TextUtils.isEmpty(url)) {
            mUrl = url;
        }

        return mUrl != null ? mUrl : "";
    }

    //保存网页的API
    private native void nativeSaveWebArchieve(long nativeTabAndroid, String filename, ValueCallback<String> callback);
}

对应Tab_jni.h中的内容:

//生成一个方法对调用Java中的方法进行封装
//本质还是: 通过JNIEnv找到method id, 然后通过CallObjectMethod() 进行调用.
static base::android::ScopedJavaLocalRef<jstring> Java_Tab_getUrl(JNIEnv* env,
    jobject obj) {
  /* Must call RegisterNativesImpl()  */
  CHECK_CLAZZ(env, obj,
      Tab_clazz(env), NULL);
  jmethodID method_id =
      base::android::MethodID::LazyGet<
      base::android::MethodID::TYPE_INSTANCE>(
      env, Tab_clazz(env),
      "getUrl",

"("
")"
"Ljava/lang/String;",
      &g_Tab_getUrl);

  jstring ret =
      static_cast<jstring>(env->CallObjectMethod(obj, //***这里实现的真正调用java中的方法***
          method_id));
  jni_generator::CheckException(env);
  return base::android::ScopedJavaLocalRef<jstring>(env, ret);
}

对java中的native方法进行实现,

方法名必须按照: 包名类名方法名, 符合签名规范:

void Java_org_chromium_chrome_browser_Tab_nativeSaveWebArchieve(JNIEnv* env,
    jobject jcaller,
    jlong nativeTabAndroid,
    jstring filename,
    jobject callback) {
  TabAndroid* native = reinterpret_cast<TabAndroid*>(nativeTabAndroid);
  CHECK_NATIVE_PTR(env, jcaller, native, "SaveWebArchieve");
  return native->SaveWebArchieve(env, jcaller, filename, callback);
}

在tab_android.cc中 #include "jni/Tab_jni.h"

#include "jni/Tab_jni.h"
GURL TabAndroid::GetURL() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  return GURL(base::android::ConvertJavaStringToUTF8(
      Java_Tab_getUrl(env, weak_java_tab_.get(env).obj())));
}


void TabAndroid::SaveWebArchieve(JNIEnv *env, jobject obj, jstring path, jobject callback) {
    ScopedJavaGlobalRef<jobject>* j_callback = new ScopedJavaGlobalRef<jobject>();
    j_callback->Reset(env, callback);
    base::FilePath target_path(ConvertJavaStringToUTF8(env, path));
    web_contents()->GenerateMHTML(
      target_path,
      base::Bind(&GenerateMHTMLCallback, base::Owned(j_callback), target_path));
}

如何调用:

通过上述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。

image.png

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的关联,系统中的相关类因为没有提供该方法,因此需要手动调用了各自相应的注册方法。编译器则为我们做了这个操作,也不需要我们来做。写好映射关系之后,调用registerNativeMethods方法来将这些方法进行注册。具体的函数映射和注册方式如上Runtime所示。

在编译成的java代码中,普通的Java方法会直接指向方法表中具体的方法,而对于native方法则是做了特殊的标记,在执行到native方法时,就会根据我们之前加载进来的native的方法对应表中去查找相应的方法,然后执行。

⬇️辅助理解:

image.png

关于.h文件: