关键词:JNI / JNIEnv / 二级指针 / 函数表 / 函数指针 / C 对象模型 /
->语法糖 / 系统接口
适合人群:Android NDK / C / 系统层方向学习者
一、先给结论:JNI 不是函数库,是函数表
几乎所有 JNI 教程都会从这句开始:
(*env)->CallVoidMethod(env, obj, mid);很多人被这行代码吓住,是因为:
- 二级指针
*->- 函数指针
- 可变参数
全叠在了一起。
但如果从系统层视角看,这行代码非常“朴素”:
👉 JNI 不是函数集合
👉 JNI 是 JVM 给 native 世界的一张函数表(能力表)
而这行代码,本质就是:
👉从函数表中取一个函数地址,然后跳过去执行。
二、什么叫“结构体函数表”?
先不谈 JNI,看一个纯 C 结构体。
1️⃣ 普通结构体(存数据)
struct Person { int age; float height; };这是“数据结构”。
2️⃣ 函数表结构体(存函数地址)
struct Ops { void (*open)(); int (*read)(int); void (*close)(); };这个结构体:
- 不存业务数据
- 每个成员都是函数指针
内存形态是:
open -> 某个函数地址 read -> 某个函数地址 close -> 某个函数地址👉 这种结构体,在系统层就叫:
✅函数表 / 操作表 / 虚函数表(vtable)
一句话定义:
👉结构体里几乎全是函数指针,它就是函数表。
三、JNI 的函数表是什么?
在jni.h中(简化):
struct JNINativeInterface_ { jclass (*FindClass)(JNIEnv*, const char*); jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); ... };这个结构体是什么?
👉 JVM 对外暴露的全部能力入口表
FindClass / CallVoidMethod / NewObject
都只是这张表里的“函数地址槽位”。
四、JNIEnv 到底是什么?
接着看这一句:
typedef const struct JNINativeInterface_* JNIEnv;这句话极其关键:
👉JNIEnv本身就是一个指针类型
也就是:
JNIEnv == struct JNINativeInterface_*所以:
👉 JNIEnv 不是对象
👉 是“指向函数表结构体的指针”
五、那JNIEnv* env又是什么?
native 方法参数是:
JNIEnv* env把 JNIEnv 展开:
struct JNINativeInterface_** env;所以真实关系是:
env ---> JNIEnv ---> JNINativeInterface_ 指针 指针 结构体(函数表)结论非常清楚:
env:二级指针*env:一级指针(函数表指针)**env:函数表结构体本体
六、env 和 *env 分别是什么?
类比最普通的 C 指针:
int x = 10; int* p = &x; int** pp = &p;| 表达式 | 含义 |
|---|---|
| pp | 指向 p 的指针 |
| *p | p |
| **p | x |
JNI 中:
| 表达式 | 含义 |
|---|---|
| env | 指向函数表指针的指针 |
| *env | 函数表指针 |
| **env | 函数表结构体 |
所以这句话完全正确:
👉*env仍然是指针
👉 它是指向函数表结构体的指针
七、->到底在干嘛?(核心解惑点)
C 语言里有一个定义级别等价式:
p->field ≡ (*p).field也就是说:
👉->不是新机制
👉 它只是(*p).的语法糖
作用只有一个:
先解引用指针,再访问结构体成员。
1️⃣ 普通例子
struct S { int x; }; struct S s; struct S* p = &s; p->x 等价于 (*p).xp 是指针,
但它指向 struct,所以可以用->。
2️⃣ 套回 JNI
你已经知道:
*env == struct JNINativeInterface_*那:
(*env)->CallVoidMethod严格展开就是:
(*(*env)).CallVoidMethod也就是:
👉 解一次指针
👉 得到“指向结构体的指针”
👉 再解一次
👉 访问结构体里的字段
八、->CallVoidMethod到底是什么意思?
回到结构体定义:
struct JNINativeInterface_ { void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); };所以:
table->CallVoidMethod
含义是:
👉 从结构体中,取出一个成员变量
👉 这个变量的类型是:函数指针
它不是在“调用函数”,
👉 是在取函数地址。
九、整句 JNI 拆成三行白话 C
(*env)->CallVoidMethod(env, obj, mid);
等价于:
struct JNINativeInterface_* table = *env; // ① 取函数表指针 void (*func)(JNIEnv*, jobject, jmethodID, ...) = table->CallVoidMethod; // ② 取函数地址 func(env, obj, mid); // ③ 调用这一刻你可以清楚看到:
👉 没有魔法
👉 全是指针 + 结构体 + 函数指针调用
十、为什么 JNI 一定要设计成“函数表 + 二级指针”?
因为 JNI 是系统级 ABI 边界。
它必须:
- 跨虚拟机实现(Dalvik / ART / 未来 JVM)
- 跨 Android 版本
- 跨 CPU 架构
- 保证 so 的二进制兼容
系统世界最稳定的接口形式只有一种:
👉函数表(vtable / ops table)
Linux 内核、HAL、驱动、OpenGL、FFmpeg、COM,全是这套。
十一、把这条认知链彻底钉死
👉 函数表:结构体里全是函数指针
👉 JNIEnv:指向函数表的指针
👉 env:指向“函数表指针”的指针(二级指针)
👉*env:函数表指针
👉->:(*p).的语法糖
👉->CallVoidMethod:从结构体中取函数指针
👉(...):真正调用
十二、你以后应该怎样“系统级读 JNI”
当你再看到:
(*env)->FindClass(env, ...); (*env)->CallObjectMethod(env, ...);你脑中自动翻译成:
👉 通过函数表
👉 查到函数地址
👉 用 this(env)调用
而不是:
👉 JNI 的奇怪写法。
十三、一句话系统级总结
JNI 不是函数集合,
JNI 是 JVM 的能力函数表。
env 是这张表的对象句柄。->只是解引用 + 取成员的语法糖。
一切 JNI 调用,本质都是“查表跳转”。