C语言对象模型系列(三)JNIEnv 为什么是二级指针?本质就是函数表—— 一篇讲透 JNI 的底层设计思想
一、引言
第一次看到 JNI 时,我是懵的。
很多 Android 开发第一次看到 JNI 代码:
(*env)->CallVoidMethod(env, obj, method);
基本都会产生几个疑问:
第一反应:
env 是什么?
第二反应:
为什么是 (*env)->
第三反应:
这到底是函数调用?
还是对象调用?
甚至很多人学 JNI:
- 会写
- 会复制
- 会调用
但:根本不知道 JNI 为什么这样设计。
而当我真正理解:“C语言对象模型”
之后,我突然意识到:JNI 本质根本不是“普通 C API”。
而是:“C语言版虚函数对象”。
二、先看 JNI 的经典代码
比如:
(*env)->FindClass(env, "java/lang/String");
或者:
(*env)->CallVoidMethod(env, obj, method);
你会发现:
第一:
函数不是:
FindClass(env)
而是:
(*env)->FindClass(...)
像对象调用。
第二:
函数参数里:
env
居然又传了一次自己。
第三:
整个写法:
(*env)->
特别像:
obj->
其实:这根本不是巧合。
三、JNIEnv 到底是什么?
很多人以为:
JNIEnv
是一个普通结构体。
实际上:它本质是:“函数表指针”。
先看 JNI 源码(简化版):
typedef const struct JNINativeInterface_* JNIEnv;
JNIEnv 本质:
JNIEnv
=
JNINativeInterface_*
也就是说:env 是一个指针。
而:
JNINativeInterface_
里面是什么?
四、真正核心来了:JNI函数表
简化版:
struct JNINativeInterface_ {
jclass (*FindClass)(JNIEnv*, const char*);
void (*CallVoidMethod)(
JNIEnv*,
jobject,
jmethodID,
...
);
};
看到这里是不是突然熟悉了?
这就是:
struct + 函数指针
还记得上一篇:
typedef struct {
void (*open)(void);
} DeviceOps;
JNI 本质一样:
JNIEnv
↓
函数表
↓
具体JNI函数
五、所以 (*env)->xxx() 到底是什么?
现在拆开:
第一步
env
是:
函数表指针
第二步
(*env)
解引用。
得到:
真正函数表
第三步
(*env)->FindClass
取出函数指针。
第四步
(*env)->FindClass(...)
调用函数。
所以:
(*env)->CallVoidMethod()
本质其实就是:
从函数表中找到函数地址
↓
再调用函数
六、这其实就是 C 的“虚函数表”
现在再回头看上一篇:
device->ops->open();
是不是和 JNI 一模一样?
Linux 驱动
device
↓
ops
↓
open()
JNI
env
↓
函数表
↓
CallVoidMethod()
本质完全一致。
所以:
JNIEnv 本质就是:
“C语言版虚函数对象”。
七、为什么 JNI 不直接设计成普通函数?
很多人会问:
为什么不这样?
CallVoidMethod(env, obj, method);
非得:
(*env)->CallVoidMethod(...)
这么复杂?
因为:
JNI 从设计上,
就是“可替换函数表”。
也就是说:
JVM 可以替换整套 JNI 实现。
比如:
- HotSpot
- ART
- Dalvik
都可以:
提供不同函数实现。
但:
接口统一。
这就是:
多态。
八、为什么 env 还要再传一次自己?
经典代码:
(*env)->FindClass(env, xxx);
很多人会问:
不是已经有 env 了吗?
为什么参数里还传 env?
原因很简单:
因为:
JNI 本质还是 C。
而:
C 没有 this 指针。
Java:
obj.run();
实际上:
编译器偷偷传 this
而 C:
必须手动传。
所以:
(*env)->FindClass(env, xxx);
本质其实类似:
env->FindClass(this, xxx);
九、为什么 JNI 喜欢二级指针?
再看定义:
JNIEnv*
很多人又懵了:
怎么又是指针套指针?
其实:
因为 env 本身就是指针。
也就是说:
JNIEnv
本质:
JNINativeInterface_*
于是:
JNIEnv*
就变成:
函数表指针的指针
也就是:
二级指针。
十、为什么 JNI 必须这样设计?
因为 JNI 有一个非常重要目标:
“跨平台 ABI 稳定”
Java:
Windows
Linux
Mac
Android
都要支持 JNI。
所以:
JNI 必须:
- 不依赖 C++ ABI
- 不依赖编译器对象模型
- 保持稳定接口
于是:
最终选择:
struct + function pointer
这套经典系统级方案。
十一、现在你会发现
JNI 根本不是:
“奇怪的C语法”
而是:
完整的系统层对象模型。
甚至:
(*env)->CallVoidMethod()
本质其实等价于:
Java
obj.call();
C++
obj->virtual_func();
Linux
device->ops->open();
只是:
JNI 用 C 手写了整个对象模型。
十二、一句话总结
JNIEnv 本质不是“对象”
而是:
“函数表指针”
而:
(*env)->CallVoidMethod()
本质其实是:
“从函数表中取出函数并调用”
这就是:
C语言版虚函数表思想。
十三、最后
现在再回头看 JNI:
你会发现:
JNI 不是孤立知识。
它其实连接着:
C
↓
函数指针
↓
虚函数表
↓
Linux对象模型
↓
Android Runtime
所以:
学懂 JNI 的关键,
从来不是背 API。
而是:
真正理解 C 的对象模型。
下一篇预告
《Linux 内核里的 container_of 到底是什么黑魔法?》
下一篇正式进入 Linux Kernel。
深入:
- offsetof
- container_of
- struct embedding
- intrusive list
- Linux对象模型
真正理解:
Linux 内核为什么能用 C 写出“超级面向对象系统”。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)