Android C++系列:JNI常见问题

发布于:2024-07-03 ⋅ 阅读:(65) ⋅ 点赞:(0)

1. 背景

本文整理了JNI开发中常见的问题和解决方案。

2. 编译时指定SDK版本问题

智能语音交互SDK工程模块编译时指定的ANDROID_PLATFORM统一是23:-DANDROID_PLATFORM=23,ndk使用的是版本是17,在手上现有设备跑的都没问题,但是在一个新采购的temi移动机器人上跑不起来,定位到问题是信号处理库报了下面问题:

java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "__aeabi_memclr4" referenced by "/data/app/com.xxx.xxxx.robot-2/lib/arm/libkeos_signal_processing.so"...

最开始以为是信号处理库中用到了什么不兼容方法,把库的实现都改为空实现后仍报该错误,网上查询到是target version和目标设备不对应会报该错,机器人的系统版本是6.0,信号处理库编译时Application.mk中设置的APP_PLATFORM := android-26,修改完后就果然解决。

https://github.com/android/ndk/issues/126

https://github.com/android/ndk/issues/1188

官网中对该问题有介绍:

当尝试加载原生库时,这些错误会显示在日志中。此符号可以是 __aeabi_* 中的任意一个;其中 __aeabi_memcpy__aeabi_memclr 可能是最常见的。此问题已记录在问题 126

2. Using _FILE_OFFSET_BITS=64 with older API levels

与上一个类似,也是API版本问题。

统一头文件之前,NDK 并不支持 _FILE_OFFSET_BITS=64。如果在构建应用时定义了该选项,系统会静默地忽略它。现在,_FILE_OFFSET_BITS=64 选项受统一头文件的支持,但在旧版本的 Android 中,很少有 off_t API 可用作 off64_t 变体。因此,如果将该功能与旧版 API 级别搭配使用,会导致可用函数减少。

r16 博文bionic 文档对该问题作了详细解释。

问题: build 请求获得的 minSdkVersion 中不存在的 API。

解决方案:停用 _FILE_OFFSET_BITS=64 或提高 minSdkVersion

3. jni local reference table overflow (max=512)

正常情况我们在方法里面申请的local reference会在方法执行完后自动释放,所以一直没有太在意local reference的释放,结果在一个线程的looper方法中有两个jstring使用完一直没释放,导致交互多轮后泄露崩溃。

4. FindClass 找不到类

常见问题类型:

  1. 确保类名称字符串的格式正确无误,JNI 类名称以软件包名称开头,并用斜线分隔,例如 java/lang/String。如果要查找某个数组类,则需要以适当数量的英文方括号开头,并且还必须用“L”和“;”将该类包裹起来,因此 String 的一维数组将是 [Ljava/lang/String;。如果要查找内部类,使用“$”而不是“.”,可以在 .class 文件上使用 javap 是查找类的内部名称。
  2. 如果启用代码混淆,请确保查找的类名未被混淆。
  3. 如果类名称形式正确,可能是遇到了类加载器问题。FindClass 需要在与代码关联的类加载器的启动类搜索。它会检查调用堆栈,如下所示:
    Foo.myfunc(Native Method)
        Foo.main(Foo.java:10)

最顶层的方法是 Foo.myfuncFindClass 会查找与 Foo 类关联的 ClassLoader 对象并使用它。

采用这种方法一般情况可以满足我们的需求。但是如果在native线程(比如通过调用 pthread_create,然后使用 AttachCurrentThread 进行附加),可能会有问题。因为在这个线程中没有堆栈帧,如果从此线程调用 FindClass,JavaVM 会在“系统”类加载器(而不是与应用关联的类加载器)中启动,因此尝试查找特定于应用的类将失败。

可以通过以下几种方法来解决此问题:

  • JNI_OnLoad 中执行一次 FindClass 查找,然后缓存类引用以供日后使用。在执行 JNI_OnLoad 过程中发出的任何 FindClass 调用都会使用与调用 System.loadLibrary 的函数关联的类加载器(这是一条特殊规则,用于更方便地进行库初始化)。如果我们的应用代码要加载库,FindClass 会使用正确的类加载器。
  • 通过声明原生方法来获取 Class 参数,然后传入 Foo.class,从而将类的实例传递给需要它的函数。
  • 在某个便捷位置缓存 ClassLoader 对象的引用,然后直接发出 loadClass 调用,这个比较麻烦一些,而且缓存classloader可能也会存在一些问题。
  • 使用java/lang/ClassLoadergetClassLoader,然后调用findClass:
static jobject gClassLoader;
static jmethodID gFindClassMethod;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {
    gJvm = pjvm;  // cache the JavaVM pointer
    auto env = getEnv();
    //replace with one of your classes in the line below
    auto randomClass = env->FindClass("com/example/RandomClass");
    jclass classClass = env->GetObjectClass(randomClass);
    auto classLoaderClass = env->FindClass("java/lang/ClassLoader");
    auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",
                                             "()Ljava/lang/ClassLoader;");
    gClassLoader = env->CallObjectMethod(randomClass, getClassLoaderMethod);
    gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",
                                    "(Ljava/lang/String;)Ljava/lang/Class;");

    return JNI_VERSION_1_6;
}

jclass findClass(const char* name) {
    return static_cast<jclass>(getEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, getEnv()->NewStrin