在 Android 中,这是一个非常经典且常见的 JNI 问题。核心原因在于 类加载器(ClassLoader)的差异 以及 JNI 如何查找类 的机制。
以下是详细的机制分析:
1. 核心原因:ClassLoader 查找机制
在 Java 中,类的加载是由 ClassLoader 完成的。Android 中主要有两种 ClassLoader:
- **System ClassLoader (BootClassLoader)**:负责加载 Android 系统核心库(如
java.lang.*,android.os.*等)。 - **Application ClassLoader (PathClassLoader)**:负责加载你的 APK 里的代码(包括你的自定义类
com.didi...)。
FindClass 的逻辑:
当你调用 env->FindClass 时,它会去寻找合适的 ClassLoader:
- 如果当前线程的调用栈里有 Java 方法,JNI 会使用调用该 Native 方法的那个 Java 类的 ClassLoader。
- 关键点: 如果你在 C++ 中自行创建了一个线程(如
pthread_create或std::thread),这个线程的调用栈里没有任何 Java 帧。 - 当你通过
AttachCurrentThread将该线程挂载到 JVM 时,JNI 默认会使用 System ClassLoader。
结论: System ClassLoader 只能找到系统类,它根本不知道你的 APK 里定义的 com.didi.hawaii.mapsdkv2.jni.ApolloJni 在哪里,所以会报 ClassNotFoundException。
2. 为什么在主线程(或 Java 调用的线程)没问题?
当 Java 代码调用一个 native 方法时,JVM 知道是哪个类发起的调用。此时 FindClass 会自动使用这个类的 PathClassLoader,它能看到 APK 内部的所有类。
3. 具体的运行机制流程
- C++ 创建线程:
pthread_create-> 产生一个纯底层的 Linux 线程。 - 挂载 JVM:调用
JavaVM->AttachCurrentThread。此时 JVM 为该线程分配了一个JNIEnv,但该线程的上下文是“空”的。 - 调用 FindClass:
FindClass检查调用栈,发现没有 Java 方法。 - 回退机制:JNI 引擎回退到
System ClassLoader。 - 查找失败:
System ClassLoader在系统的boot.art或system/framework下找不到com.didi...,返回NULL并抛出异常。
4. 解决方案
要解决这个问题,通常有以下几种标准做法:
方法 A:在 JNI_OnLoad 中缓存全局引用(推荐)
JNI_OnLoad 会在库被加载时(通常在主线程)自动执行。此时的 ClassLoader 是正确的。
1 | jclass g_apollo_class = nullptr; |
方法 B:缓存 Application ClassLoader
在 JNI_OnLoad 里手动获取并保存 Application 的 ClassLoader 对象,在子线程中通过调用 Java 的 loadClass 方法来查找类。这种方法比较复杂,一般用于插件化或特殊框架。
方法 C:由 Java 层将 Class 传进来
如果你的 native 线程是由 Java 触发的,可以在 Java 层先获取 Class 对象,通过参数传给底层。
总结
之所以找不到,是因为 C++ 异步线程丢失了 Application ClassLoader 的上下文。默认的 System ClassLoader 搜索范围不包括你的 APK。最好的实践是在 JNI_OnLoad 中预先查找类并保存为 NewGlobalRef。