
1. 为什么需要JNI开发在Android开发中我们经常会遇到需要直接调用C/C代码的场景。Java虚拟机(JVM)虽然功能强大但在某些特定领域存在性能瓶颈比如高性能图形处理OpenGL数学密集型计算机器学习、图像处理重用现有的C/C库底层硬件操作JNI(Java Native Interface)正是为了解决这些问题而生的桥梁技术。它允许Java代码与本地代码主要是C/C相互调用既保留了Java的开发效率又能获得本地代码的执行性能。提示虽然JNI能带来性能提升但也会增加开发复杂度。建议只在确实需要时才使用JNI比如处理计算密集型任务或复用现有C/C库。2. 环境准备与工具链配置2.1 安装NDK工具包Android NDK(Native Development Kit)是进行JNI开发的必备工具包它包含了交叉编译器将C/C代码编译为Android可执行文件平台库提供对Android系统功能的访问构建工具在Android Studio中安装NDK打开SDK ManagerFile Settings Appearance Behavior System Settings Android SDK切换到SDK Tools标签页勾选NDK (Side by side)和CMake点击Apply进行安装2.2 配置CMake构建系统现代Android项目推荐使用CMake作为JNI项目的构建系统。CMake是一个跨平台的构建工具通过CMakeLists.txt文件来描述构建过程。典型的CMakeLists.txt配置示例cmake_minimum_required(VERSION 3.4.1) # 定义库名称和类型 add_library( # 设置库名称 hello-jni # 设置库类型 SHARED # 提供源文件路径 src/main/cpp/hello-jni.cpp ) # 查找系统库 find_library( # 设置路径变量名称 log-lib # 指定要查找的NDK库名称 log ) # 链接库到目标 target_link_libraries( # 指定目标库 hello-jni # 链接日志库到目标库 ${log-lib} )3. 实现第一个JNI程序Hello World3.1 创建Java本地方法声明首先在Java类中声明native方法public class HelloJNI { // 加载本地库 static { System.loadLibrary(hello-jni); } // 声明native方法 public native String stringFromJNI(); }关键点说明System.loadLibrary()用于加载编译后的本地库不需要加lib前缀和.so后缀native关键字表示这是一个JNI方法实现在本地代码中3.2 生成JNI头文件使用javac和javah工具生成头文件现代Android Studio会自动处理这一步javac HelloJNI.java javah -jni HelloJNI这会生成一个HelloJNI.h头文件包含类似下面的函数声明/* DO NOT EDIT THIS FILE - it is machine generated */ #include jni.h /* Header for class HelloJNI */ #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern C { #endif /* * Class: HelloJNI * Method: stringFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_HelloJNI_stringFromJNI (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif3.3 实现C函数创建hello-jni.cpp文件实现函数#include jni.h #include string extern C JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJNI_stringFromJNI(JNIEnv* env, jobject /* this */) { std::string hello Hello from C; return env-NewStringUTF(hello.c_str()); }关键点说明函数名必须严格遵循Java_完整类名_方法名的格式JNIEnv*指针提供了访问JVM功能的接口jstring是Java String类型在JNI中的对应类型NewStringUTF将C字符串转换为Java字符串4. 构建与调试技巧4.1 配置build.gradle在模块的build.gradle中添加CMake配置android { defaultConfig { externalNativeBuild { cmake { cppFlags -stdc14 arguments -DANDROID_STLc_shared } } } externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt version 3.10.2 } } }4.2 常见构建问题解决ABI不兼容错误现象java.lang.UnsatisfiedLinkError: dlopen failed解决检查gradle中配置的ABI过滤器是否与设备匹配未找到符号错误现象undefined reference to...解决确保所有依赖库都已正确链接检查CMakeLists.txt中的target_link_librariesJNI方法签名不匹配现象java.lang.NoSuchMethodError解决使用javap -s检查方法签名是否一致4.3 调试技巧使用Android Studio的LLDB调试器调试本地代码添加日志输出#include android/log.h #define LOG_TAG JNI_DEBUG #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) LOGD(调试信息: %s, someString);检查崩溃日志adb logcat查看native崩溃信息5. JNI开发进阶技巧5.1 数据类型转换JNI中常见的数据类型转换Java类型JNI类型C/C类型booleanjbooleanunsigned charbytejbytesigned charcharjcharunsigned shortshortjshortshortintjintintlongjlonglong longfloatjfloatfloatdoublejdoubledoubleStringjstringconst char*ObjectjobjectN/A5.2 异常处理JNI中的异常处理需要特别注意// 检查是否发生异常 if (env-ExceptionCheck()) { env-ExceptionDescribe(); // 打印异常信息到logcat env-ExceptionClear(); // 清除异常 return NULL; } // 抛出新的异常 jclass exClass env-FindClass(java/lang/IllegalArgumentException); env-ThrowNew(exClass, 参数无效);5.3 性能优化建议减少JNI调用次数每次JNI调用都有开销尽量批量处理数据缓存字段ID和方法ID避免重复查找// 类加载时缓存 jclass cls env-FindClass(com/example/MyClass); jmethodID mid env-GetMethodID(cls, myMethod, ()V); // 后续直接使用缓存的mid使用直接缓冲区对于大量数据传输使用ByteBuffer.allocateDirect()避免全局引用泄漏及时删除不再需要的全局引用6. 实际项目中的JNI应用6.1 图像处理示例以下是一个简单的图像处理JNI示例将图像转为灰度extern C JNIEXPORT void JNICALL Java_com_example_imageproc_ImageProcessor_grayscale( JNIEnv* env, jobject thiz, jobject bitmap) { AndroidBitmapInfo info; void* pixels; // 获取位图信息 if (AndroidBitmap_getInfo(env, bitmap, info) 0) { return; } // 锁定像素数据 if (AndroidBitmap_lockPixels(env, bitmap, pixels) 0) { return; } // 处理像素 uint32_t* pixelPtr (uint32_t*)pixels; for (int y 0; y info.height; y) { for (int x 0; x info.width; x) { uint32_t pixel pixelPtr[y * info.width x]; // 提取RGB分量 uint8_t a (pixel 24) 0xff; uint8_t r (pixel 16) 0xff; uint8_t g (pixel 8) 0xff; uint8_t b pixel 0xff; // 计算灰度值 uint8_t gray (uint8_t)(0.299 * r 0.587 * g 0.114 * b); // 设置新像素值 pixelPtr[y * info.width x] (a 24) | (gray 16) | (gray 8) | gray; } } // 解锁像素数据 AndroidBitmap_unlockPixels(env, bitmap); }6.2 多线程注意事项在JNI中处理多线程需要特别注意JNIEnv与线程每个线程需要获取自己的JNIEnv指针JavaVM* gJavaVM; // 在JNI_OnLoad中保存 void* threadFunc(void* arg) { JNIEnv* env; gJavaVM-AttachCurrentThread(env, NULL); // 在这里使用env gJavaVM-DetachCurrentThread(); return NULL; }同步访问对Java对象的访问需要适当同步全局引用管理跨线程使用的引用必须是全局引用7. 现代Android JNI开发最佳实践7.1 使用C17/20特性现代C提供了许多有用的特性// 使用智能指针管理资源 auto buffer std::make_uniqueuint8_t[](size); // 使用lambda简化回调 auto callback [env](jobject listener) { jclass cls env-GetObjectClass(listener); jmethodID mid env-GetMethodID(cls, onComplete, ()V); env-CallVoidMethod(listener, mid); };7.2 模块化设计将JNI代码组织为清晰的模块jni/ ├── CMakeLists.txt ├── common/ # 公共工具类 │ ├── jni_utils.cpp │ └── jni_utils.h ├── imageproc/ # 图像处理模块 │ ├── image_proc.cpp │ └── image_proc.h └── audio/ # 音频处理模块 ├── audio_proc.cpp └── audio_proc.h7.3 单元测试为JNI代码编写单元测试#include gtest/gtest.h #include my_jni_code.h TEST(JNITest, StringConversion) { JavaVM* jvm; JNIEnv* env; // 初始化JVM... const char* testStr Hello JNI; jstring jstr env-NewStringUTF(testStr); const char* backStr env-GetStringUTFChars(jstr, NULL); ASSERT_STREQ(testStr, backStr); env-ReleaseStringUTFChars(jstr, backStr); }在Android项目中可以使用Google Test框架来测试本地代码。8. 从Hello World到生产级JNI代码从简单的示例到生产级代码还需要考虑以下方面错误处理建立统一的错误处理机制日志系统实现分级的日志输出性能监控添加性能统计代码内存管理严格管理本地和Java对象引用ABI兼容性确保支持所有目标架构API设计提供清晰简洁的Java接口一个健壮的JNI模块通常包含以下组件清晰的Java API层健壮的JNI桥接层高效的核心C/C实现完善的构建和测试系统详细的文档和示例在实际项目中我通常会为每个JNI模块创建详细的README包含功能描述API文档构建说明常见问题性能指标这种文档习惯可以显著提高团队协作效率特别是在多人参与的大型项目中。