Android JNI开发环境搭建与Hello World实现
1. Android JNI开发环境搭建
在开始编写"Hello World"示例之前,我们需要先搭建好Android JNI开发环境。Android Studio是目前最主流的Android开发IDE,它内置了对NDK和CMake的支持,可以大大简化JNI开发流程。
1.1 安装必要组件
首先确保你已经安装了以下组件:
- Android Studio最新稳定版
- Android NDK(通过SDK Manager安装)
- CMake(通过SDK Manager安装)
在Android Studio中,打开SDK Manager,切换到"SDK Tools"标签页,勾选以下项目:
- NDK (Side by side)
- CMake
- LLDB(用于调试原生代码)
提示:建议选择较新的NDK版本,但不要使用预览版,以避免兼容性问题。目前稳定版本是NDK r21或r22。
1.2 创建支持C++的新项目
- 在Android Studio中选择"File > New > New Project"
- 选择"Native C++"模板
- 按照向导完成项目创建
这个模板会自动配置好CMake和NDK的基本设置,为我们生成一个简单的JNI示例代码框架。
2. JNI项目结构解析
让我们看一下自动生成的项目结构:
app/ ├── src/ │ ├── main/ │ │ ├── cpp/ # 原生代码目录 │ │ │ └── native-lib.cpp │ │ ├── java/ # Java代码目录 │ │ └── res/ │ └── build.gradle # 模块级构建配置 ├── CMakeLists.txt # CMake构建脚本 └── build.gradle # 项目级构建配置2.1 CMakeLists.txt详解
CMakeLists.txt是CMake的构建脚本,定义了如何编译原生代码。模板生成的CMakeLists.txt通常包含以下关键内容:
cmake_minimum_required(VERSION 3.4.1) # 指定CMake最低版本 # 定义库名称、类型和源文件 add_library( native-lib # 库名称 SHARED # 库类型:SHARED表示动态库 native-lib.cpp # 源文件 ) # 查找系统库 find_library( log-lib # 变量名 log # 要查找的库名称 ) # 链接库 target_link_libraries( native-lib # 目标库 ${log-lib} # 要链接的库 )2.2 build.gradle配置
模块级的build.gradle中包含了与NDK和CMake相关的配置:
android { defaultConfig { externalNativeBuild { cmake { cppFlags "" # 可添加C++编译选项 // 指定ABI过滤器 abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' } } } externalNativeBuild { cmake { path "CMakeLists.txt" # 指定CMake脚本路径 version "3.10.2" # 指定CMake版本 } } }3. 实现JNI Hello World
现在我们来编写一个简单的JNI "Hello World"示例。
3.1 Java层代码
在MainActivity.java中添加native方法声明:
public class MainActivity extends AppCompatActivity { static { System.loadLibrary("native-lib"); // 加载动态库 } // 声明native方法 public native String stringFromJNI(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.sample_text); tv.setText(stringFromJNI()); // 调用native方法 } }3.2 C++层实现
在native-lib.cpp中实现native方法:
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }3.3 JNI方法命名规则
JNI方法的命名遵循特定规则:
Java_包名_类名_方法名例如:
Java_com_example_myapplication_MainActivity_stringFromJNI注意:如果包名包含下划线(_),在JNI方法名中需要替换为"_1"。类似地,其他特殊字符也有对应的转义规则。
4. 构建和运行
4.1 构建过程
当你点击运行按钮时,Android Studio会执行以下步骤:
- 编译Java代码为DEX字节码
- 使用CMake和NDK编译C++代码为共享库(.so)
- 将所有资源打包成APK
- 安装APK到设备或模拟器
4.2 查看生成的.so文件
构建完成后,你可以在以下路径找到生成的.so文件:
app/build/intermediates/cmake/debug/obj/ ├── arm64-v8a/ │ └── libnative-lib.so ├── armeabi-v7a/ │ └── libnative-lib.so ├── x86/ │ └── libnative-lib.so └── x86_64/ └── libnative-lib.so5. 调试JNI代码
5.1 使用LLDB调试
Android Studio支持使用LLDB调试原生代码:
- 在C++代码中设置断点
- 选择"Debug"而非"Run"来启动应用
- 当执行到native方法时,调试器会停在断点处
5.2 常见调试技巧
- 使用
__android_log_print输出日志:
#include <android/log.h> #define LOG_TAG "JNI_DEBUG" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) LOGD("Debug message: %d", value);- 检查JNI方法签名是否正确
- 确保Java包名和类名与JNI方法中的完全匹配
- 使用
adb logcat查看原生层日志
6. 进阶配置
6.1 添加额外的C++源文件
要在项目中添加更多C++源文件:
- 在cpp目录下创建新文件
- 在CMakeLists.txt中添加源文件:
add_library( native-lib SHARED native-lib.cpp new-source.cpp # 新增的源文件 )6.2 使用第三方原生库
要使用预编译的第三方原生库:
- 将库文件(.so)放入src/main/jniLibs/对应ABI目录
- 在CMakeLists.txt中添加:
add_library( imported-lib SHARED IMPORTED ) set_target_properties( imported-lib PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libimported.so ) target_link_libraries( native-lib imported-lib )6.3 配置不同的构建类型
可以为debug和release构建配置不同的选项:
if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_definitions(-DDEBUG=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g") else() add_definitions(-DRELEASE=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") endif()7. 常见问题解决
7.1 UnsatisfiedLinkError
如果遇到java.lang.UnsatisfiedLinkError,可能的原因包括:
- 库名称不匹配(Java中加载的名称与CMake中定义的不一致)
- 库未正确打包到APK中
- ABI不兼容(设备CPU架构与构建的ABI不匹配)
解决方案:
- 检查System.loadLibrary()的参数
- 确认build.gradle中配置了正确的ABI过滤器
- 清理并重新构建项目
7.2 JNI方法找不到
如果native方法无法链接,可能的原因:
- 方法签名不正确
- 方法名拼写错误(注意包名、类名的大小写)
- 方法未正确导出(C++中需要使用extern "C")
解决方案:
- 使用
javah或javac -h生成正确的头文件 - 检查方法名是否完全匹配
- 确保C++函数使用了正确的JNIEXPORT和JNICALL修饰符
7.3 CMake配置错误
常见的CMake错误包括:
- CMake版本不兼容
- 路径配置错误
- 语法错误
解决方案:
- 检查CMakeLists.txt的语法
- 确认使用的CMake版本符合要求
- 查看Gradle Console中的详细错误信息
8. 性能优化建议
8.1 减少JNI调用
JNI调用开销较大,应尽量减少Java和原生层之间的频繁调用。最佳实践是:
- 将多个小操作合并为一个大操作
- 在原生层缓存Java对象和类引用
- 使用直接缓冲区(Direct Buffer)传递大量数据
8.2 内存管理
JNI编程中常见的内存问题:
- 未正确释放局部引用
- 全局引用泄漏
- 未检查JNI函数的返回值
最佳实践:
- 使用
DeleteLocalRef释放不再需要的局部引用 - 谨慎使用全局引用,确保在不再需要时释放
- 始终检查JNI函数的返回值
8.3 异常处理
JNI中的异常处理需要特别注意:
- 检查是否有未处理的Java异常
- 正确处理原生代码抛出的异常
- 在调用可能抛出异常的JNI函数后检查异常
示例代码:
jmethodID mid = env->GetMethodID(cls, "method", "()V"); if (env->ExceptionCheck()) { env->ExceptionDescribe(); // 打印异常信息 env->ExceptionClear(); // 清除异常 return; }9. 项目实战扩展
9.1 添加更多JNI方法
让我们扩展示例,添加一个计算斐波那契数列的native方法:
Java层:
public native long fibonacci(int n);C++层:
extern "C" JNIEXPORT jlong JNICALL Java_com_example_myapplication_MainActivity_fibonacci( JNIEnv* env, jobject thiz, jint n) { if (n <= 1) return n; return Java_com_example_myapplication_MainActivity_fibonacci(env, thiz, n-1) + Java_com_example_myapplication_MainActivity_fibonacci(env, thiz, n-2); }9.2 使用C++ STL
要在JNI中使用C++标准库,需要在CMakeLists.txt中配置:
# 使用静态链接的C++标准库 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") target_link_libraries( native-lib ${log-lib} android # 链接Android特定库 )9.3 回调Java方法
从原生代码调用Java方法的示例:
- 在Java类中定义回调方法:
public void onProgressUpdate(int progress) { runOnUiThread(() -> { progressBar.setProgress(progress); }); }- 在C++中调用回调方法:
// 获取类引用和方法ID(应该在合适的地方缓存这些引用) jclass clazz = env->GetObjectClass(thiz); jmethodID methodId = env->GetMethodID(clazz, "onProgressUpdate", "(I)V"); // 调用Java方法 env->CallVoidMethod(thiz, methodId, progressValue);10. 构建变体与ABI管理
10.1 配置构建变体
在build.gradle中可以为不同构建类型配置不同的CMake选项:
android { buildTypes { debug { externalNativeBuild { cmake { cppFlags "-DDEBUG=1" arguments "-DANDROID_ARM_MODE=arm" } } } release { externalNativeBuild { cmake { cppFlags "-DNDEBUG=1" arguments "-DANDROID_ARM_MODE=thumb" } } } } }10.2 ABI过滤
为了减小APK大小,可以只包含特定ABI的库:
android { defaultConfig { ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' // 只包含ARM架构 } } }或者为不同ABI配置不同选项:
android { flavorDimensions "abi" productFlavors { arm8 { dimension "abi" ndk { abiFilters 'arm64-v8a' } } arm7 { dimension "abi" ndk { abiFilters 'armeabi-v7a' } } } }11. 测试与持续集成
11.1 单元测试
可以为原生代码编写单元测试:
- 创建测试目录结构:
app/ ├── src/ │ ├── test/ # Java单元测试 │ ├── androidTest/ # 仪器化测试 │ └── testcpp/ # 原生单元测试 │ └── test/ │ └── test.cpp- 在CMakeLists.txt中添加测试配置:
enable_testing() add_executable( native-tests test/test.cpp ../main/cpp/native-lib.cpp ) target_link_libraries( native-tests ${log-lib} gtest ) add_test(NAME native-tests COMMAND native-tests)11.2 持续集成
在CI服务器上构建JNI项目的关键步骤:
- 安装必要的SDK组件
sdkmanager "cmake;3.10.2" "ndk;21.3.6528147"- 设置环境变量
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/21.3.6528147- 执行构建
./gradlew assembleDebug12. 安全最佳实践
12.1 输入验证
在JNI边界处必须严格验证输入:
- 检查指针是否为NULL
- 验证数组长度
- 检查字符串编码
示例:
extern "C" JNIEXPORT void JNICALL Java_com_example_myapplication_MainActivity_processData( JNIEnv* env, jobject thiz, jbyteArray data) { if (data == NULL) { env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "Input data cannot be null"); return; } jsize length = env->GetArrayLength(data); if (length <= 0) { return; // 空数组,无需处理 } // 处理数据... }12.2 敏感数据保护
处理敏感数据时的注意事项:
- 不要将敏感数据存储在Java字符串中
- 使用完后立即清除内存中的敏感数据
- 考虑使用Android的KeyStore系统
12.3 符号隐藏
为了安全性和减小库大小,应该隐藏不必要的符号:
在CMakeLists.txt中添加:
set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)或者在源代码中:
#define JNIEXPORT __attribute__ ((visibility ("default"))) #define JNIIMPORT __attribute__ ((visibility ("default")))13. 性能分析与优化
13.1 使用Simpleperf分析性能
Simpleperf是Android上的原生代码性能分析工具:
- 在设备上收集性能数据:
adb shell simpleperf record -p <pid> -o /data/local/tmp/perf.data --duration 10- 拉取分析数据:
adb pull /data/local/tmp/perf.data- 生成报告:
python $ANDROID_NDK_HOME/simpleperf/report.py -i perf.data -o report.txt13.2 关键优化技术
提高JNI性能的常见技术:
- 缓存方法ID和字段ID
- 使用局部引用而非全局引用
- 预计算JNI方法签名
- 使用GetPrimitiveArrayCritical处理大型数组
示例(缓存方法ID):
jclass nativeClass; jmethodID callbackMethodId; JNIEXPORT void JNICALL Java_com_example_myapplication_NativeClass_init( JNIEnv* env, jclass clazz) { nativeClass = (jclass)env->NewGlobalRef(clazz); callbackMethodId = env->GetStaticMethodID( nativeClass, "callback", "(I)V"); }14. 跨平台开发考虑
14.1 支持多平台
如果代码需要支持Android以外的平台,可以这样组织项目:
src/ ├── common/ # 平台无关代码 ├── android/ # Android特定实现 └── desktop/ # 桌面平台实现在CMake中使用条件编译:
if(ANDROID) add_subdirectory(android) else() add_subdirectory(desktop) endif()14.2 使用预处理器宏
使用宏处理平台差异:
#if defined(__ANDROID__) #include <android/log.h> #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "TAG", __VA_ARGS__) #else #include <stdio.h> #define LOGI(...) printf(__VA_ARGS__); printf("\n") #endif15. 资源管理与清理
15.1 JNI引用管理
JNI引用类型:
- 局部引用:在native方法返回后自动释放
- 全局引用:必须手动释放
- 弱全局引用:不会阻止GC回收对象
最佳实践:
// 创建全局引用 jclass globalClazz = (jclass)env->NewGlobalRef(localClazz); // 使用完后释放 env->DeleteGlobalRef(globalClazz);15.2 原生资源管理
管理原生资源的模式:
- 在Java类中使用long字段存储原生指针
- 提供native方法创建/销毁资源
- 在finalize()方法中确保资源释放
示例:
public class NativeResource { private long nativeHandle; public native void init(); public native void dispose(); protected void finalize() throws Throwable { try { dispose(); } finally { super.finalize(); } } }16. 高级主题
16.1 使用JNI_OnLoad
可以在库加载时初始化资源:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // 初始化工作... return JNI_VERSION_1_6; }16.2 动态注册native方法
除了使用静态方法名匹配,还可以动态注册方法:
static JNINativeMethod methods[] = { {"stringFromJNI", "()Ljava/lang/String;", (void*)stringFromJNI}, {"fibonacci", "(I)J", (void*)fibonacci} }; JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } jclass clazz = env->FindClass("com/example/myapplication/MainActivity"); env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof(JNINativeMethod)); return JNI_VERSION_1_6; }17. 工具与资源
17.1 实用工具
- javah:生成JNI头文件(较旧版本)
- javac -h:生成JNI头文件(JDK 8+)
- ndk-stack:将原生崩溃堆栈符号化
- addr2line:将地址转换为源代码行号
17.2 学习资源
- Android NDK官方文档
- JNI规范
- Google NDK示例
18. 项目打包与发布
18.1 减小APK大小
优化原生库大小的技术:
- 使用
-Oz优化标志(最大程度减小大小) - 去除调试符号(发布构建)
- 按ABI拆分APK
在CMake中配置:
if(CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Oz -flto") endif()18.2 App Bundle支持
Android App Bundle支持自动按设备配置分发原生库:
在build.gradle中启用:
android { bundle { language { enableSplit = true } density { enableSplit = true } abi { enableSplit = true } } }19. 兼容性考虑
19.1 多API级别支持
处理不同Android版本的兼容性问题:
- 检查
ANDROID_PLATFORM值 - 使用
__ANDROID_API__宏 - 为旧API提供替代实现
示例:
#if __ANDROID_API__ >= 21 // 使用新API #else // 使用兼容实现 #endif19.2 64位支持
从Android 5.0开始支持64位架构,需要注意:
- 确保为所有ABI构建库
- 检查指针大小相关的代码
- 测试32位和64位设备上的行为
20. 总结与后续学习
通过这个"Hello World"示例,我们学习了Android JNI开发的基础知识,包括:
- 搭建开发环境
- 配置CMake和NDK
- 编写Java和C++交互代码
- 调试和优化技巧
后续可以深入学习:
- 更复杂的JNI类型转换
- 多线程JNI编程
- 使用Android特定的原生API(如Bitmap、传感器等)
- 结合其他原生库(如OpenGL ES、Vulkan)