前面第八篇 Binder 中,我们已经看到过这样一条路线:
Java Binder
↓
JNI
↓
Native Binder
↓
Linux Binder Driver
当时我们只是简单说:
JNI 是 Java 世界进入 Native 世界的一座桥。
现在终于轮到把这座桥单独讲清楚。
这一篇不会钻进 JNI 大量 API,也不会去背各种函数签名。
我们只解决几个最核心的问题:
JNI 到底解决什么?
Kotlin 怎么调用 C++?
.so 是什么?
System.loadLibrary() 在干嘛?
JNIEnv 是什么?
JavaVM 又是什么?
JNI 和 NDK 有什么区别?
Binder 为什么也会经过 JNI?
还是按照我们一直的原则:
先建立地图,再深入源码。
一、先看最简单的问题
假设 Android App 里有:
fun add(
a: Int,
b: Int
): Int {
return a + b
}
这是:
Kotlin
↓
ART
世界。
现在突然有一个 C++ 函数:
int add(
int a,
int b
) {
return a + b;
}
问题来了:
Kotlin 能直接写
add(1, 2)调这个 C++ 函数吗?
不能直接调用。
因为:
Kotlin / Java
和:
C / C++
属于两套不同的运行体系。
二、两边到底有什么不同?
Kotlin / Java 世界:
Object
Class
String
GC
Exception
Method
ART Runtime
Native C / C++ 世界:
Pointer
malloc / free
Native Stack
Native Function
Native Memory
ELF / .so
它们甚至对:
对象
字符串
数组
方法
线程
的表示方式都不一样。
所以中间必须有一套:
双方都能理解的接口规范。
这就是:
JNI
三、JNI 全称是什么?
JNI:
Java Native Interface
翻译:
Java 原生接口。
Android 官方对 JNI 的定义非常直接:
JNI 定义了 Java / Kotlin 编译后的托管代码与 C/C++ Native Code 交互的方式。Android 上 Kotlin 和 Java 在 JNI 架构层面可以按同样方式理解。
所以可以先记:
Java / Kotlin
│
│ JNI
↓
C / C++
四、JNI 本身不是一种编程语言
这个很容易混。
JNI 不是:
Java
Kotlin
C++
Rust
这种语言。
它是一套:
Java Runtime 与 Native Code 之间约定好的调用接口。
你可以理解成:
JNI
=
双方交流的协议 + API
就像 Binder 规定:
Proxy
Parcel
Transaction
Stub
怎么完成跨进程调用。
JNI 则规定:
Java Method
Native Function
Object
String
Array
Exception
Thread
怎么跨越:
Managed ↔ Native
边界。
五、先写一个真正能运行的 JNI 例子
Kotlin:
class NativeMath {
companion object {
init {
System.loadLibrary(
"native_math"
)
}
}
external fun add(
a: Int,
b: Int
): Int
}
使用:
val math =
NativeMath()
val result =
math.add(1, 2)
看起来和普通 Kotlin 方法几乎一样:
math.add(1, 2)
但这里有两个特殊东西:
System.loadLibrary()
external
这两个就是入口。
六、external 是什么意思?
普通 Kotlin:
fun add(
a: Int,
b: Int
): Int {
return a + b
}
方法实现就在 Kotlin 里。
但:
external fun add(
a: Int,
b: Int
): Int
没有方法体。
它实际上是在告诉 Runtime:
这个方法的真正实现不在 Kotlin / Java 这里。
真正实现:
在 Native Library
里面。
Java 对应写法就是:
public native int add(
int a,
int b
);
Android 官方 NDK 示例同样使用 Kotlin external / Java native 来声明 Native 实现的方法。
七、真正实现在哪里?
比如:
extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_NativeMath_add(
JNIEnv* env,
jobject thiz,
jint a,
jint b
) {
return a + b;
}
这就是 C++ 世界真正干活的地方。
于是:
Kotlin
math.add(1, 2)
↓
external fun add()
↓
JNI
↓
C++
Java_com_demo_NativeMath_add()
↓
return 3
结果再回到 Kotlin。
八、这就是 JNI 最核心的事情
把它压缩一下:
Kotlin / Java 方法
↓
JNI
↓
Native Function
返回时:
Native Result
↓
JNI
↓
Kotlin / Java
所以:
JNI 本质就是托管代码和 Native Code 之间的调用桥。
九、但是 C++ 代码怎么进入 APK?
C++ 文件:
native_math.cpp
不能直接塞进去就运行。
它需要通过:
NDK
+
CMake / ndk-build
编译。
最终生成:
libnative_math.so
Android Studio / Gradle 可以调用 CMake,把 C/C++ 源码编译成 .so,然后按照 ABI 打进 APK。官方文档就是这套流程。
十、.so 是什么?
.so:
Shared Object
可以简单理解成:
Linux / Android 世界里的动态共享库。
Windows 常见:
.dll
Linux / Android 常见:
.so
例如:
libnative_math.so
里面就是:
已经编译好的机器代码
十一、不同 CPU 还需要不同 .so
Android 手机可能是:
arm64-v8a
armeabi-v7a
x86_64
不同 CPU 架构:
机器指令并不一样。
所以 Native Library 通常需要针对不同:
ABI
编译。
APK 中可能看到:
lib/
├── arm64-v8a/
│ └── libnative_math.so
│
├── armeabi-v7a/
│ └── libnative_math.so
│
└── x86_64/
└── libnative_math.so
这里:
ABI
=
Application Binary Interface
第一阶段可以理解成:
Native 二进制程序和 CPU / 系统之间的二进制约定。
十二、System.loadLibrary() 是干嘛的?
回到:
System.loadLibrary(
"native_math"
)
它的作用:
把 Native Library 加载进当前进程。
这里写:
native_math
而不是:
libnative_math.so
System.loadLibrary() 接收的是 Library Name,不带平台相关前缀、扩展名和路径。
所以概念上:
System.loadLibrary(
"native_math"
)
↓
加载
libnative_math.so
十三、注意:.so 是加载进当前 App Process 的
这一点非常重要。
假设:
com.demo
进程里执行:
System.loadLibrary("native_math")
那么:
libnative_math.so
会映射到:
com.demo App Process
的虚拟地址空间里。
所以:
App Process
ART / Kotlin / Java
│
│ JNI
↓
libnative_math.so
两边其实:
还在同一个 Linux Process。
十四、所以 JNI 不是 IPC
这个一定要分清。
Binder:
Process A
↓
Binder
↓
Process B
解决:
进程之间。
JNI:
同一个 Process
Java / Kotlin
↓
JNI
↓
C / C++
解决:
语言 / Runtime 边界。
所以:
JNI
≠
IPC
十五、把 Binder 和 JNI 放一起就更清楚了
Binder:
App Process
│
│ Binder
↓
system_server
JNI:
App Process
Java / Kotlin
│
│ JNI
↓
Native C++
而第八篇为什么会同时出现它们?
因为 Java Binder 自己还要继续调用 Native Binder:
App Process
Java Binder
↓
JNI
↓
Native Binder
↓
Binder Driver
↓
Kernel
也就是说:
JNI
和:
Binder
解决的是不同维度的问题。
十六、现在看 C++ JNI 方法的奇怪参数
刚才:
JNIEXPORT jint JNICALL
Java_com_demo_NativeMath_add(
JNIEnv* env,
jobject thiz,
jint a,
jint b
)
第一次看肯定觉得:
这都是什么?
真正业务参数明明只有:
a
b
为什么前面还有:
JNIEnv*
jobject
?
这就是 JNI 需要补充的运行环境。
十七、jint 是什么?
Java:
int
JNI:
jint
类似还有:
Java JNI
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
这些属于:
JNI 在 Java 与 Native 之间定义的数据类型。
所以:
external fun add(
a: Int,
b: Int
): Int
Native 可以看到:
jint add(
...,
jint a,
jint b
)
十八、对象类型就没这么简单了
基础类型:
Int
Long
Float
比较简单。
但如果 Kotlin:
external fun hello(
name: String
): String
Native 不会直接拿到:
std::string
而是:
jstring
因为:
Java String 是 ART 管理的对象。
C++:
不能直接把它当成自己的 C++ Object。
所以必须通过 JNI API 转换和访问。
十九、Java Object 在 Native 里通常是"引用"
例如:
jobject
jstring
jclass
jarray
本质都是:
JNI 对托管对象的引用形式。
不要简单理解成:
真正 Java Object 的裸 C++ 指针
GC 还需要管理这些对象。
所以 JNI 给 Native 提供的是:
受 Runtime 管理的引用
而不是让 C++ 随便操作 ART Heap。
二十、JNIEnv 到底是什么?
这是 JNI 最常见的东西:
JNIEnv* env
几乎所有普通 JNI Native 方法:
第一个参数都会拿到
JNIEnv。Android 官方同样指出,除特殊@CriticalNative情况外,Native JNI 函数都会收到JNIEnv。
第一阶段可以直接理解:
JNIEnv 是 Native Code 操作 Java / ART 世界的一套工具入口。
二十一、Native 想操作 Java 对象怎么办?
比如 C++ 想:
创建 Java String
读取 Java Field
调用 Java Method
找 Java Class
抛 Java Exception
不能自己瞎改 ART 内存。
都需要通过:
JNIEnv
比如:
env->FindClass(...);
env->GetMethodID(...);
env->CallVoidMethod(...);
env->NewStringUTF(...);
所以可以把:
JNIEnv
理解成:
Native 世界伸进 Java Runtime 的操作手柄。
二十二、JNIEnv 更准确地说是什么?
底层来看,JNIEnv 本质和:
JNI Function Table
相关。
JNI 规范定义:
JNIEnv
通过函数表暴露大量 JNI Function;Android 官方 JNI 文档也将其描述为指向函数表的结构。
所以:
env->FindClass()
可以粗略理解成:
JNIEnv
↓
找到 JNI Function Table
↓
调用 FindClass
↓
ART 帮你完成操作
第一阶段不用再往下钻。
二十三、JNIEnv 最重要的特征:和线程绑定
这个特别重要。
假设:
Thread A
获得:
JNIEnv A
不能把这个 JNIEnv 随便保存下来,然后交给:
Thread B
使用。
Android 官方明确说明:
JNIEnv是线程相关的,不能在线程之间共享。
所以记:
JNIEnv
=
当前线程进入 JNI 世界的操作入口
二十四、为什么要和线程绑定?
因为 Runtime 需要知道:
当前到底是哪条线程?
这条线程当前有哪些 Local Reference?
异常状态是什么?
Native → Java 调用状态是什么?
等等。
所以:
JNIEnv
天然和:
Thread
联系在一起。
二十五、那 JavaVM 又是什么?
JNI 里另一个很重要的:
JavaVM*
第一阶段可以简单理解:
JavaVM
=
当前进程里的 Java / ART Runtime 入口
而:
JNIEnv
=
某一条线程使用这个 Runtime 的 JNI 操作入口
可以画:
App Process
JavaVM
/ \
/ \
Thread A Thread B
│ │
JNIEnv A JNIEnv B
Android 进程中只允许一个 JavaVM,但不同线程会有自己的 JNIEnv。
二十六、JavaVM 和 JNIEnv 最简单怎么记?
直接记:
JavaVM
=
进程级
JNIEnv
=
线程级
虽然这是简化模型,但第一阶段非常好用。
二十七、为什么 Native 代码经常保存 JavaVM?
例如:
JavaVM* gVm;
因为如果 Native 自己创建:
C++ Thread
这个线程一开始并不是:
ART 已知的 Java Thread
它可能没有:
JNIEnv
那么它以后想:
回调 Java
怎么办?
就可以通过:
JavaVM
把当前 Native Thread:
Attach
到 Runtime。
二十八、Native Thread 回调 Java 的过程
例如 C++:
创建 pthread
↓
执行后台任务
↓
想回调 Kotlin
概念流程:
Native Thread
↓
JavaVM
↓
AttachCurrentThread()
↓
获得当前线程 JNIEnv
↓
JNIEnv
↓
调用 Java / Kotlin
线程结束之前:
DetachCurrentThread()
Android 官方建议也明确说明,Native Thread 若需要使用 JNI,需要 attach 到 VM,并在退出前 detach。
二十九、所以为什么不能直接保存 JNIEnv 给所有线程用?
因为:
JNIEnv
=
Thread Local
错误做法:
Thread A
↓
得到 env
↓
保存 globalEnv
↓
Thread B 使用 globalEnv
这是危险的。
正确思路:
保存 JavaVM
↓
不同线程
↓
各自获取 JNIEnv
这个点以后真正写 Native 多线程时非常重要。
三十、现在再看 System.loadLibrary()
前面:
System.loadLibrary(
"native_math"
)
把:
libnative_math.so
加载进进程。
Library 被加载以后,还可以提供一个特殊入口:
JNI_OnLoad()
例如:
JNIEXPORT jint JNI_OnLoad(
JavaVM* vm,
void* reserved
) {
return JNI_VERSION_1_6;
}
这个方法可以在 Native Library 加载时获得:
JavaVM
并进行初始化。
三十一、JNI_OnLoad() 常用来干嘛?
比如:
保存 JavaVM
查找 Class
缓存 MethodID
注册 Native Method
等等。
尤其现在 Android 官方更推荐:
JNI_OnLoad()
↓
RegisterNatives()
来显式注册 Native 方法。官方 JNI 指南指出,大多数 App 应考虑在 JNI_OnLoad() 中使用 RegisterNatives() 做显式绑定。
三十二、什么叫"注册 Native Method"?
还记得:
external fun add(
a: Int,
b: Int
): Int
Runtime 必须知道:
Kotlin add()
到底对应 Native 哪个:
C++ Function
所以需要:
建立映射关系
比如:
Kotlin / Java
add(II)I
│
│ 注册
↓
C++
nativeAdd()
这就是:
RegisterNatives
做的重要事情。
三十三、那刚才那个超长方法名又是什么?
我们前面为了容易看懂写:
Java_com_demo_NativeMath_add(...)
这属于:
按照 JNI 命名规则,让 Runtime 根据函数名查找 Native 实现。
也就是:
Java
+ 包名
+ 类名
+ 方法名
组成 Native Symbol。
例如:
com.demo.NativeMath.add()
可能对应:
Java_com_demo_NativeMath_add
三十四、JNI 有两种常见绑定思路
第一种:
名字匹配
例如:
Java_com_demo_NativeMath_add
优点:
简单
适合教学
一眼能看懂
第二种:
RegisterNatives()
显式告诉 Runtime:
Java 方法
↓
对应
↓
哪个 C++ 函数
现代 Android 项目中,官方更推荐后一种用于大多数 App。
第一阶段:
先把名字匹配看懂,再知道生产项目还有 RegisterNatives,就够了。
三十五、RegisterNatives 可以粗略理解成什么?
就像我们自己维护一张表:
Java Method
Native Function
add() → nativeAdd
hello() → nativeHello
decode() → nativeDecode
Runtime 以后调用:
add()
直接知道:
去 nativeAdd()
而不需要根据超长 Symbol Name 动态搜索。
三十六、现在把一个完整 JNI 调用跑一遍
Kotlin:
class NativeMath {
companion object {
init {
System.loadLibrary(
"native_math"
)
}
}
external fun add(
a: Int,
b: Int
): Int
}
调用:
val result =
NativeMath().add(1, 2)
Native:
extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_NativeMath_add(
JNIEnv* env,
jobject thiz,
jint a,
jint b
) {
return a + b;
}
三十七、真实链路就是
App Process
Kotlin
NativeMath.add(1, 2)
↓
发现是 external
↓
JNI Native Method
↓
找到 libnative_math.so 中实现
↓
进入 C++
↓
jint a = 1
jint b = 2
↓
执行 Native Code
↓
return 3
↓
JNI
↓
Kotlin Int
↓
result = 3
整个过程:
没有跨进程。
始终发生在:
同一个 App Process
三十八、如果参数变成 String 呢?
Kotlin:
external fun hello(
name: String
): String
Native:
JNIEXPORT jstring JNICALL
Java_com_demo_NativeMath_hello(
JNIEnv* env,
jobject thiz,
jstring name
) {
...
}
这里:
Kotlin String
↓
jstring
C++ 如果想得到字符数据,需要:
通过 JNIEnv
↓
读取 jstring
处理完成以后:
C++ String
↓
JNIEnv
↓
创建新的 jstring
↓
返回 Kotlin
三十九、JNI 的成本主要在哪?
很多人有一个错误直觉:
C++ 快,所以把代码放 JNI 一定更快。
不一定。
跨 JNI 边界本身就有:
参数转换
对象访问
Reference 管理
线程切换相关处理
Managed ↔ Native 状态切换
成本。
Android 官方 JNI 指南明确建议:
尽量减少 JNI 边界的数据编组和跨越频率,把 JNI 接口设计得尽量小而清晰。
所以:
每帧调用 JNI 几万次
未必是什么好设计。
四十、JNI 什么时候有价值?
典型场景:
复用已有 C / C++ Library
音视频
Codec
图像处理
游戏 Engine
高性能计算
底层硬件库
已有跨平台 Native Core
Android 官方也把:
性能要求较高
以及:
复用已有 C/C++ 库
列为使用 NDK 的常见理由。
四十一、所以 JNI ≠ "性能加速开关"
比如:
a + b
为了所谓性能:
Kotlin
↓
JNI
↓
C++
↓
a + b
↓
JNI
↓
Kotlin
显然没有意义。
更适合 JNI 的是:
Kotlin
↓
一次 JNI 调用
↓
C++ 连续处理大量工作
↓
一次返回
也就是:
减少边界穿越,让 Native 真正承担适合它的工作。
四十二、JNI 和 NDK 到底是什么关系?
这个特别容易混。
很多人会说:
JNI
=
NDK
其实不是。
四十三、NDK 是什么?
NDK:
Native Development Kit
Android 官方定义:
NDK 是允许 Android 项目使用 C/C++ 的一套工具和平台库。
里面包括:
Compiler / Clang
Headers
Native APIs
Linker Support
Build Toolchain
CMake / ndk-build 配合
libc
Native Libraries
等等。
所以:
NDK 是 Native 开发工具箱。
四十四、JNI 又是什么?
JNI:
Managed Code 和 Native Code 之间的调用接口。
所以关系:
Android App
│
│ JNI
↓
C / C++
│
│ 使用 NDK 编译
↓
.so
可以直接记:
NDK
=
帮你开发 / 编译 Native Code
JNI
=
帮 Java / Kotlin 和 Native Code 互相调用
四十五、一个非常简单的比喻
可以把:
Java / Kotlin
想成:
中国人。
C++:
外国人。
JNI:
翻译。
NDK:
外国人工作的整套工具、设备和工作环境。
所以:
JNI
≠
NDK
但 Android App 用 C++ 时:
经常两个一起出现。
四十六、JNI 也可以反过来:C++ 调 Java
很多人第一次学只看到:
Java
↓
C++
其实 JNI 是双向的。
Native 可以通过:
JNIEnv
找到:
Class
Method
Object
然后:
C++
↓
JNIEnv
↓
CallXXXMethod()
↓
Java / Kotlin
所以:
Java → Native
可以。
Native → Java
也可以。
四十七、这在音视频里特别常见
例如 Native Decoder:
C++
↓
后台解码
↓
得到进度
↓
callback
↓
Kotlin
↓
更新 UI
这时候:
Native
↓
JNI
↓
Java Callback
就非常常见。
当然如果回调来自:
Native Thread
就又涉及刚才讲的:
JavaVM
↓
AttachCurrentThread
↓
JNIEnv
所以这些知识是连在一起的。
四十八、JNI Object Reference 还有生命周期
Native 拿到:
jobject
不能简单理解:
我拿到了以后永远有效。
JNI Reference 有:
Local Reference
Global Reference
Weak Global Reference
等类型。
第一阶段不用深挖。
只需要知道:
Java Object 仍然由 ART / GC 管理,Native 必须按照 JNI Reference 规则持有它。
不能把:
jobject
当普通 C++ Pointer 随便长期保存。
四十九、Local Reference 可以先理解成"当前调用临时使用"
例如:
jclass clazz =
env->FindClass(...);
这类引用很多时候属于:
Local Reference
通常和当前:
JNI 调用 / Native Thread
的生命周期相关。
如果 Native 想长期保存 Java Object:
需要使用:
Global Reference
并在不需要时释放。
否则:
Native 长期持有 Java Object
↓
GC 无法回收
↓
泄漏
五十、所以 JNI 其实是一套"受控访问"
JNI 并不是:
C++
↓
直接伸手进 ART Heap
↓
随便改对象
而是:
C++
↓
JNIEnv
↓
JNI API
↓
ART
↓
Java Object
Runtime 始终参与其中。
这样才能让:
GC
Thread
Exception
Object Lifetime
继续保持正确。
五十一、Native Code 也不是绝对安全的
进入 C++ 后:
GC
不会替你管理普通 Native Memory。
你可能遇到:
内存泄漏
野指针
Use After Free
越界访问
Double Free
SIGSEGV
如果 Native Code:
Crash
通常就是:
整个 App Process 一起死。
不是只崩掉一个 .so。
五十二、为什么会整个 App 一起死?
因为刚才已经讲过:
Kotlin / Java
+
libnative_math.so
其实都运行在:
同一个 App Process
所以:
C++ SIGSEGV
↓
Linux Process Crash
↓
整个 App Process 死亡
非常合理。
这也是 Native 开发比纯 Kotlin / Java:
更需要重视内存安全的原因。
五十三、进入 Native 也不会获得系统权限
这一点和第八篇可以再串一次。
假设:
普通 App
↓
JNI
↓
C++
你还是:
这个 App 的 Linux UID
还是运行在:
这个 App 的 Sandbox
所以:
Java
↓
JNI
↓
C++
不意味着:
突然变成 root
也不意味着:
突然拥有 system 权限
NativeActivity 等 NDK 应用同样仍然运行在自己的应用沙箱中。
五十四、现在终于可以回头看 Binder 的 JNI
第八篇我们留下:
BinderProxy.transact()
↓
transactNative()
↓
JNI
↓
Native Binder
现在应该完全不陌生了。
Java Framework:
BinderProxy
需要调用:
C++
中的 Native Binder。
怎么办?
当然就是:
JNI
五十五、AOSP 里真的就是这样
当前 AOSP:
android_util_Binder.cpp
中仍然注册:
BinderProxy.transactNative
对应 Native:
android_os_BinderProxy_transact
。
也就是说:
Java
BinderProxy.transactNative()
│
│ JNI
↓
C++
android_os_BinderProxy_transact()
这就是一个真正的:
Framework 自己使用 JNI 的例子。
五十六、所以 Android Framework 自己也大量使用 JNI
JNI 不是:
"只有 App 做音视频时才用的东西"。
Android Framework 自己就大量存在:
Java Framework
↓
JNI
↓
Native Framework
例如我们已经看到:
Binder
还有很多:
Graphics
Input
Media
Runtime
Surface
相关系统都会跨越 Java / Native 边界。
所以系统层开发:
JNI 是必须能看懂的基础语言。
五十七、现在 Android 体系可以再往下补一层
以前:
App
↓
Framework
↓
Binder
↓
system_server
现在又能看到:
Java Framework
↓
JNI
↓
Native Framework
↓
Kernel
所以 Android 其实一直在几个世界之间穿梭:
Kotlin / Java
↓
JNI
↓
C / C++
↓
Linux Kernel
而进程之间:
Process A
↓
Binder
↓
Process B
这两个维度是同时存在的。
五十八、不要把 JNI 和 Binder 混成一条概念
可以这样区分:
JNI
解决:
语言 / Runtime 边界
Java
↓
C++
而:
Binder
解决:
Process 边界
Process A
↓
Process B
所以第八篇 Binder 里出现 JNI:
只是因为 Binder 自身的实现需要从:
Java Binder
进入:
Native Binder
然后才继续:
Binder Driver
五十九、把 Binder 完整图再看一次
现在你应该比第八篇更能理解:
App Process
Java
IPowerManager.Proxy
↓
BinderProxy
↓
transactNative()
│
│ JNI
↓
Native
BpBinder
↓
IPCThreadState
↓
ioctl()
│
↓
Linux Kernel
Binder Driver
这里:
JNI
负责:
Java → Native
Binder Driver:
Process → Process
两者职责完全不同。
六十、再把普通 App JNI 图看一次
普通 NDK App:
App Process
Kotlin / Java
↓
external / native Method
↓
JNI
↓
libxxx.so
↓
C / C++
全部:
在同一个进程
所以这张图和 Binder 图不能混。
六十一、第一阶段 JNI 只需要认识几个名字
以后看源码,看到:
JNIEnv
想到:
当前线程操作 Java Runtime 的 JNI 接口。
看到:
JavaVM
想到:
当前进程 Java / ART Runtime 的 VM 接口。
看到:
jobject
想到:
Java Object 的 JNI Reference。
看到:
jclass
想到:
Java Class 的 JNI Reference。
看到:
jstring
想到:
Java String 的 JNI Reference。
看到:
JNI_OnLoad
想到:
Native Library 加载时常见的 JNI 初始化入口。
看到:
RegisterNatives
想到:
Java Native Method ↔ C/C++ Function 显式建立映射。
六十二、JNI 的调用地图
可以记:
Kotlin / Java
external fun xxx()
│
↓
JNI
│
↓
Native Function
│
↓
C / C++
如果 Native 需要操作 Java:
C / C++
↓
JNIEnv
↓
Java Object / Method / Class
如果 Native Thread 想进入 Java:
Native Thread
↓
JavaVM
↓
AttachCurrentThread()
↓
JNIEnv
↓
Java
六十三、JNI、NDK、.so 三者关系
这三个经常一起出现,直接这样记:
NDK
↓
开发 / 编译 C/C++
↓
生成
↓
.so
运行时:
System.loadLibrary()
↓
加载 .so
↓
JNI
↓
Kotlin / Java ↔ C/C++
所以:
NDK
=
工具链
.so
=
Native 编译产物
JNI
=
Java / Native 通信桥
六十四、为什么系统层工程师一定要懂 JNI?
因为 Android 并不是纯 Java 系统。
上层:
Framework Java
下面大量都是:
C / C++
再往下:
Linux Kernel
所以经常出现:
Java API
↓
JNI
↓
Native Service / Library
↓
Driver
如果不懂 JNI:
你看到:
native xxx()
以后就像:
代码突然消失了。
懂 JNI 后:
native
意味着:
这条调用链开始从 Java 世界进入 Native 世界了。
六十五、以后看源码看到 native 不要慌
例如:
private static native
boolean nativeSomething(...);
第一反应应该是:
Java 这里没有实现
↓
找 JNI 注册
↓
找对应 Native Function
↓
进入 C / C++
于是源码继续跟:
Java
↓
JNI Registration
↓
.cpp
就行。
这就是系统层源码阅读很重要的一种能力。
六十六、第一阶段必须回答出的几个问题
1. JNI 是什么?
Java / Kotlin
和
C / C++
之间的调用接口
2. JNI 是 IPC 吗?
不是。
普通 JNI 调用:
同一个 Process
3. external / native 是什么?
表示:
方法真正实现
在 Native Code
4. .so 是什么?
Android / Linux Native Shared Library
5. System.loadLibrary() 做什么?
把 Native Library
加载进当前 Process
6. JNIEnv 是什么?
第一阶段:
当前线程
操作 Java Runtime
的一套 JNI 接口
7. JNIEnv 能跨线程共用吗?
不能。
JNIEnv
与线程绑定
8. JavaVM 是什么?
第一阶段:
进程级 Java / ART Runtime 接口
Android 一个进程中只有一个 JavaVM。
9. Native Thread 怎么调用 Java?
JavaVM
↓
AttachCurrentThread()
↓
得到 JNIEnv
↓
调用 Java
10. JNI 和 NDK 是一个东西吗?
不是。
JNI
=
通信桥
NDK
=
Native 开发工具箱
11. C++ 一定比 Kotlin 快吗?
不是。
JNI 本身有跨边界成本。
12. JNI 能绕过 Android 权限吗?
不能。
Native Code 仍然属于:
当前 App Process
+
当前 Linux UID
+
当前 Sandbox
六十七、最核心的一张图
Android App Process
Kotlin / Java
│
│
external / native
│
↓
JNI
┌──────────┴──────────┐
│ │
↓ ↓
JNIEnv JavaVM
│ │
当前线程操作 Java 进程级 VM 接口
│ │
└──────────┬──────────┘
↓
C / C++
│
↓
libxxx.so
│
↓
Native Library
而 Native Library 来自:
C / C++ Source
↓
NDK
↓
CMake / ndk-build
↓
libxxx.so
六十八、如果再加入 Binder
整个 Android 就更清楚:
App Process
Java Framework
↓
BinderProxy
↓
JNI
↓
Native Binder
↓
Binder Driver
=========================
Process Boundary
=========================
system_server
所以:
JNI
=
跨语言 / Runtime
Binder
=
跨进程
这是第十一篇最重要的两个概念边界。
六十九、把前十一篇继续串起来
现在我们的 Android 地图已经越来越完整:
Linux Kernel
↑
│
Native C / C++
↑
│ JNI
│
Java Framework
↑
│
App
而横向:
App Process
↓
Binder
↓
system_server
再结合:
Zygote
↓
fork
↓
App Process
现在你已经开始能看到:
Android 并不是一套单一语言写出来的 Framework。
而是:
Linux
+
Native
+
ART / Java
+
Binder IPC
+
Framework
共同组成的系统。
七十、一句话总结
JNI 本质上就是:
Java / Kotlin 与 C/C++ Native Code 之间的一座受控调用桥。
Kotlin:
external fun add(
a: Int,
b: Int
): Int
表面看:
add(1, 2)
下面实际:
Kotlin / Java
↓
JNI
↓
Native Function
↓
C / C++
↓
JNI
↓
Kotlin / Java
而:
JNIEnv
负责:
让当前 Native Thread 操作 Java Runtime。
JavaVM
负责:
提供进程级的 VM 接口,并帮助 Native Thread 接入 Runtime。
NDK
负责:
开发和编译 Native Code。
.so:
是 Native Code 编译出来的共享库。
最后一定要记住:
JNI
≠
Binder
JNI:
Java
↓
C++
Binder:
Process A
↓
Process B
而 Android Framework 经常把两者连起来:
Java Framework
↓
JNI
↓
Native Framework
↓
Binder / Driver / Kernel
到这里:
Java 世界怎么进入 Native 世界,这块黑盒也基本补上了。
下一篇:
《Android 系统层扫盲 12:HAL 到底是什么?Framework 为什么不能直接操作硬件?》
现在我们已经走到了:
App
↓
Java Framework
↓
JNI
↓
Native
接下来再往下一层:
Native / System Service
↓
HAL
↓
Kernel Driver
↓
Hardware
第十二篇会把:
HAL 是什么?
为什么 Android 需要 HAL?
Framework 为什么不直接操作 Driver?
HAL 和 Kernel Driver 有什么区别?
AIDL HAL 又是什么?
System Service、HAL、Driver、Hardware 到底怎么串起来?
一次讲清。
这样第一阶段的 Android 系统层大地图:
App
↓
Framework
↓
Binder
↓
System Service
↓
JNI / Native
↓
HAL
↓
Kernel Driver
↓
Hardware
就真正闭环了。