JNI层交互原理

Android_java基础

类、对象、实例

  • 类(Class) :模板。
    • 例如,java.lang.String是一个类,它定义了字符串的基本操作。
  • 对象(Object) :实例化的类。
    • 例如,通过new String()创建的字符串就是对象。
  • 实例(Instance) :实例是对象的具体表现形式,通常指某个特定的对象。
    • 例如,new String("Hello")创建了一个具体的字符串实例

jobject

jobject传递的意义(重要)

jobject

  • 在JNI中,jobject用于表示运行时的具体实例,例如调用实例方法时需要传递一个jobject参数。
// 通过类名找到jclass,也就是类(模板)的引用
jclass cls = (*env)->FindClass(env, "com/example/MyClass"); 

// 通过类引用和构造方法找到jmethodID
jmethodID constructor_mid = (*env)->GetMethodID(env, cls, "<init>", "()V"); 

// 通过类模板和方法ID创建一个新的对象
jobject obj = (*env)->NewObject(env, cls, constructor_mid); 

// 然后就可以通过这个jobject调用类中的实例方法了

注意,上面的方法NewObject()是通过找到类和方法,在native层新建一个对象,从而调用新建对象的类的实例方法的,而不是通过 JNI索引到 Java层已存在的某个特定实例。

如果需要操作 Java层已有的某个特定实例(而非新建),必须通过其他方式(如 JNI方法参数或全局变量)将该实例的引用传递到 Native代码,如:
通过 JNI 方法参数传递已有对象

public class MyClass {
    public native void modifyExistingObject(MyClass existingObj); // 传入已有对象

    static {
        System.loadLibrary("mynative");
    }

    public static void main(String[] args) {
        MyClass obj = new MyClass(); // 已有对象
        new MyClass().modifyExistingObject(obj); // 传入 Native 方法
    }
}
#include <jni.h>

JNIEXPORT void JNICALL Java_MyClass_modifyExistingObject(
    JNIEnv *env,
    jobject thisObj,
    jobject existingObj // 接收 Java 传入的已有对象
) {
    // 1. 获取类信息
    jclass cls = env->GetObjectClass(existingObj);

    // 2. 获取字段 ID
    jfieldID fid = env->GetFieldID(cls, "someField", "I");

    // 3. 修改字段值
    env->SetIntField(existingObj, fid, 100);

    // 4. 调用实例方法(可选)
    jmethodID mid = env->GetMethodID(cls, "someMethod", "()V");
    env->CallVoidMethod(existingObj, mid);
}

还有其他方法,如:通过全局变量长期持有 Java 对象的引用、通过 Weak Global Reference(弱全局引用),这里不做介绍。

jobject继承

一个 jobject 变量本身不包含所有子类,而是通过继承关系可以转型为任何子类
每个 jobject 本质上是一个指针(typedef jobject *jobject)这个指针同一时间只能指向一个具体的子类(要么是jclass 要么是jstring 或者是其他)
例如:

jobject ob=env->NewStringUTF(""); // 此时是 jstring*
jobject ob=env->FindClass("java/lang/Object"); // 此时是 jclass*

assets/JAVA基础/file-20250506153620975.png
jobject包含关系图2

jclass

jclass

  • jclass表示Java中的java.lang.Class实例,它是一个类的引用。在JNI中,jclass用于表示类的类型信息,例如获取类的字段描述符或方法ID等操作。
  • 例如,GetFieldID函数需要一个jclass参数来获取特定字段的描述符。
  • 在实际应用中,jclass通常用于静态方法调用,因为静态方法属于类本身,而不是某个特定的对象

jstring

jstring

  • jstring表示Java中的java.lang.String实例,即字符串对象。它是一种特殊的jobject,专门用于表示字符串。
  • 在JNI中,jstring常用于传递字符串参数或从本地代码返回字符串结果。例如,JNI函数NewStringUTF可以将C语言字符串转换为jstring

总结

  • 在JNI中,jobject用于表示对象或实例的引用,而jclass用于表示类的引用。例如调用实例方法时使用jobject,而调用静态方法时使用jclass

  • jstring作为特殊的jobject,专门用于表示字符串对象。

  • 类、对象和实例之间的关系可以通过以下方式理解:

    • 类是模板,对象是类的实例。
    • 在JNI中,通过类的引用(jclass)可以获取对象的实例(jobject),并通过对象实例调用其方法

JNI层交互原理

JNIEnv

JNI 原理 - 简书
安卓逆向基础知识之JNI开发
jdk/src/java.base/share/native/include/jni.h at master · openjdk/jdk · GitHub
类与对象
jni.h代码和网上看到的讲解不太一样,说法换了下:

代码中为 JNINativeInterface_ ,  讲解则为:JNINativeInterface
代码中为 JavaVM_ ,  讲解则为:JavaVM

代码中为 JNIEnv_ ,  讲解则为:_JNIEnv

代码中为 JNIEnv ,  讲解则为:JNIEnv 。这个没变

然后就是typedef 科普下

typedef 别名 原型;

如C 语言在 C99 之前并未提供布尔类型,但我们可以使用 typedef 关键字来定义一个简单的布尔类型
typedef int BOOL;

先来看看JNIEnv是什么,可以看到下面代码中,JNIEnv是一个结构体,里面包含了一个JNINativeInterface指针及诸多JNI函数

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif

// 如果是cpp,则定义一个别名      `JNIEnv_`      ,该别名为`JNIEnv`类型
// 如果是c  ,则定义一个别名 `JNINativeInterface`,该别名为`JNIEnv`类型
struct JNIEnv_ { 
    // 由于上面的别名,所以在cpp中定义 `JNIEnv_` 实质上就是定义 JNIEnv
    const struct JNINativeInterface_ *functions;
    // 
    // 下面都是将 JNINativeInterface 指向的函数 封装进来方便调用
    // 例如说原本是 JNIEnv_->functions->GetVersion
    // 现在就只需要 JNIEnv_->GetVersion
    // 起到 传递指针 的作用
    // 
    // JNIEnv_结构体内存着的也就可以简单看做:
    // 存着一个方法指针和定义了一堆简化方法指针的函数
    // 
    
#ifdef __cplusplus
    jint GetVersion() {
        return functions->GetVersion(this);
    }
    ...// 其他方法这里省略
 }
struct JNINativeInterface_ {
    // 同理,JNIEnv 别名为 JNINativeInterface_
    void *reserved0;
    void *reserved1;
    void *reserved2;
    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);
    jclass (JNICALL *DefineClass)(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len);
    jclass (JNICALL *FindClass)(JNIEnv *env, const char *name);
    // 省略其他函数指针(如NewStringUTF、GetFieldID等)
};

那么我们就可以很明显看出区别了:

cpp中的JNIEnv_存在一个JNINativeInterface的结构体指针,然后就是方法的封装(详情看上面代码注释)。这使得cpp可直接调用成员方法,如 env->FindClass(...)

JNIEnv *env -> struct _JNIEnv -> struct JNINativeInterface* -> jni.h文件中使用C语言结构体定义的三百多个C语言函数

但是c中则是可以直接通过别名调用到JNINatvieInterface的方法,这使得c需通过指针间接调用函数,如 (*env)->FindClass(env, ...)

JNIEnv *env -> struct JNINativeInterface* -> jni.h文件中使用C语言结构体定义的三百多个C语言函数

那么JNIEnv的一些特性也在这里介绍下:

  • 首先它保存了函数指针表上面已经体现了。这里面的函数能够直接操作 Java 对象(比如调用方法、获取字段值),
    例如:
// C++ 实现
JNIEXPORT void JNICALL Java_Demo_nativeMethod(JNIEnv* env, jobject obj) {
    // 1. 用 JNIEnv 获取 String 类的 jclass
    jclass stringClass = env->FindClass("java/lang/String");
    
    // 2. 调用 String 的静态方法
    jmethodID hashCodeMethod = env->GetStaticMethodID(stringClass, "hashCode", "()I");
    int hash = env->CallStaticIntMethod(stringClass, hashCodeMethod);
}
  • 每个线程拥有 独立 的 JNIEnv 实例,且不可跨线程共享。不同线程的函数表调用需隔离。若将线程 A 的 JNIEnv 传递给线程 B 使用,会导致未定义行为(如崩溃或数据错误)
  • 主线程:由 JVM 自动创建并传递给 Native 方法。
  • 原生线程:需通过 JavaVM->AttachCurrentThread() 显式创建,并通过 DetachCurrentThread() 释放

JavaVM

老规矩我们来看源代码
别名定义这里c和cpp的区别就省略了,和上面的JNIEnv一样理解就行。

struct JNIInvokeInterface_;

struct JavaVM_;

#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const struct JNIInvokeInterface_ *JavaVM;
#endif
struct JavaVM_ {
    const struct JNIInvokeInterface_ *functions;
    
#ifdef __cplusplus
    jint DestroyJavaVM() { 
    // 卸载Java虚拟机并释放资源。
        return functions->DestroyJavaVM(this);
    }
    jint AttachCurrentThread(void **penv, void *args) { 
    // 将当前本地线程附加到JVM,使其能够访问JNI环境(`JNIEnv`)
        return functions->AttachCurrentThread(this, penv, args);
    }
    jint DetachCurrentThread() {
    // 将当前线程从JVM分离,释放其占用的资源(如本地引用表)。
        return functions->DetachCurrentThread(this);
    }

    jint GetEnv(void **penv, jint version) {
    // 获取当前线程的`JNIEnv`指针,并检查线程是否已附加到JVM。
        return functions->GetEnv(this, penv, version);
    }
    jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
    // 类似`AttachCurrentThread`,但附加的线程标记为守护线程。
        return functions->AttachCurrentThreadAsDaemon(this, penv, args);
    }
#endif
};

可以发现,JavaVM里面也是存储了一个方法指针JNIInvokeInterface,然后就是方法指针的封装。这些方法主要关于管理JVM生命周期和线程交互。

相关引用的地方:

JavaVM* g_vm = nullptr;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_vm = vm; // 保存到全局变量
    ...
}

JVM在加载动态库时,会将一个JavaVM*类型的指针作为JNI_OnLoad函数的第一个参数传入。这个指针表示当前进程中的Java虚拟机实例,是JVM与本地代码交互的入口

通过JavaVM*,本地线程可以调用AttachCurrentThread()与JVM关联,获取JNIEnv*指针,从而访问JNI接口。例如:

// g_vm 在上面的代码块中进行了初始化和赋值,保存着 JavaVM 指针
JNIEnv* env; 
(*g_vm)->AttachCurrentThread(g_vm, &env, nullptr) // 传入g_vm,在该函数里会对env进行赋值,获取JNIEnv*

JNI方法传参

了解了JNIEnv之后,我们再来研究这个结构体里面如FindClassGetObjectClass等JNI方法

一般来说,这些方法第一个传参都是JNIEnv,表示传入一个指向JNI 函数表的指针提供与 Java 交互的接口,使调用的方法能够调用 Java 方法(如 CallObjectMethod)、操作 Java 对象(如创建 jobject、获取字段值)、管理本地引用(如 NewLocalRefDeleteLocalRef)等操作

在传参中,我们还经常看到其他传入类型
jclass类型,用于表示一个Java类在JNI环境中的引用。可通过 FindClass 或 GetObjectClass 等函数获取 jclass,如

  • jclass 对应 Java 中的 Class<T> 对象(如 String.class)。
jclass clazz = env->FindClass("com/example/as_jni_project/MainActivity");

jobject类型,用于表示 Java 对象在本地代码中的引用。在 JNI 中,所有 Java 对象(包括数组、类实例等)的本地表示均为 jobject
例如:

  • jobject 对应 Java 中的对象实例(如 new String()

同理还有jstring,jfieldID,···

jfieldID strField = env->GetFieldID(clazz, "str", "Ljava/lang/String;");

下一步结合代码块来学习:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_as_1jni_1project_MainActivity_staticFromC(JNIEnv *env, jobject thiz) {
    // TODO: implement staticFromC()
    jclass clazz = env->GetObjectClass(thiz); // 第二种获取Java类的Class对象的方式,接受一个 Java对象 返回它的 class类
    jfieldID staticField = env->GetStaticFieldID(clazz, "static_str", "Ljava/lang/String;"); // 获取当前clazz对象下的static_str的字段值,返回String类型
    jstring staticStr = env->NewStringUTF("Hello JAVA!我是修改后的静态字段"); // new一个字符串
    env->SetStaticObjectField(clazz, staticField, staticStr); // 将new的字符串赋值给staticField
    return nullptr;
}

接下来再学习动态注册:
在下面的例子我们注册了一个add函数,并通过RegisterNatives进行注册。

// Native层的具体实现(C/C++代码)
jint add(JNIEnv* env, jobject thiz, jint a, jint b) {
    return a + b;
}

 
extern "C"
// JNI层的注册逻辑(C/C++代码)
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // 定义Java方法与Native函数的映射
    JNINativeMethod methods[] = {
        {"add", "(II)I", (void*)add}  // 将Java的add方法绑定到Native的add函数
    };

    // 注册到Java类
    jclass clazz = env->FindClass("com/example/MyClass");
    env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof(methods[0]));

    return JNI_VERSION_1_6;
}

Java层 -> JNI层 -> Native层
那么具体的,哪些对应JNI层,哪些对应Native层呢?
正如上面的代码里的,无论静态注册还是动态注册,add 函数本身是用 C/C++ 编写的本地代码,那么它就属于 Native层
而通过 JNI_OnLoad 函数将 add 函数与Java类中的某个 native 方法进行绑定。这一过程属于 JNI层,负责建立Java方法与Native代码的映射关系。

很简单,至此我们就对Android中比较重要的JNI层的静态函数和动态函数的注册过程有了简单的认识了。

我们都知道JNI结构是 Java 层 -> JNI -> Native 层, 以此实现Java 层和Native层可以互相调用
每个应用程序都是由一个或多个进程组成,每个进程都对应着一个JVM。JVM是由代码native启动,在JVM启动后,会返回一个JavaVM结构体。每个线程又对应着一个JNIEnv的结构体。也就是说整个进程都在native的管理之下,所以native可以非常容易的改变JVM内部的数据。

总结
程序与进程是1:N,进程与DalivkVM是1:1,启动JVM后会得到一个JavaVM,同时一个进程又对应n个线程,线程对应着一个JNIEnv的结构体,我们需要通过JavaVM和JNIEnv的机构来实现相互的访问,而JNIEnv内部包含一个Pointer来指向虚拟机的Function Table

assets/JNI/file-20250501214926827.png

posted @ 2025-05-01 21:51  方北七  阅读(64)  评论(0)    收藏  举报