Android JNI 开发完全指南

13 阅读13分钟

Android JNI 开发完全指南

目录

  1. JNI 基础概念
  2. external 关键字详解
  3. JNI 命名规范
  4. so 库加载机制
  5. JVM 方法查找与缓存
  6. JNI 类型系统
  7. 内存管理
  8. 实战案例
  9. 最佳实践
  10. 常见问题

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" 的原因:

  1. C++ 支持函数重载,会修饰函数名
  2. JVM 需要精确匹配函数名
  3. 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++ 类型说明
Booleanjbooleanuint8_t0 或 1
Bytejbyteint8_t8 位有符号整数
Charjcharuint16_t16 位 Unicode 字符
Shortjshortint16_t16 位有符号整数
Intjintint32_t32 位有符号整数
Longjlongint64_t64 位有符号整数
Floatjfloatfloat32 位浮点数
Doublejdoubledouble64 位浮点数
Unit/voidvoidvoid无返回值

6.2 对象类型映射

Kotlin 类型JNI 类型说明
Stringjstring需要转换才能使用
Any / 对象jobject任意 Java 对象
Classjclass类对象
Array<T>jarray数组基类
ByteArrayjbyteArray字节数组
IntArrayjintArray整数数组
Object[]jobjectArray对象数组
Throwablejthrowable异常对象

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 开发的核心知识点:

  1. JNI 基础:理解 Kotlin 和 C/C++ 交互的桥梁
  2. external 关键字:声明 Native 方法的关键字
  3. 命名规范:Java 方法到 C 函数的映射规则
  4. 库加载机制:.so 文件如何被 JVM 加载和使用
  5. 方法查找缓存:JVM 如何高效查找 Native 方法
  6. 类型系统:Kotlin 和 C++ 类型的对应关系
  7. 内存管理:Local/Global/Weak 引用的生命周期
  8. 实战案例:完整的 Native 方法实现流程
  9. 最佳实践:命名、错误处理、性能优化
  10. 常见问题:错误排查和解决方案

掌握这些知识点,你就可以熟练地在 Android 项目中使用 JNI 进行高性能开发!