一、引言
第一次看到 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对象模型
真正理解: