【全栈第5课】JNI 原理 + 注册方式(Java ↔ C++ 的桥)
Android 全栈工程师进阶教程 第05课。JNI 原理 + 注册方式(Java ↔ C++ 的桥)。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 05 课。完整 26 课见 GitHub 仓库。
【全栈第5课】JNI 原理 + 注册方式(Java ↔ C++ 的桥)
0. 这节课你将做出什么
写一个 HelloJni 类:Java 声明 native 方法,C++ 实现并用动态注册(framework 标准方式)绑定。编出 libhellojni.so,Java 调 nativeGetCpuCount() 时下到 C++ 读系统 CPU 核数返回。
学完你能回答:Java 怎么调到 C/C++?为什么 framework 不用教程里常见的"静态注册",而用 RegisterNatives?(Landroid/os/MessageQueue;Z)J 这种天书签名怎么读?
1. 背景:Java 有些事干不了,得下到 C/C++
Java 跑在 ART 虚拟机里,但有些事它做不了或做不好:
- 调操作系统/硬件:
open("/dev/xxx")、ioctl、读寄存器 —— 你后面写 HAL/驱动全靠这个 - 复用现成 C/C++ 库
- 性能热点(编解码、加密)
JNI(Java Native Interface) 就是 Java 和 C/C++ 互相调用的桥。IMS 读触摸事件、PowerManager 控电源,底层全是 JNI 下到 native。
2. 全局图:一次 JNI 调用
Java: HelloJni.nativeGetCpuCount() ← 声明为 native,Java 里没实现体
│ ART 通过"注册表"找到对应的 C++ 函数指针
▼
C++: static jint nativeGetCpuCount(JNIEnv* env, jclass clazz) { ... }
(第一个参数 JNIEnv* 是 JNI 的"瑞士军刀",所有 JNI 操作都靠它)
两种把 Java native 方法 ↔ C++ 函数绑起来的方式:
| 方式 | 怎么绑 | 谁用 |
|---|---|---|
| 静态注册 | 函数名必须叫 Java_包名_类名_方法名,JVM 按名字找 | 教程常见,framework 不用 |
| 动态注册 | 一张 JNINativeMethod[] 表,RegisterNatives 绑 | framework 标准 |
3. 某真实机型 真实源码印证(IMS 的 JNI)
services/core/jni/com_android_server_input_InputManagerService.cpp:
static const JNINativeMethod gInputManagerMethods[] = { :3849
// Java方法名, 签名, C++函数指针
{"start", "()V", (void*)nativeStart},
{"init", "(L...;Landroid/os/MessageQueue;Z)J", (void*)nativeInit},
};
int register_android_server_InputManager(JNIEnv* env) { :4014
jniRegisterNativeMethods(env,
"com/android/server/input/NativeInputManagerService$NativeImpl", // 哪个类
gInputManagerMethods, NELEM(gInputManagerMethods)); // 方法表
}
一张表把 Java 方法名 ↔ C++ 函数指针绑起来,加载 so 时一次注册完。 framework 全这么干。
4. 前置准备
- 依赖 L1 对"native 服务"的认知。
- 本课 3 文件:
HelloJni.java(Java 侧)、hellojni.cpp(C++ 实现+动态注册)、Android.bp(编 .so + jar)。 - 环境:AOSP 编译(
cc_library_shared出 .so)。
5. 动手:一步步做
步骤 1:Java 侧 HelloJni.java
package com.example.hellojni;
public class HelloJni {
static {
System.loadLibrary("hellojni"); // 加载 libhellojni.so,触发 JNI_OnLoad
}
// native 方法:Java 只声明,实现在 C++
public static native int nativeGetCpuCount();
public native String nativeSayHello(String name);
}
> System.loadLibrary("hellojni") 加载 libhellojni.so,加载时 ART 会自动调用 so 里的 JNI_OnLoad —— 我们就在那里做动态注册。
步骤 2:C++ 侧 hellojni.cpp(逐段)
native 实现:
#include <jni.h>
#include <unistd.h>
// 对应 Java: public static native int nativeGetCpuCount();
// 注意参数:静态方法是 (JNIEnv*, jclass);实例方法是 (JNIEnv*, jobject)
static jint nativeGetCpuCount(JNIEnv* env, jclass clazz) {
long n = sysconf(_SC_NPROCESSORS_ONLN); // Java 干不了的事:读系统 CPU 核数
return (jint) n;
}
// 对应 Java: public native String nativeSayHello(String name);
static jstring nativeSayHello(JNIEnv* env, jobject thiz, jstring name) {
const char* cname = env->GetStringUTFChars(name, nullptr); // jstring → C 字符串
std::string result = std::string("Hello from native, ") + cname;
env->ReleaseStringUTFChars(name, cname); // ★必须释放,否则内存泄漏
return env->NewStringUTF(result.c_str()); // C 字符串 → jstring
}
> 字符串转换是 JNI 最常见的坑:GetStringUTFChars 拿到的 C 字符串用完必须 ReleaseStringUTFChars 释放,否则每次调用都泄漏一点。
动态注册表 + JNI_OnLoad:
static const JNINativeMethod gMethods[] = {
{"nativeGetCpuCount", "()I", (void*)nativeGetCpuCount},
{"nativeSayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)nativeSayHello},
};
// JNI_OnLoad:加载 so 时 ART 自动调用,我们在这里动态注册
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR;
jclass clazz = env->FindClass("com/example/hellojni/HelloJni"); // 找到 Java 类(注意是 / 不是 .)
if (clazz == nullptr) return JNI_ERR;
if (env->RegisterNatives(clazz, gMethods, // 绑定方法表
sizeof(gMethods)/sizeof(gMethods[0])) < 0) return JNI_ERR;
return JNI_VERSION_1_6;
}
> FindClass 的类名用 / 分隔(com/example/hellojni/HelloJni),不是 .。这是 JNI 的约定,写错就找不到类。
步骤 3:Android.bp
cc_library_shared {
name: "libhellojni",
srcs: ["hellojni.cpp"],
shared_libs: ["liblog"],
header_libs: ["jni_headers"], // 提供 jni.h
sdk_version: "current",
}
java_library {
name: "hellojni-java",
srcs: ["HelloJni.java"],
sdk_version: "current",
}
6. 看底层:方法签名怎么读
签名格式 (参数类型...)返回类型,类型用单字母编码:
| Java 类型 | JNI 签名 |
|---|---|
| void | V |
| int | I |
| boolean | Z |
| long | J |
| float / double | F / D |
| 对象 | L包名/类名; |
| 数组 | [ |
例子:
()I= 无参,返回 int(Ljava/lang/String;)Ljava/lang/String;= 参数 String,返回 String(Landroid/os/MessageQueue;Z)J= 参数(MessageQueue 对象, boolean)→ 返回 long([Landroid/hardware/display/DisplayViewport;)V= 参数(DisplayViewport 数组)→ void
签名错了 RegisterNatives 会失败或运行时 NoSuchMethodError。
7. 编译 & 运行(验证状态如实)
mm # 编出 libhellojni.so
# 加载运行(在能跑的环境):
# Java 端 System.loadLibrary("hellojni") 后调 nativeGetCpuCount()
adb logcat | grep -i HelloJni # 看 JNI_OnLoad 注册日志 + 调用日志
预期日志:
HelloJni: JNI_OnLoad: 动态注册 2 个 native 方法完成
HelloJni: nativeGetCpuCount -> 8
HelloJni: nativeSayHello(zhoubenliang)
> 验证状态(诚实):HelloJni.java 本机 javac 通过;.so 完整 mm 编译未单独跑(需 AOSP 环境)。代码为 framework 标准动态注册写法。
8. 踩坑提醒
- FindClass 用
/不是.:com/example/hellojni/HelloJni,写成.找不到类。 - 字符串/数组用完不释放:
GetStringUTFChars/GetArrayElements必须配对Release...,否则泄漏。 - JNIEnv 不能跨线程用:每个线程的 JNIEnv 不同,native 线程要用得先
AttachCurrentThread。 - 签名写错:返回值/参数类型对不上,RegisterNatives 失败或运行时 NoSuchMethodError。
- 局部引用爆表:循环里创建大量 jobject 不删,会
LocalReferenceTable overflow,用DeleteLocalRef。
9. 常见问题分析与定位(JNI 实战)
9.1 常见问题清单
UnsatisfiedLinkError: No implementation found for ...java.lang.UnsatisfiedLinkError: dlopen failed: library "xxx.so" not found- native crash(SIGSEGV)在 JNI 函数里
LocalReferenceTable overflow- 内存持续上涨(JNI 泄漏)
9.2 分析与定位
① No implementation found
- 原因:native 方法没注册成功 —— 签名不匹配 / FindClass 类名错 / JNI_OnLoad 没执行。
- 定位:
adb logcat | grep -iE "JNI_OnLoad|RegisterNatives|No implementation"
- 看 JNI_OnLoad 有没有跑、RegisterNatives 返回值;核对方法名+签名和 Java 声明完全一致。
② library not found(dlopen failed)
- 原因:.so 没打进包 / abi 不匹配(arm64 设备装了 armv7 的 so)/ 依赖的 so 缺失。
- 定位:
adb shell ls /system/lib64/ | grep hellojni # so 在不在
adb logcat | grep -iE "dlopen|cannot locate" # 缺哪个依赖符号
③ native crash 在 JNI 函数
- 现象:tombstone 栈顶在你的 cpp。
- 原因:空指针(GetStringUTFChars 返回 null 没判)、越界、用了已释放的引用。
- 定位:
adb shell ls /data/tombstones/
adb shell cat /data/tombstones/tombstone_xx # 看 backtrace 哪一行 cpp
ndk-stack -sym <符号目录> -dump tombstone_xx # 符号化
④ LocalReferenceTable overflow
- 原因:循环里创建大量局部 jobject 引用不删(JNI 局部引用默认上限 512)。
- 修复:循环里
env->DeleteLocalRef(obj),或PushLocalFrame/PopLocalFrame。
⑤ JNI 内存泄漏
- 原因:
GetStringUTFChars/NewGlobalRef没配对释放。 - 定位:
dumpsys meminfo看 native heap 涨;用 malloc debug / asan 抓。
9.3 定位决策树
JNI 出问题
├─ No implementation found → logcat JNI_OnLoad/RegisterNatives → 签名/类名/OnLoad 没跑
├─ library not found → ls /system/lib64 + logcat dlopen → so 没打进/abi不符/缺依赖
├─ native crash → tombstone + ndk-stack 符号化 → 空指针/越界/野引用
├─ ReferenceTable overflow → 循环里 DeleteLocalRef
└─ 内存涨 → GetStringUTFChars/GlobalRef 没释放 → 配对 Release
10. 小结
- JNI = Java↔C/C++ 的桥,访问硬件/OS、复用 C 库、性能热点必经。
- framework 用动态注册:
JNINativeMethod[]表 +RegisterNatives,在JNI_OnLoad里绑。 - 签名要会读:
()I/(Ljava/lang/String;)V,L 对象[数组。 - 字符串/数组/引用用完必须释放,否则泄漏或溢出。
- 排查口诀:No implementation 查签名+OnLoad;not found 查 so+abi;crash 看 tombstone+ndk-stack。
下节预告
L6:SELinux 基础 —— 你的服务编进去了却起不来、logcat 一片 avc: denied?这节教你给服务"放行"。