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 级别搭配使用,会导致可用函数减少。
问题: build 请求获得的 minSdkVersion
中不存在的 API。
解决方案:停用 _FILE_OFFSET_BITS=64
或提高 minSdkVersion
。
3. jni local reference table overflow (max=512)
正常情况我们在方法里面申请的local reference会在方法执行完后自动释放,所以一直没有太在意local reference的释放,结果在一个线程的looper方法中有两个jstring使用完一直没释放,导致交互多轮后泄露崩溃。
4. FindClass
找不到类
常见问题类型:
- 确保类名称字符串的格式正确无误,JNI 类名称以软件包名称开头,并用斜线分隔,例如
java/lang/String
。如果要查找某个数组类,则需要以适当数量的英文方括号开头,并且还必须用“L”和“;”将该类包裹起来,因此String
的一维数组将是[Ljava/lang/String;
。如果要查找内部类,使用“$”而不是“.”,可以在 .class 文件上使用javap
是查找类的内部名称。 - 如果启用代码混淆,请确保查找的类名未被混淆。
- 如果类名称形式正确,可能是遇到了类加载器问题。
FindClass
需要在与代码关联的类加载器的启动类搜索。它会检查调用堆栈,如下所示:
Foo.myfunc(Native Method)
Foo.main(Foo.java:10)
最顶层的方法是 Foo.myfunc
。FindClass
会查找与 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/ClassLoader
的getClassLoader
,然后调用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