第1章:常见错误与解决方案

各位同学好,我是老张。今天咱们聊聊NDK开发中那些让人头疼的错误。说实话,我刚开始搞NDK那会儿,光是undefined reference就折腾了我整整两天。后来带团队时发现,新人踩的坑几乎都是这几个。今天我把它们掰开揉碎了讲清楚,希望能帮你少走弯路。

本章核心内容:undefined reference、linker error、ABI mismatch、JNI FindClass失败、内存泄漏检测。这五个问题,基本覆盖了NDK开发中90%的编译期和运行时错误。

1.1 undefined reference:链接器在抱怨什么?

先说说最常见的undefined reference。说白了,就是链接器找不到某个函数或变量的定义。你声明了,但没实现,或者实现文件没参与编译。

典型场景:

  • 你调用了native函数,但对应的C/C++实现没写
  • 你链接了静态库,但库文件路径不对
  • 你用了第三方库,但忘了加-l参数

举个例子,我在项目中遇到过这样一个情况:

// native-lib.cpp
#include <jni.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    // 调用了某个函数,但没包含实现
    return getSomeString();  // undefined reference to 'getSomeString()'
}

为什么会这样?因为getSomeString函数只声明了,没定义。或者定义在另一个.cpp文件里,但CMakeLists.txt没把它加进去。

我的习惯:每次写JNI函数前,先在头文件里把函数声明写好,然后统一在CMakeLists.txt里用add_library把所有源文件列清楚。这样能避免漏文件的问题。

解决方案:

  1. 检查函数是否真的实现了
  2. 确认所有源文件都加到了add_library中
  3. 如果是第三方库,检查target_link_libraries是否正确
  4. 用nm命令查看目标文件或库的符号表:nm -C libnative.so | grep getSomeString

1.2 linker error:链接错误的深层原因

linker error比undefined reference更复杂。它可能涉及符号冲突、库版本不匹配、链接顺序问题等。

常见类型:

错误类型 原因 解决方案
multiple definition 同一个符号被定义了多次 检查是否有重复的源文件或静态库
cannot find -lxxx 链接器找不到指定的库 检查库路径和库名是否正确
relocation overflow 地址偏移超出范围 检查是否使用了不兼容的编译选项

我记得有一次,团队里一个同事把同一个静态库链接了两次,结果报multiple definition。排查了半天才发现是CMakeLists.txt里写重复了。

避坑指南:我曾经因为链接顺序问题折腾了一下午。GNU链接器对库的依赖顺序很敏感——被依赖的库要放在后面。比如libA依赖libB,那么链接顺序应该是:-lA -lB,而不是反过来。

1.3 ABI mismatch:架构不匹配的噩梦

ABI mismatch说白了,就是你编译的.so文件跟目标设备的CPU架构对不上。比如你用arm64-v8a的库,但设备是armeabi-v7a的。

为什么会发生?

  • CMakeLists.txt里只指定了一种ABI
  • 第三方库只提供了特定架构的版本
  • build.gradle里abiFilters配置不对

来看一个典型的配置:

// build.gradle
android {
    defaultConfig {
        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
        }
    }
}

如果你只编译了arm64-v8a,但设备是armeabi-v7a,运行时就会报错:java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol

我的建议:发布时尽量覆盖主流ABI。如果为了减小APK体积,可以只保留arm64-v8a和armeabi-v7a。但测试时一定要在真机上跑,模拟器经常用x86,容易忽略这个问题。

1.4 JNI FindClass失败:Java与C++的桥梁断了

FindClass失败是JNI开发中最常见的运行时错误之一。你明明在Java端定义了类,但C++里就是找不到。

原因分析:

  1. 类名写错了,注意JNI的类名格式是"包名/类名",不是"包名.类名"
  2. 线程问题:在非Java线程(比如C++创建的子线程)里调用FindClass,找不到类
  3. 类加载器问题:JNI_OnLoad里缓存了错误的JNIEnv指针

我遇到过最坑的一次,是在一个后台线程里调用了FindClass。当时百思不得其解,后来才发现JNI规范里明确说了:FindClass只能在Java线程或持有正确JNIEnv的线程里调用。

// 错误示例:在C++线程里直接调用FindClass
void* worker_thread(void* arg) {
    JNIEnv* env = nullptr;
    // 这里env是空的,或者是从其他地方拿来的无效指针
    jclass clazz = env->FindClass("com/example/MyClass");  // 失败!
    return nullptr;
}

// 正确做法:通过JavaVM获取当前线程的JNIEnv
void* worker_thread(void* arg) {
    JavaVM* vm = (JavaVM*)arg;
    JNIEnv* env = nullptr;
    vm->AttachCurrentThread(&env, nullptr);
    jclass clazz = env->FindClass("com/example/MyClass");  // 成功
    vm->DetachCurrentThread();
    return nullptr;
}

避坑指南:我曾经在JNI_OnLoad里缓存了JNIEnv指针,然后在其他线程里直接用。结果崩溃得一塌糊涂。记住:JNIEnv是线程相关的,不能跨线程使用。要跨线程操作,必须通过JavaVM来获取当前线程的JNIEnv。

1.5 内存泄漏检测:别让内存悄悄溜走

NDK开发中,内存泄漏是个隐形杀手。Java有GC帮你管,但C/C++得自己动手。我见过不少项目,功能跑得好好的,但内存占用越来越高,最后OOM崩溃。

常见泄漏场景:

  • new了对象但没delete
  • malloc了内存但没free
  • JNI的NewGlobalRef创建了全局引用但没DeleteGlobalRef
  • 打开的文件描述符没关闭

检测工具:

工具 适用场景 使用方法
AddressSanitizer (ASan) 开发调试阶段 在CMakeLists.txt中添加:set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
Valgrind Linux环境 通过adb在root设备上运行
LeakCanary Java层内存泄漏 集成到Android项目中

我个人最常用的是AddressSanitizer。它能在编译期插入检测代码,运行时一旦发现内存错误就立即报错,还能给出详细的调用栈。

// CMakeLists.txt 启用ASan
cmake_minimum_required(VERSION 3.10)
project("mynative")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fsanitize=address")

add_library(native-lib SHARED native-lib.cpp)

我的习惯:每次提交代码前,先用ASan跑一遍单元测试。虽然会拖慢运行速度,但能提前发现90%的内存问题。发布版本时再关掉ASan,不影响性能。

知识体系总览

下面这张图总结了本章的核心内容,方便你快速回顾:

NDK常见错误与解决方案知识体系 NDK开发常见错误 undefined reference linker error ABI mismatch JNI FindClass失败 内存泄漏检测 函数未实现 库路径错误 符号冲突 链接顺序 abiFilters配置 线程问题 类加载器 ASan检测 Valgrind 核心原则:编译期错误看符号,运行时错误看环境 遇到问题先看日志,再查配置,最后怀疑代码

好了,以上就是本章的全部内容。这五个问题,每一个我都踩过坑,也帮别人填过坑。你刚开始接触NDK时,可能会觉得这些错误很烦人。但相信我,等你把这些坑都趟一遍,后面的路就好走多了。

记住一点:遇到错误别慌,先看日志,再查配置,最后才怀疑代码。很多时候,问题出在CMakeLists.txt或者build.gradle里,而不是C++代码本身。

公众号:蓝海资料掘金营,微信deep3321