AI 技术博客
Android全栈9 分钟阅读6626

【全栈第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[] 表,RegisterNativesframework 标准

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 签名
voidV
intI
booleanZ
longJ
float / doubleF / 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. 踩坑提醒

  1. FindClass 用 / 不是 .:com/example/hellojni/HelloJni,写成 . 找不到类。
  2. 字符串/数组用完不释放:GetStringUTFChars/GetArrayElements 必须配对 Release...,否则泄漏。
  3. JNIEnv 不能跨线程用:每个线程的 JNIEnv 不同,native 线程要用得先 AttachCurrentThread
  4. 签名写错:返回值/参数类型对不上,RegisterNatives 失败或运行时 NoSuchMethodError。
  5. 局部引用爆表:循环里创建大量 jobject 不删,会 LocalReferenceTable overflow,用 DeleteLocalRef

9. 常见问题分析与定位(JNI 实战)

9.1 常见问题清单

  1. UnsatisfiedLinkError: No implementation found for ...
  2. java.lang.UnsatisfiedLinkError: dlopen failed: library "xxx.so" not found
  3. native crash(SIGSEGV)在 JNI 函数里
  4. LocalReferenceTable overflow
  5. 内存持续上涨(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. 小结

  1. JNI = Java↔C/C++ 的桥,访问硬件/OS、复用 C 库、性能热点必经。
  2. framework 用动态注册:JNINativeMethod[] 表 + RegisterNatives,在 JNI_OnLoad 里绑。
  3. 签名要会读:()I/(Ljava/lang/String;)V,L 对象 [ 数组。
  4. 字符串/数组/引用用完必须释放,否则泄漏或溢出。
  5. 排查口诀:No implementation 查签名+OnLoad;not found 查 so+abi;crash 看 tombstone+ndk-stack。

下节预告

L6:SELinux 基础 —— 你的服务编进去了却起不来、logcat 一片 avc: denied?这节教你给服务"放行"。

评论