Android JNI 开发完全指南
目录
1. JNI 基础概念
1.1 什么是 JNI?
JNI (Java Native Interface) 是 Java 平台的标准编程接口,允许 Java 代码与使用其他编程语言(主要是 C/C++)编写的应用程序和库进行交互。
┌─────────────────────────────────────────────────────────┐
│ Java/Kotlin 层 │
│ ├─ Activity/Fragment │
│ ├─ 业务逻辑 │
│ └─ external fun nativeMethod() ← 调用 Native 方法 │
└─────────────────────────────────────────────────────────┘
↓ JNI
┌─────────────────────────────────────────────────────────┐
│ JNI 接口层 │
│ ├─ JNIEnv (JNI 环境指针) │
│ ├─ 类型转换 │
│ └─ 方法调用 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Native 层 (C/C++) │
│ ├─ 高性能计算 │
│ ├─ FFmpeg/OpenCV 等库 │
│ └─ 硬件加速 │
└─────────────────────────────────────────────────────────┘
1.2 为什么使用 JNI?
| 应用场景 | 说明 | 示例 |
|---|---|---|
| 性能优化 | C/C++ 执行效率高,适合计算密集型任务 | 图像处理、音视频编解码 |
| 复用现有库 | 使用成熟的 C/C++ 库 | FFmpeg、OpenCV、OpenGL |
| 硬件访问 | 某些硬件只提供 C 层接口 | 相机、传感器、NFC |
| 代码保护 | Native 代码反编译难度高 | 核心算法、加密逻辑 |
1.3 JNI 的组成部分
JNI 包含三个核心部分:
1. Kotlin/Java 代码
- 声明 native 方法
- 加载 Native 库
2. JNI 接口层
- JNIEnv *env:JNI 环境指针
- 类型转换函数
- 调用 Java 方法
3. Native 代码 (C/C++)
- 实现 native 方法
- 调用系统库
- 性能敏感操作
2. external 关键字详解
2.1 external 的含义
// Kotlin 层声明
private external fun compressImage(path: String): Boolean
// ^^^^^^^^
// 声明此方法的实现由 Native 代码提供
关键点:
- ❌ 不是直接调用 C 方法
- ✅ 是声明:"方法实现位于 Native 库中,JVM 会去找"
- ✅ 类似于接口声明,通过 JNI 命名规范关联到 C++ 实现
2.2 external 的工作流程
┌─────────────────────────────────────────────────────────┐
│ 步骤 1: Kotlin 代码 │
├─────────────────────────────────────────────────────────┤
│ external fun compressImage(path: String): Boolean │
│ // 只声明,不实现 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 2: 加载 Native 库 │
├─────────────────────────────────────────────────────────┤
│ System.loadLibrary("ffmpegandoridlibrary") │
│ // 加载 libffmpegandoridlibrary.so │
│ // dlopen() 系统调用打开 .so 文件 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 3: 运行时方法查找 │
├─────────────────────────────────────────────────────────┤
│ JVM 通过 JNI 命名规范查找: │
│ Java_com_package_Class_method_name │
│ │
│ 在 .so 符号表中查找对应函数 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 4: C++ 实现 │
├─────────────────────────────────────────────────────────┤
│ extern "C" │
│ JNIEXPORT jboolean JNICALL │
│ Java_com_package_Class_method_name(JNIEnv *env, ...) { │
│ // 具体实现代码 │
│ } │
└─────────────────────────────────────────────────────────┘
2.3 为什么用 external?
// 对比:普通方法 vs external 方法
// 普通方法 - 实现在 Kotlin 中
fun add(a: Int, b: Int): Int {
return a + b // 代码在这里
}
// external 方法 - 实现在 Native 中
external fun compressImage(path: String): Boolean
// 代码在 C++ 中,通过 JNI 调用
3. JNI 命名规范
3.1 命名规则
JNI 函数名必须严格按照以下格式:
Java_<包名路径>_<类名>_<方法名>
示例:
Kotlin: com.aykon.demo.ffmpegandoridlibrary.ImageCompress.compressImageFromPath()
│ │ │ └─────────┬────────┘
│ │ │ │
│ └─────────────────────┴─────────────────── 方法名
│ method_name
└────────────────────────────────────────────────────── 类名
Class
JNI 函数名:
Java_com_aykon_demo_ffmpegandoridlibrary_image_ImageCompress_compressImageFromPath
│ │ │ │ └─────────┬──────────────┘ └───────┬────────┘ └───────┬───────
│ │ │ │ │ │ │
│ │ │ └──────────────┴─────────────────────────┴──────────────── method name
│ │ └─────────────────────────────────────────────────────────── class name
│ └─────────────────────────────────────────────────────────────── package segments
└─────────────────────────────────────────────────────────────────── JNI prefix
3.2 命名转换规则
// Kotlin 代码
package com.example.myapp
class ImageProcessor {
external fun processImage(path: String): Boolean
}
// 对应的 C++ 函数名
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_myapp_ImageProcessor_processImage(
JNIEnv *env,
jobject thiz,
jstring path
) {
// 实现
}
3.3 包名转换规则
com.aykon.demo.ffmpegandoridlibrary
↓
com_aykon_demo_ffmpegandoridlibrary
↓ 全部用下划线替换点号
com_aykon_demo_ffmpegandoridlibrary
3.4 方法重载的处理
// Kotlin 中允许重载
external fun process(data: ByteArray)
external fun process(data: String)
// JNI 会添加签名后缀
Java_..._process__3_3B // ByteArray
Java_..._process__Ljava_lang_String_2 // String
// 建议:避免重载,使用不同方法名
external fun processBytes(data: ByteArray)
external fun processString(data: String)
3.5 extern "C" 的作用
// 不使用 extern "C"
JNIEXPORT jboolean JNICALL myFunction(...) // ❌ C++ 会修饰名称
// 符号表: _Z12myFunctionP7JNIEnv_P8_jstring
// 使用 extern "C"
extern "C" {
JNIEXPORT jboolean JNICALL myFunction(...) // ✅ 保持原名
// 符号表: myFunction
}
必须使用 extern "C" 的原因:
- C++ 支持函数重载,会修饰函数名
- JVM 需要精确匹配函数名
- C 语言不修饰名称,保证符号一致
4. so 库加载机制
4.1 加载过程
┌─────────────────────────────────────────────────────────┐
│ 1. System.loadLibrary() 调用 │
├─────────────────────────────────────────────────────────┤
│ System.loadLibrary("ffmpegandoridlibrary") │
│ │
│ 内部执行:Runtime.getRuntime().loadLibrary() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. 构造库文件名 │
├─────────────────────────────────────────────────────────┤
│ 输入: "ffmpegandoridlibrary" │
│ │
│ 添加前缀: lib + name + .so │
│ 结果: libffmpegandoridlibrary.so │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. 搜索库文件 │
├─────────────────────────────────────────────────────────┤
│ 按优先级搜索以下路径: │
│ │
│ 1. System.getProperty("java.library.path") │
│ 2. APK 中的 lib 目录 │
│ ├── lib/arm64-v8a/libxxx.so │
│ ├── lib/armeabi-v7a/libxxx.so │
│ └── lib/x86/libxxx.so │
│ 3. 系统库路径 /system/lib64 │
│ 4. 应用私有路径 /data/app/.../lib │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 加载库到内存 │
├─────────────────────────────────────────────────────────┤
│ 使用系统调用 dlopen() 打开 .so 文件 │
│ │
│ dlopen("libffmpegandoridlibrary.so", RTLD_LAZY) │
│ │
│ 执行: │
│ - 读取 .so 文件头(ELF 格式) │
│ - 加载代码段到内存 │
│ - 加载数据段到内存 │
│ - 解析符号表 │
│ - 重定位地址 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. 注册 JNI 方法 │
├─────────────────────────────────────────────────────────┤
│ JVM 扫描 .so 符号表,查找 JNI 函数 │
│ │
│ 模式 1: 自动命名规范查找 │
│ Java_..._method_name │
│ │
│ 模式 2: JNI_OnLoad 手动注册 │
│ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, ...) { │
│ // 注册函数 │
│ } │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 6. 加载完成 │
├─────────────────────────────────────────────────────────┤
│ 库已加载,可以调用 external 方法 │
└─────────────────────────────────────────────────────────┘
4.2 多库加载示例
// 加载多个 Native 库
class NativeLib {
companion object {
init {
// 按依赖顺序加载
System.loadLibrary("avutil") // FFmpeg 工具库
System.loadLibrary("avcodec") // FFmpeg 编解码库
System.loadLibrary("avformat") // FFmpeg 格式库
System.loadLibrary("swscale") // FFmpeg 缩放库
System.loadLibrary("ffmpegandroid") // 主库
}
}
}
4.3 CMake 配置
# CMakeLists.txt
# 添加 FFmpeg 预编译库
add_library(libavutil SHARED IMPORTED)
set_target_properties(libavutil PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/lib/${CMAKE_ANDROID_ARCH_ABI}/libavutil.so)
# 编译主库
add_library(ffmpegandroid SHARED
ffmpegandoridlibrary.cpp
imageprocessor.cpp
)
# 链接库
target_link_libraries(ffmpegandroid
libavutil
libavcodec
libavformat
log # Android 日志库
android # Android 原生库
)
4.4 库的依赖关系
主库: libffmpegandoridlibrary.so
↓ 依赖
├─ libavutil.so (工具函数)
├─ libavcodec.so (编解码器)
├─ libavformat.so (格式处理)
├─ libswscale.so (图像缩放)
└─ libswresample.so (音频重采样)
所有库都会打包到 APK 中:
APK/
└─ lib/
├─ arm64-v8a/
│ ├─ libffmpegandoridlibrary.so
│ ├─ libavutil.so
│ └─ ...
└─ armeabi-v7a/
└─ ...
5. JVM 方法查找与缓存
5.1 全局缓存架构
┌─────────────────────────────────────────────────────────────────┐
│ JVM Native 方法管理 (每个 ClassLoader 一个实例) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 已加载的 .so 库列表 │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ [0] libffmpegandoridlibrary.so ← 主库 │ │ │
│ │ │ [1] libavutil.so │ │ │
│ │ │ [2] libavcodec.so │ │ │
│ │ │ [3] libswscale.so │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 方法缓存表 (全局哈希表) │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Key: (类名 + 方法名 + 签名) │ │ │
│ │ │ Value: (库句柄 + 函数指针) │ │ │
│ │ ├────────────────────────────────────────────────────┤ │ │
│ │ │ "NativeLib.stringFromJNI()V" │ │ │
│ │ │ → (lib[0], 0x12345678) │ │ │
│ │ │ │ │ │
│ │ │ "ImageCompress.compressImage(Ljava/lang/String;)Z" │ │ │
│ │ │ → (lib[0], 0x34567890) │ │ │
│ │ │ │ │ │
│ │ │ "ImageCompress.getMetadata(Ljava/lang/String;)..." │ │ │
│ │ │ → (lib[0], 0x78901234) │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 方法查找流程
第一次调用 external 方法:
┌─────────────────────────────────────────────────────────┐
│ 调用: stringFromJNI() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 1: 检查缓存 │
├─────────────────────────────────────────────────────────┤
│ 查找 Key: "NativeLib.stringFromJNI()V" │
│ 结果: 未找到 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 2: 构造 JNI 函数名 │
├─────────────────────────────────────────────────────────┤
│ Java_com_aykon_demo_..._NativeLib_stringFromJNI │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 3: 遍历已加载库查找 │
├─────────────────────────────────────────────────────────┤
│ 库[0]: libffmpegandoridlibrary.so │
│ dlsym("Java_..._stringFromJNI") │
│ ✅ 找到函数指针: 0x12345678 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 4: 存入缓存 │
├─────────────────────────────────────────────────────────┤
│ 缓存["NativeLib.stringFromJNI()V"] = (lib[0], 0x12345678) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤 5: 执行函数 │
├─────────────────────────────────────────────────────────┤
│ 跳转到函数指针 0x12345678 执行 │
└─────────────────────────────────────────────────────────┘
第二次调用同一方法:
┌─────────────────────────────────────────────────────────┐
│ 调用: stringFromJNI() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 检查缓存: 命中! │
│ 直接返回函数指针: 0x12345678 ← 非常快! │
└─────────────────────────────────────────────────────────┘
5.3 性能对比
| 操作 | 第一次调用 | 后续调用 |
|---|---|---|
| 查找方式 | 遍历符号表 O(n) | 哈希表查找 O(1) |
| 性能 | 较慢(微秒级) | 极快(纳秒级) |
| 缓存 | 否 | 是 |
5.4 多个库的查找顺序
// 如果有多个库都实现了同名函数(虽然不推荐)
// 加载顺序
System.loadLibrary("libraryA") // [0]
System.loadLibrary("libraryB") // [1]
System.loadLibrary("libraryC") // [2]
// 查找顺序
调用 external method()
→ 查找 libraryA ✅ 找到就使用
→ 否则查找 libraryB
→ 否则查找 libraryC
→ 都找不到 → UnsatisfiedLinkError
6. JNI 类型系统
6.1 基本类型映射
| Kotlin 类型 | JNI 类型 | C++ 类型 | 说明 |
|---|---|---|---|
Boolean | jboolean | uint8_t | 0 或 1 |
Byte | jbyte | int8_t | 8 位有符号整数 |
Char | jchar | uint16_t | 16 位 Unicode 字符 |
Short | jshort | int16_t | 16 位有符号整数 |
Int | jint | int32_t | 32 位有符号整数 |
Long | jlong | int64_t | 64 位有符号整数 |
Float | jfloat | float | 32 位浮点数 |
Double | jdouble | double | 64 位浮点数 |
Unit/void | void | void | 无返回值 |
6.2 对象类型映射
| Kotlin 类型 | JNI 类型 | 说明 |
|---|---|---|
String | jstring | 需要转换才能使用 |
Any / 对象 | jobject | 任意 Java 对象 |
Class | jclass | 类对象 |
Array<T> | jarray | 数组基类 |
ByteArray | jbyteArray | 字节数组 |
IntArray | jintArray | 整数数组 |
Object[] | jobjectArray | 对象数组 |
Throwable | jthrowable | 异常对象 |
6.3 JNI 函数签名
JNI 方法签名用于唯一标识方法,格式:(参数类型...)返回类型
基础类型签名:
B - byte
C - char
D - double
F - float
I - int
J - long
S - short
Z - boolean
V - void
对象类型签名:
L包名/类名; 例如:Ljava/lang/String;
数组类型签名:
[类型 例如:[I 表示 int[]
[[I 表示 int[][]
示例:
// Kotlin 方法签名
fun process(input: String, count: Int, flag: Boolean): String
对应的 JNI 签名:
(Ljava/lang/String;IZ)Ljava/lang/String;
│ │ │ │ │
│ │ │ │ └─ 返回 String
│ │ │ └───────────── Boolean
│ │ └─────────────── Int
│ └───────────────── String 参数
└─────────────────────────── 方法开始
复杂示例:
fun example(
str: String,
bytes: ByteArray,
listener: MyListener,
count: Int
): Boolean
JNI 签名:
(Ljava/lang/String;[BLcom/example/MyListener;I)Z
│ │ │ │ └─ 返回 boolean
│ │ │ └─────── Int
│ │ └───────────────────────── Listener 对象
│ └──────────────────────────── ByteArray
└─────────────────────────────────────── String
6.4 JNIEnv 接口
JNIEnv 是指向 JNI 函数表的指针,提供了大量操作函数:
struct JNIEnv {
const struct JNINativeInterface* functions;
// 版本信息
jint GetVersion();
// 类操作
jclass FindClass(const char* name);
// 对象操作
jobject AllocObject(jclass clazz);
jobject NewObject(jclass clazz, jmethodID methodID, ...);
// 方法操作
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig);
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig);
// 字段操作
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig);
jint GetIntField(jobject obj, jfieldID fieldID);
// 字符串操作
jstring NewStringUTF(const char* str);
const char* GetStringUTFChars(jstring str, jboolean* isCopy);
void ReleaseStringUTFChars(jstring str, const char* utf);
// 数组操作
jsize GetArrayLength(jarray array);
jint* GetIntArrayElements(jintArray array, jboolean* isCopy);
void ReleaseIntArrayElements(jintArray array, jint* elems, jint mode);
// 异常处理
jboolean ExceptionCheck();
void ExceptionDescribe();
void ExceptionClear();
jint ThrowNew(jclass clazz, const char* msg);
// 内存管理
void DeleteLocalRef(jobject localRef);
jobject NewGlobalRef(jobject obj);
void DeleteGlobalRef(jobject globalRef);
};
7. 内存管理
7.1 JNI 内存类型
JNI 中有三种引用类型:
1. Local Reference (局部引用)
- 生命周期:仅在当前 Native 方法执行期间
- 自动释放:方法返回时自动释放
- 创建方式:大部分 JNI 函数返回的引用
- 示例:jstring, jobject, jclass
2. Global Reference (全局引用)
- 生命周期:显式释放之前一直有效
- 手动释放:必须调用 DeleteGlobalRef()
- 创建方式:NewGlobalRef()
- 用途:跨方法、跨线程共享
3. Weak Global Reference (弱全局引用)
- 生命周期:GC 可以随时回收
- 手动释放:建议调用 DeleteWeakGlobalRef()
- 创建方式:NewWeakGlobalRef()
- 用途:缓存,不强引用
7.2 局部引用管理
// ✅ 正确:短生命周期方法
extern "C"
JNIEXPORT jstring JNICALL
Java_..._simpleMethod(JNIEnv *env, jobject thiz) {
jstring str = env->NewStringUTF("Hello"); // 局部引用
// ... 使用 str ...
return str; // 自动释放
}
// ❌ 错误:循环中创建大量局部引用
extern "C"
JNIEXPORT void JNICALL
Java_..._wrongMethod(JNIEnv *env, jobject thiz, jint count) {
for (int i = 0; i < 10000; i++) {
jstring str = env->NewStringUTF("test"); // 创建 10000 个!
// 可能导致局部引用表溢出
}
}
// ✅ 正确:循环中手动释放
extern "C"
JNIEXPORT void JNICALL
Java_..._correctMethod(JNIEnv *env, jobject thiz, jint count) {
for (int i = 0; i < 10000; i++) {
jstring str = env->NewStringUTF("test");
// ... 使用 str ...
env->DeleteLocalRef(str); // 立即释放
}
}
7.3 字符串内存管理
// GetStringUTFChars / ReleaseStringUTFChars 成对使用
extern "C"
JNIEXPORT void JNICALL
Java_..._processString(JNIEnv *env, jobject thiz, jstring input) {
const char *cStr = NULL; // 初始化为 NULL
// 转换:jstring → C 字符串
cStr = env->GetStringUTFChars(input, NULL);
if (cStr == NULL) {
return; // 内存不足
}
// 使用 C 字符串
printf("Input: %s\n", cStr);
// 必须释放!
env->ReleaseStringUTFChars(input, cStr);
}
7.4 数组内存管理
extern "C"
JNIEXPORT jint JNICALL
Java_..._sumArray(JNIEnv *env, jobject thiz, jintArray array) {
jint *cArray = NULL;
jint sum = 0;
// 获取数组元素
cArray = env->GetIntArrayElements(array, NULL);
if (cArray == NULL) {
return 0; // 失败
}
// 获取长度
jsize length = env->GetArrayLength(array);
// 处理数组
for (int i = 0; i < length; i++) {
sum += cArray[i];
}
// 释放数组
// mode: 0 = 复制回去并释放,JNI_ABORT = 不复制直接释放
env->ReleaseIntArrayElements(array, cArray, 0);
return sum;
}
7.5 全局引用示例
// 全局变量存储缓存
static jclass gStringClass = NULL;
static jmethodID gStringConstructor = NULL;
// 初始化函数
extern "C"
JNIEXPORT void JNICALL
Java_..._initCache(JNIEnv *env, jobject thiz) {
// 查找类
jclass localClass = env->FindClass("java/lang/String");
// 创建全局引用
gStringClass = (jclass)env->NewGlobalRef(localClass);
// 删除局部引用(已有全局引用,不需要局部了)
env->DeleteLocalRef(localClass);
// 获取方法 ID(不需要释放)
gStringConstructor = env->GetMethodID(gStringClass, "<init>", "([B)V");
}
// 使用缓存的引用
extern "C"
JNIEXPORT jstring JNICALL
Java_..._createString(JNIEnv *env, jobject thiz, jbyteArray bytes) {
// 使用全局引用(无需再次查找)
return (jstring)env->NewObject(gStringClass, gStringConstructor, bytes);
}
// 清理函数
extern "C"
JNIEXPORT void JNICALL
Java_..._cleanup(JNIEnv *env, jobject thiz) {
if (gStringClass != NULL) {
env->DeleteGlobalRef(gStringClass);
gStringClass = NULL;
}
}
8. 实战案例
8.1 添加 Native 方法完整流程
场景:实现获取图片元数据的 Native 方法
步骤 1:定义 Kotlin 数据类
// ImageMetadata.kt
package com.example.ffmpeg
data class ImageMetadata(
val width: Int,
val height: Int,
val format: String,
val fileSize: Long
) {
fun getResolution(): String = "${width}x${height}"
fun getReadableSize(): String {
return when {
fileSize < 1024 -> "${fileSize}B"
fileSize < 1024 * 1024 -> "${fileSize / 1024}KB"
else -> "${fileSize / (1024 * 1024)}MB"
}
}
}
步骤 2:在接口中声明方法
// IImageCompress.kt
interface IImageCompress {
fun getMetadata(imagePath: String): ImageMetadata?
}
步骤 3:在实现类中调用 Native
// ImageCompress.kt
class ImageCompress : IImageCompress {
override fun getMetadata(imagePath: String): ImageMetadata? {
if (imagePath.isEmpty()) return null
val file = File(imagePath)
if (!file.exists()) return null
// 调用 Native 方法
return getImageMetadataNative(imagePath)
}
// 声明 Native 方法
private external fun getImageMetadataNative(path: String): ImageMetadata?
}
步骤 4:实现 C++ JNI 函数
// ffmpegandoridlibrary.cpp
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ffmpeg_ImageCompress_getImageMetadataNative(
JNIEnv *env,
jobject thiz,
jstring imagePath) {
const char *cPath = NULL;
AVFormatContext *formatCtx = NULL;
jobject metadataObj = NULL;
// 1. 验证输入
if (imagePath == NULL) {
return NULL;
}
// 2. 转换字符串
cPath = env->GetStringUTFChars(imagePath, NULL);
if (cPath == NULL) {
return NULL;
}
// 3. 打开文件(使用 FFmpeg)
if (avformat_open_input(&formatCtx, cPath, NULL, NULL) != 0) {
goto CLEANUP;
}
if (avformat_find_stream_info(formatCtx, NULL) < 0) {
goto CLEANUP;
}
// 4. 获取元数据
int videoStreamIndex = av_find_best_stream(
formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0
);
if (videoStreamIndex < 0) {
goto CLEANUP;
}
AVStream *stream = formatCtx->streams[videoStreamIndex];
AVCodecParameters *codecPar = stream->codecpar;
// 获取文件大小
int64_t fileSize = 0;
FILE *file = fopen(cPath, "rb");
if (file) {
fseek(file, 0, SEEK_END);
fileSize = ftell(file);
fclose(file);
}
// 5. 创建 Kotlin 对象
jclass metadataClass = env->FindClass("com/example/ffmpeg/ImageMetadata");
if (metadataClass == NULL) {
goto CLEANUP;
}
// 查找构造函数: (I I J Ljava/lang/String; J)
jmethodID constructor = env->GetMethodID(
metadataClass, "<init>", "(IILjava/lang/String;J)V"
);
jstring formatStr = env->NewStringUTF("jpeg");
metadataObj = env->NewObject(
metadataClass, constructor,
codecPar->width, // Int width
codecPar->height, // Int height
formatStr, // String format
(jlong)fileSize // Long fileSize
);
// 6. 清理局部引用
env->DeleteLocalRef(formatStr);
env->DeleteLocalRef(metadataClass);
CLEANUP:
// 7. 释放资源
if (formatCtx) {
avformat_close_input(&formatCtx);
}
if (cPath) {
env->ReleaseStringUTFChars(imagePath, cPath);
}
return metadataObj;
}
步骤 5:使用方法
// 在 Kotlin 代码中使用
val metadata = FFmpegCompress.getImageCompress()
.getMetadata("/sdcard/photo.jpg")
metadata?.let {
println("分辨率: ${it.getResolution()}")
println("格式: ${it.format}")
println("大小: ${it.getReadableSize()}")
}
8.2 回调机制实现
// C++ 端:回调 Java 方法
// 1. 定义回调接口方法
jmethodID onProgressMethod = env->GetMethodID(
listenerClass,
"onProgress",
"(II)V" // (int, int) -> void
);
// 2. 调用回调
env->CallVoidMethod(listener, onProgressMethod, 50, 100);
// 3. 检查异常
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
// Kotlin 端:定义回调接口
interface ProgressListener {
fun onProgress(current: Int, total: Int)
}
// 使用
comressor.enqueue(object : ProgressListener {
override fun onProgress(current: Int, total: Int) {
println("进度: $current / $total")
}
})
9. 最佳实践
9.1 命名规范
// ✅ 推荐:避免重载,使用明确的方法名
external fun processImage(path: String): Boolean
external fun processImageBytes(data: ByteArray): Boolean
// ❌ 不推荐:重载会导致签名复杂
external fun process(data: String): Boolean
external fun process(data: ByteArray): Boolean
9.2 错误处理
// ✅ 推荐:统一清理标签
extern "C"
JNIEXPORT jboolean JNICALL
Java_..._safeMethod(JNIEnv *env, jobject thiz, jstring input) {
const char *cInput = NULL;
jboolean result = JNI_FALSE;
// 参数验证
if (input == NULL) {
return JNI_FALSE;
}
// 资源获取
cInput = env->GetStringUTFChars(input, NULL);
if (cInput == NULL) {
return JNI_FALSE; // OOM
}
// 业务逻辑
if (doWork(cInput) != 0) {
// 抛出异常
jclass exc = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(exc, "Operation failed");
env->DeleteLocalRef(exc);
goto CLEANUP;
}
result = JNI_TRUE;
CLEANUP:
// 统一清理
if (cInput != NULL) {
env->ReleaseStringUTFChars(input, cInput);
}
return result;
}
9.3 性能优化
// ✅ 推荐:缓存 jclass 和 jmethodID
static jclass gCachedClass = NULL;
static jmethodID gCachedMethod = NULL;
// 初始化时缓存
void initCache(JNIEnv *env) {
jclass local = env->FindClass("java/lang/String");
gCachedClass = (jclass)env->NewGlobalRef(local);
gCachedMethod = env->GetMethodID(gCachedClass, "getBytes", "()[B");
env->DeleteLocalRef(local);
}
// 使用时直接使用缓存
jbyteArray stringToBytes(JNIEnv *env, jstring str) {
return (jbyteArray)env->CallObjectMethod(str, gCachedMethod);
}
9.4 线程安全
// ✅ JNI 方法本身是线程安全的
// 但共享数据需要加锁
static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER;
extern "C"
JNIEXPORT void JNICALL
Java_..._threadSafeMethod(JNIEnv *env, jobject thiz) {
// 加锁
pthread_mutex_lock(&gLock);
// 访问共享资源
// ...
// 解锁
pthread_mutex_unlock(&gLock);
}
9.5 调试技巧
// Android 日志输出
#include <android/log.h>
#define LOG_TAG "FFmpegNative"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C"
JNIEXPORT void JNICALL
Java_..._debugMethod(JNIEnv *env, jobject thiz) {
LOGI("Method called");
LOGE("Error occurred: %s", "details");
}
10. 常见问题
10.1 UnsatisfiedLinkError
错误:java.lang.UnsatisfiedLinkError: No implementation found for method
原因:
1. JNI 函数名不匹配
2. 没有加载 .so 库
3. .so 库架构不匹配
4. extern "C" 缺失
解决:
1. 检查函数名是否完全匹配
2. 确保调用 System.loadLibrary()
3. 检查 CPU 架构(arm64-v8a, armeabi-v7a)
4. 添加 extern "C"
10.2 内存泄漏
问题:GetStringUTFChars 后没有 Release
解决:使用 goto CLEANUP 模式
10.3 局部引用溢出
问题:循环中创建大量局部引用
解决:及时 DeleteLocalRef()
10.4 多线程问题
问题:JNIEnv 不能跨线程使用
解决:每个线程通过 JavaVM 获取自己的 JNIEnv
JavaVM *gVM = NULL;
// JNI_OnLoad 保存 JavaVM
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
gVM = vm;
return JNI_VERSION_1_6;
}
// 在新线程中获取 JNIEnv
JNIEnv* getJNIEnv() {
JNIEnv *env = NULL;
gVM->GetEnv((void**)&env, JNI_VERSION_1_6);
if (env == NULL) {
// 线程附加
gVM->AttachCurrentThread(&env, NULL);
}
return env;
}
总结
本文档涵盖了 Android JNI 开发的核心知识点:
- JNI 基础:理解 Kotlin 和 C/C++ 交互的桥梁
- external 关键字:声明 Native 方法的关键字
- 命名规范:Java 方法到 C 函数的映射规则
- 库加载机制:.so 文件如何被 JVM 加载和使用
- 方法查找缓存:JVM 如何高效查找 Native 方法
- 类型系统:Kotlin 和 C++ 类型的对应关系
- 内存管理:Local/Global/Weak 引用的生命周期
- 实战案例:完整的 Native 方法实现流程
- 最佳实践:命名、错误处理、性能优化
- 常见问题:错误排查和解决方案
掌握这些知识点,你就可以熟练地在 Android 项目中使用 JNI 进行高性能开发!