Android 系统层扫盲 11:JNI 到底是什么?Java / Kotlin 是怎么调用 C/C++ 的?

前面第八篇 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

就真正闭环了。

相关推荐
mmsx1 小时前
Android 地图几万要素卡成幻灯片?顶点缓冲批上传与显存复用
android·opengl·地图·栅格
北京自在科技12 小时前
谷歌 Find Hub 迎来新版本更新
android·安卓
古法安卓18 小时前
Android-休眠唤醒后onLocationChanged没有数据问题排查
android·java·android studio
Htr_20 小时前
Creem 2.0 使用指南:面向 AI 构建时代的资金平台
android·数据库·人工智能·ui·photoshop
wardenlzr1 天前
车机看门狗(Watchdog)为何会让整机硬重启
android·优化·车机
IT毕设实战小研1 天前
基于大数据的商场商铺数据分析与可视化的设计与实现
android·java·大数据·django·课程设计
aqi001 天前
一文读懂 HarmonyOS 7.0 带来的十大API重要升级
android·华为·harmonyos·鸿蒙·harmony
是店小二呀1 天前
鸿蒙PC开源移植:CodeLite原生IDE与Remote Agent适配
android·智能手机·远程桌面