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*
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之后,我们再来研究这个结构体里面如FindClass、GetObjectClass等JNI方法
一般来说,这些方法第一个传参都是JNIEnv,表示传入一个指向JNI 函数表的指针,提供与 Java 交互的接口,使调用的方法能够调用 Java 方法(如 CallObjectMethod)、操作 Java 对象(如创建 jobject、获取字段值)、管理本地引用(如 NewLocalRef、DeleteLocalRef)等操作
在传参中,我们还经常看到其他传入类型
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



浙公网安备 33010602011771号