Android JNI 语法全解析:从基础到实战

在 Android 开发中,有些场景需要借助 C/C++ 实现 ------ 例如处理复杂算法(如音视频编解码)、调用硬件驱动、优化性能敏感模块。JNI(Java Native Interface)作为 Java 与 C/C++ 的桥梁,是实现这一需求的核心技术。但 JNI 语法复杂、内存管理严格,稍有不慎就会导致崩溃或内存泄漏,让很多开发者望而却步。

本文将从 JNI 的基础概念讲起,系统梳理核心语法(数据类型、方法注册、内存操作),通过实例解析 Java 与 C 的交互流程,并总结常见错误及优化技巧,帮你轻松掌握 JNI 开发。

一、JNI 核心概念:为什么需要 JNI?

1.1 JNI 的作用与适用场景

JNI 是 Java 调用原生代码(C/C++)的接口规范,其核心价值在于 "扬长避短"------ 让 Java 的便捷性与 C/C++ 的高性能结合:

  • Java 的优势:开发效率高、内存管理自动化、跨平台;
  • C/C++ 的优势:执行速度快(适合计算密集型任务)、可直接操作硬件、可复用现有 C 库(如 FFmpeg)。

典型适用场景

  • 音视频处理(如用 FFmpeg 解码视频,C++ 性能远高于 Java);
  • 游戏引擎(如 Unity、Cocos2d-x 核心逻辑用 C++ 实现);
  • 加密算法(如 AES、RSA 的核心加密用 C++ 实现,更难逆向);
  • 硬件交互(如调用传感器、蓝牙芯片的驱动接口)。

不适用场景:简单业务逻辑(JNI 调用有性能开销,反而降低效率)、纯 UI 交互(Java 更便捷)。

1.2 JNI 的工作流程

JNI 的核心是 "双向映射"------Java 方法映射到 C 函数,Java 数据类型映射到 C 类型。完整流程如下:

1.Java 声明原生方法:用native关键字标记需要用 C 实现的方法;

2.生成头文件:通过javah工具生成包含函数签名的头文件;

3.C 实现原生方法:根据头文件的函数签名,编写 C 代码;

4.编译动态库:将 C 代码编译为.so文件(Android 的动态链接库);

5.Java 加载动态库:通过System.loadLibrary()加载.so,调用原生方法。

例如:Java 声明native int add(int a, int b),C 实现Java_com_example_jnidemo_MainActivity_add函数,完成两数相加。

1.3 JNI 的核心组件

|---------------|---------------------------|------------------------|
| 组件 | 作用 | 类比 |
| JNIEnv | JNI 环境指针,提供 JNI 函数(如创建对象) | C 语言的stdio.h(提供 IO 函数) |
| jclass | Java 类的引用 | Java 的Class对象 |
| jobject | Java 对象的引用 | Java 的Object对象 |
| jmethodID | Java 方法的标识符 | 方法的 "内存地址" |
| jfieldID | Java 字段的标识符 | 字段的 "内存地址" |
| .so 文件 | 编译后的 C 代码动态库 | Java 的.class文件 |

JNIEnv是最核心的组件 ------ 所有 JNI 操作(如访问 Java 字段、调用 Java 方法)都需通过它提供的函数完成。

二、JNI 基础语法:数据类型与方法注册

2.1 数据类型映射:Java 与 C 的 "翻译器"

Java 与 C 的数据类型不同,JNI 定义了对应的映射关系,确保数据正确传递。

(1)基本数据类型

基本类型直接映射(无内存差异):

|---------|----------|----------------|--------|
| Java 类型 | JNI 类型 | C 类型 | 长度(字节) |
| boolean | jboolean | unsigned char | 1 |
| byte | jbyte | signed char | 1 |
| char | jchar | unsigned short | 2 |
| short | jshort | short | 2 |
| int | jint | int | 4 |
| long | jlong | long long | 8 |
| float | jfloat | float | 4 |
| double | jdouble | double | 8 |

使用示例

java 复制代码
// Java:声明原生方法(基本类型参数)
public native int add(int a, int b);
cpp 复制代码
// C:实现方法(jint对应int)
JNIEXPORT jint JNICALL Java_com_example_jnidemo_MainActivity_add
  (JNIEnv *env, jobject thiz, jint a, jint b) {
    return a + b; // 直接运算,无需转换
}
(2)引用类型

引用类型(对象、数组等)需要通过 JNI 函数操作(不能直接访问内存):

|---------|--------------|------------------------------|
| Java 类型 | JNI 类型 | 说明 |
| Object | jobject | 所有对象的基类 |
| Class | jclass | 类对象(对应 Java 的 Class) |
| String | jstring | 字符串对象 |
| 数组 | jintArray 等 | 基本类型数组(如 int []→jintArray) |
| 对象数组 | jobjectArray | 对象类型数组(如 String []) |
| 自定义对象 | jobject | 需通过类名获取引用 |

引用类型的核心是 "不直接操作内存"------ 例如 Java 的String在 C 中是jstring,需通过GetStringUTFChars等函数转换为 C 的字符串。

2.2 方法注册:Java 方法与 C 函数的绑定

Java 的native方法需要与 C 函数绑定,有两种注册方式:

(1)静态注册(推荐入门)

通过 "函数名约定" 自动绑定 ------C 函数名包含 Java 类名和方法名,格式为:

复制代码

Java_包名_类名_方法名

  • 包名中的.替换为_;
  • 内部类用_分隔(如MainActivity$Inner→Java_com_example_MainActivity_00024Inner_method)。

步骤示例

1.Java 声明 native 方法

java 复制代码
package com.example.jnidemo;

public class JNIManager {
    // 加载动态库
    static {
        System.loadLibrary("native-lib"); // 加载libnative-lib.so
    }

    // 声明原生方法
    public native String getHelloString();
    public native int calculate(int a, int b);
}

2.生成头文件

在app/src/main/java目录下执行:

java 复制代码
javah -jni com.example.jnidemo.JNIManager

生成com_example_jnidemo_JNIManager.h头文件,内容包含函数签名:

cpp 复制代码
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnidemo_JNIManager */

#ifndef _Included_com_example_jnidemo_JNIManager
#define _Included_com_example_jnidemo_JNIManager
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_jnidemo_JNIManager
 * Method:    getHelloString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JNIManager_getHelloString
  (JNIEnv *, jobject);

/*
 * Class:     com_example_jnidemo_JNIManager
 * Method:    calculate
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_example_jnidemo_JNIManager_calculate
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

3.C 实现函数

创建native-lib.c,实现头文件中的函数:

cpp 复制代码
#include "com_example_jnidemo_JNIManager.h"

// 实现getHelloString
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JNIManager_getHelloString
  (JNIEnv *env, jobject thiz) {
    // 返回Java字符串
    return (*env)->NewStringUTF(env, "Hello from C");
}

// 实现calculate
JNIEXPORT jint JNICALL Java_com_example_jnidemo_JNIManager_calculate
  (JNIEnv *env, jobject thiz, jint a, jint b) {
    return a * 2 + b; // 自定义计算逻辑
}

优点 :简单直观,适合入门;缺点:函数名冗长,修改类名或包名需同步修改函数名。

(2)动态注册(推荐实战)

通过JNINativeMethod结构体手动绑定 ------ 在 C 中定义方法映射表,主动注册到 JVM。

步骤示例

1.C 定义方法映射表

cpp 复制代码
#include <jni.h>

// 实现函数(名称可自定义)
jstring native_hello(JNIEnv *env, jobject thiz) {
    return (*env)->NewStringUTF(env, "Hello from dynamic register");
}

jint native_calculate(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a + b * 3;
}

// 方法映射表(Java方法名 → C函数 → 签名)
static JNINativeMethod methods[] = {
    {
        "getHelloString", // Java方法名
        "()Ljava/lang/String;", // 方法签名
        (void*)native_hello // C函数指针
    },
    {
        "calculate",
        "(II)I",
        (void*)native_calculate
    }
};

// 注册函数
static int registerNatives(JNIEnv *env) {
    // Java类名(完整路径)
    const char *className = "com/example/jnidemo/JNIManager";
    // 获取类引用
    jclass clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    // 注册方法(类、方法表、方法数量)
    if ((*env)->RegisterNatives(env, clazz, methods, sizeof(methods)/sizeof(methods[0])) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

// JNI加载时自动调用(固定函数名)
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    // 获取JNIEnv
    if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    // 注册方法
    if (!registerNatives(env)) {
        return JNI_ERR;
    }
    // 返回JNI版本
    return JNI_VERSION_1_6;
}

2.Java 代码(与静态注册相同)

java 复制代码
public class JNIManager {
    static {
        System.loadLibrary("native-lib");
    }
    public native String getHelloString();
    public native int calculate(int a, int b);
}

优点

  • 函数名可自定义,无需冗长命名;
  • 类名或包名修改时,只需修改注册时的类路径;
  • 支持动态添加方法(如根据条件注册不同实现)。

缺点:需手动编写方法签名,容易出错;适合有经验的开发者。

2.3 方法签名:描述方法的 "身份证"

方法签名用于唯一标识 Java 方法(解决重载问题),格式为:

  • 参数类型:用字符表示(如I表示 int,Ljava/lang/String;表示 String);
  • 返回值类型:紧跟参数类型后;
  • 整体格式:(参数类型)返回值类型。

基本类型签名

|---------|------|---------|--------------------|
| Java 类型 | 签名字符 | Java 类型 | 签名字符 |
| boolean | Z | byte | B |
| char | C | short | S |
| int | I | long | J |
| float | F | double | D |
| void | V | Object | Ljava/lang/Object; |

引用类型签名

  • 类:L包名/类名;(如String→Ljava/lang/String;);
  • 数组:[类型签名(如int[]→[I,String[]→[Ljava/lang/String;)。

方法签名示例

|--------------------------------------|-----------------------------------------|---------------------------|
| Java 方法 | 签名 | 说明 |
| void test() | ()V | 无参数,无返回值 |
| int add(int a, int b) | (II)I | 两个 int 参数,返回 int |
| String getInfo(String name, int age) | (Ljava/lang/String;I)Ljava/lang/String; | String 和 int 参数,返回 String |
| void setData(int[] data) | ([I)V | int 数组参数,无返回值 |

生成签名工具:通过javap命令(JDK 自带)生成:

java 复制代码
# 查看类的方法签名(需先编译为class)
javap -s -p com.example.jnidemo.JNIManager

三、JNI 核心操作:字符串、数组与对象

掌握引用类型的操作是 JNI 开发的核心,以下是高频场景的实现。

3.1 字符串操作:Java String 与 C 字符串的转换

Java 的String是不可变的,在 C 中需通过 JNI 函数转换为可操作的字符串。

(1)Java String → C 字符串
cpp 复制代码
// 将jstring转为C的char*
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_printString
  (JNIEnv *env, jobject thiz, jstring jstr) {
    if (jstr == NULL) {
        return; // 避免空指针
    }

    // 转换为UTF-8字符串(isCopy:是否为副本,NULL表示不关心)
    const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (cstr == NULL) {
        return; // 内存不足时返回NULL
    }

    // 使用C字符串(如打印)
    printf("Java传递的字符串:%s\n", cstr);

    // 释放资源(必须调用,否则内存泄漏)
    (*env)->ReleaseStringUTFChars(env, jstr, cstr);
}

关键函数

  • GetStringUTFChars:将jstring转为 C 的char*(UTF-8 编码);
  • ReleaseStringUTFChars:释放转换后的字符串(必须与Get配对)。
(2)C 字符串 → Java String
cpp 复制代码
// 创建Java String并返回
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JNIManager_createString
  (JNIEnv *env, jobject thiz) {
    const char *cstr = "Hello from C";
    // 将C字符串转为jstring
    jstring jstr = (*env)->NewStringUTF(env, cstr);
    return jstr;
}

注意:NewStringUTF会在 JVM 中创建新的String对象,无需手动释放(由 JVM 垃圾回收)。

3.2 数组操作:基本类型数组与对象数组

(1)基本类型数组(如 int [])
cpp 复制代码
// 处理int数组:计算总和
JNIEXPORT jint JNICALL Java_com_example_jnidemo_JNIManager_sumArray
  (JNIEnv *env, jobject thiz, jintArray jarray) {
    if (jarray == NULL) {
        return 0;
    }

    // 获取数组长度
    jsize length = (*env)->GetArrayLength(env, jarray);
    if (length <= 0) {
        return 0;
    }

    // 获取数组元素(转为C的int[])
    jint *carray = (*env)->GetIntArrayElements(env, jarray, NULL);
    if (carray == NULL) {
        return 0; // 内存不足
    }

    // 计算总和
    jint sum = 0;
    for (int i = 0; i < length; i++) {
        sum += carray[i];
    }

    // 释放数组(mode参数:0=复制回Java并释放,JNI_ABORT=不复制直接释放)
    (*env)->ReleaseIntArrayElements(env, jarray, carray, 0);

    return sum;
}

关键函数

  • GetArrayLength:获取数组长度;
  • GetIntArrayElements:将jintArray转为 C 的jint*;
  • ReleaseIntArrayElements:释放数组(必须调用)。
(2)对象数组(如 String [])
cpp 复制代码
// 创建String数组并返回
JNIEXPORT jobjectArray JNICALL Java_com_example_jnidemo_JNIManager_createStringArray
  (JNIEnv *env, jobject thiz) {
    // 数组长度
    jsize length = 3;

    // 获取String类引用
    jclass stringClass = (*env)->FindClass(env, "java/lang/String");
    if (stringClass == NULL) {
        return NULL; // 类未找到
    }

    // 创建String数组(元素初始为NULL)
    jobjectArray jarray = (*env)->NewObjectArray(env, length, stringClass, NULL);
    if (jarray == NULL) {
        return NULL; // 内存不足
    }

    // 填充数组元素
    const char *strings[] = {"Apple", "Banana", "Orange"};
    for (int i = 0; i < length; i++) {
        // 创建Java String
        jstring jstr = (*env)->NewStringUTF(env, strings[i]);
        if (jstr == NULL) {
            // 失败时释放已创建的对象
            (*env)->DeleteLocalRef(env, jstr);
            return NULL;
        }
        // 设置数组元素
        (*env)->SetObjectArrayElement(env, jarray, i, jstr);
        // 释放局部引用(避免引用表溢出)
        (*env)->DeleteLocalRef(env, jstr);
    }

    return jarray;
}

关键函数

  • FindClass:获取类引用(用于指定数组元素类型);
  • NewObjectArray:创建对象数组;
  • SetObjectArrayElement:设置数组元素;
  • DeleteLocalRef:释放局部引用(重要!避免引用数量超限)。

3.3 访问 Java 对象的字段与方法

JNI 可访问 Java 对象的字段(成员变量)和调用 Java 方法,实现 C 与 Java 的双向交互。

(1)访问 Java 字段

Java 类定义

java 复制代码
public class User {
    private String name;
    public int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

C 访问字段

cpp 复制代码
// 修改User对象的字段
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_updateUser
  (JNIEnv *env, jobject thiz, jobject user) {
    if (user == NULL) {
        return;
    }

    // 1. 获取User类引用
    jclass userClass = (*env)->GetObjectClass(env, user);
    if (userClass == NULL) {
        return;
    }

    // 2. 获取字段ID(public字段)
    jfieldID ageField = (*env)->GetFieldID(env, userClass, "age", "I");
    if (ageField == NULL) {
        return;
    }

    // 3. 读取public字段值
    jint age = (*env)->GetIntField(env, user, ageField);
    age += 5; // 年龄增加5岁

    // 4. 修改public字段值
    (*env)->SetIntField(env, user, ageField, age);

    // 5. 获取private字段ID(需指定签名)
    jfieldID nameField = (*env)->GetFieldID(env, userClass, "name", "Ljava/lang/String;");
    if (nameField == NULL) {
        return;
    }

    // 6. 修改private字段值(JNI可访问private字段,不受Java访问权限限制)
    jstring newName = (*env)->NewStringUTF(env, "New Name");
    (*env)->SetObjectField(env, user, nameField, newName);

    // 释放局部引用
    (*env)->DeleteLocalRef(env, newName);
    (*env)->DeleteLocalRef(env, userClass);
}

关键函数

  • GetObjectClass:通过对象获取类引用;
  • GetFieldID:获取字段 ID(需字段名和签名);
  • GetIntField/SetIntField:读取 / 修改基本类型字段;
  • GetObjectField/SetObjectField:读取 / 修改引用类型字段。
(2)调用 Java 方法

Java 类定义

java 复制代码
public class Calculator {
    // 实例方法
    public int multiply(int a, int b) {
        return a * b;
    }

    // 静态方法
    public static String formatResult(int result) {
        return "Result: " + result;
    }
}

C 调用方法

cpp 复制代码
// 调用Calculator的方法
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JNIManager_callJavaMethod
  (JNIEnv *env, jobject thiz) {
    // 1. 获取Calculator类引用
    jclass calcClass = (*env)->FindClass(env, "com/example/jnidemo/Calculator");
    if (calcClass == NULL) {
        return NULL;
    }

    // 2. 创建Calculator实例(调用构造方法)
    jmethodID constructor = (*env)->GetMethodID(env, calcClass, "<init>", "()V"); // 构造方法签名
    jobject calcObj = (*env)->NewObject(env, calcClass, constructor);
    if (calcObj == NULL) {
        return NULL;
    }

    // 3. 调用实例方法multiply(int, int)
    jmethodID multiplyMethod = (*env)->GetMethodID(env, calcClass, "multiply", "(II)I");
    if (multiplyMethod == NULL) {
        return NULL;
    }
    jint result = (*env)->CallIntMethod(env, calcObj, multiplyMethod, 3, 4); // 3*4=12

    // 4. 调用静态方法formatResult(int)
    jmethodID formatMethod = (*env)->GetStaticMethodID(env, calcClass, "formatResult", "(I)Ljava/lang/String;");
    if (formatMethod == NULL) {
        return NULL;
    }
    jstring jresult = (*env)->CallStaticObjectMethod(env, calcClass, formatMethod, result);

    // 释放局部引用
    (*env)->DeleteLocalRef(env, calcObj);
    (*env)->DeleteLocalRef(env, calcClass);

    return jresult;
}

关键函数

  • GetMethodID:获取实例方法 ID(构造方法名为<init>);
  • CallIntMethod/CallObjectMethod:调用实例方法;
  • GetStaticMethodID:获取静态方法 ID;
  • CallStaticObjectMethod:调用静态方法。

四、JNI 内存管理:避免泄漏与崩溃

JNI 的内存管理是最容易出错的部分 ------C 的手动内存管理与 Java 的垃圾回收需协同工作,否则会导致内存泄漏或野指针。

4.1 JNI 引用类型:局部引用、全局引用与弱全局引用

JNI 有三种引用类型,生命周期不同,需正确使用:

(1)局部引用(Local Reference)
  • 生命周期:在当前 JNI 函数中有效,函数返回后自动释放;
  • 使用场景:临时对象(如jstring、jclass);
  • 限制:数量有限(默认 512 个),超出会抛出OutOfMemoryError。

正确使用

cpp 复制代码
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_useLocalRef
  (JNIEnv *env, jobject thiz) {
    // 创建局部引用
    jstring jstr = (*env)->NewStringUTF(env, "local reference");

    // 使用引用...

    // 提前释放(函数结束会自动释放,但推荐手动释放)
    (*env)->DeleteLocalRef(env, jstr);
}

常见错误:将局部引用存储到全局变量(函数返回后引用失效,访问会崩溃)。

(2)全局引用(Global Reference)
  • 生命周期:手动创建,手动释放,跨函数、跨线程有效;
  • 使用场景:需要长期使用的对象(如配置信息、全局缓存);
  • 创建 / 释放:NewGlobalRef创建,DeleteGlobalRef释放。

正确使用

cpp 复制代码
// 全局变量存储全局引用
static jobject g_config = NULL;

// 初始化全局引用
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_initGlobalRef
  (JNIEnv *env, jobject thiz, jobject config) {
    // 先释放旧引用
    if (g_config != NULL) {
        (*env)->DeleteGlobalRef(env, g_config);
    }
    // 创建全局引用(参数为局部引用)
    g_config = (*env)->NewGlobalRef(env, config);
}

// 使用全局引用
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_useGlobalRef
  (JNIEnv *env, jobject thiz) {
    if (g_config != NULL) {
        // 使用g_config...
    }
}

// 释放全局引用(如退出时)
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_releaseGlobalRef
  (JNIEnv *env, jobject thiz) {
    if (g_config != NULL) {
        (*env)->DeleteGlobalRef(env, g_config);
        g_config = NULL;
    }
}

常见错误:忘记释放全局引用(导致内存泄漏,对象无法被 GC 回收)。

(3)弱全局引用(Weak Global Reference)
  • 生命周期:手动创建,手动释放,对象可被 GC 回收;
  • 使用场景:缓存非必需对象(如图片缓存,内存不足时可回收);
  • 创建 / 释放:NewWeakGlobalRef创建,DeleteWeakGlobalRef释放。

正确使用

cpp 复制代码
static jweak g_weakCache = NULL;

// 创建弱引用
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_initWeakRef
  (JNIEnv *env, jobject thiz, jobject data) {
    if (g_weakCache != NULL) {
        (*env)->DeleteWeakGlobalRef(env, g_weakCache);
    }
    g_weakCache = (*env)->NewWeakGlobalRef(env, data);
}

// 使用弱引用(需检查是否被回收)
JNIEXPORT void JNICALL Java_com_example_jnidemo_JNIManager_useWeakRef
  (JNIEnv *env, jobject thiz) {
    if (g_weakCache == NULL) {
        return;
    }

    // 检查对象是否被回收
    jobject obj = (*env)->NewLocalRef(env, g_weakCache);
    if (obj == NULL) {
        // 对象已被GC回收
        return;
    }

    // 使用对象...

    // 释放局部引用
    (*env)->DeleteLocalRef(env, obj);
}

优势:不会阻止 GC 回收对象,适合缓存场景。

4.2 内存泄漏的常见原因及解决方案

(1)未释放引用
  • 原因:局部引用未及时释放(导致引用表溢出)、全局引用忘记释放(对象无法回收);
  • 解决方案
  • 局部引用:DeleteLocalRef手动释放(尤其在循环中);
  • 全局引用:在onDestroy等时机调用DeleteGlobalRef;
  • 弱引用:不再使用时调用DeleteWeakGlobalRef。
(2)JNIEnv 与线程的绑定
  • 原因:JNIEnv是线程私有(每个线程有独立的JNIEnv),跨线程使用会崩溃;
  • 解决方案
  • 线程中获取JNIEnv:通过JavaVM的AttachCurrentThread获取;

  • 使用后 detach:DetachCurrentThread。

    cpp 复制代码
    // 保存JavaVM(在JNI_OnLoad中获取)
    static JavaVM *g_jvm = NULL;
    
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
        g_jvm = vm; // 保存JavaVM(全局可用)
        return JNI_VERSION_1_6;
    }
    
    // 子线程函数
    void *native_thread(void *arg) {
        JNIEnv *env;
        // 绑定当前线程到JVM,获取JNIEnv
        if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) {
            return NULL;
        }
    
        // 使用env操作Java...
    
        // 解除线程绑定
        (*g_jvm)->DetachCurrentThread(g_jvm);
        return NULL;
    }
(3)数组 / 字符串未释放
  • 原因:GetIntArrayElements、GetStringUTFChars等函数分配的内存未释放;
  • 解决方案 :严格配对调用Release系列函数:

    cpp 复制代码
    // 正确示例:配对使用Get和Release
    jint *carray = (*env)->GetIntArrayElements(env, jarray, NULL);
    if (carray != NULL) {
        // 使用...
        (*env)->ReleaseIntArrayElements(env, jarray, carray, 0); // 必须释放
    }

五、Android Studio 配置与调试

5.1 NDK 环境配置

1.安装 NDK 和 CMake

  • Android Studio → File → Settings → Appearance & Behavior → System Settings → Android SDK → SDK Tools → 勾选 NDK、CMake → 安装。

2.配置 build.gradle

cpp 复制代码
android {
    defaultConfig {
        // 指定支持的CPU架构(按需添加)
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt" // CMake配置文件路径
        }
    }
}

3.创建 CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 3.10.2)

# 定义项目名称
project("native-lib")

# 添加源文件(所有C/C++文件)
add_library(
    native-lib
    SHARED
    src/main/cpp/native-lib.c
)

# 链接Android系统库
find_library(
    log-lib
    log
)

# 链接目标库
target_link_libraries(
    native-lib
    ${log-lib}
)

5.2 JNI 调试技巧

  1. 日志输出 :使用 Android 的__android_log_print打印日志:

    cpp 复制代码
    #include <android/log.h>
    
    // 定义日志宏
    #define LOG_TAG "JNI_DEBUG"
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
    
    // 使用
    void testLog() {
        LOGD("debug message: %d", 123); // 调试日志
        LOGE("error message: %s", "failed"); // 错误日志
    }
  2. 断点调试

  • 在 Android Studio 的 C 代码中点击行号旁设置断点;
  • 选择 "Debug" 运行,程序会在断点处暂停;
  • 可查看变量、单步执行(与 Java 调试类似)。

六、常见错误与解决方案

6.1 崩溃类错误

|------------------------|--------------------------|-------------------------------|
| 错误现象 | 常见原因 | 解决方案 |
| SIGSEGV(段错误) | 访问空指针、释放后继续使用引用 | 检查引用是否为 NULL,避免使用已释放的引用 |
| ClassNotFoundException | FindClass的类路径错误(如包名拼写错误) | 确认类路径正确(如com/example/MyClass) |
| NoSuchMethodError | 方法签名错误或方法名拼写错误 | 通过javap生成正确签名,检查方法名 |
| OutOfMemoryError | 局部引用未释放,超过引用表上限 | 及时调用DeleteLocalRef释放局部引用 |

6.2 内存泄漏类错误

|----------------------|---------------------------|-------------------------|
| 错误现象 | 常见原因 | 解决方案 |
| Java 对象无法被 GC 回收 | 全局引用未释放,持有对象引用 | 在合适时机调用DeleteGlobalRef |
| 频繁创建临时对象导致内存增长 | 循环中创建大量局部引用 | 复用对象,及时释放临时引用 |
| GetStringUTFChars未释放 | 忘记调用ReleaseStringUTFChars | 严格配对调用 Get 和 Release 函数 |

6.3 性能类问题

|-------------------|---------------------|------------------|
| 问题现象 | 常见原因 | 解决方案 |
| JNI 调用耗时过长 | 在 JNI 中执行大量计算,未优化循环 | 优化算法,将计算拆分为小批次执行 |
| 频繁的 Java 与 C 数据转换 | 多次转换字符串、数组 | 减少转换次数,缓存转换结果 |
| 线程创建过多 | 未复用线程,每次调用创建新线程 | 使用线程池,复用现有线程 |

七、总结:JNI 开发的核心原则

JNI 开发的核心是 "谨慎操作,释放优先"------C 的灵活性带来了高性能,但也失去了 Java 的安全保障。掌握以下原则可大幅减少错误:

1.引用管理第一

  • 局部引用:不用即释放(尤其在循环和分支中);
  • 全局引用:明确生命周期,必在退出时释放;
  • 弱引用:使用前检查是否被回收。

2.类型转换严格

  • 字符串:GetStringUTFChars与Release配对;
  • 数组:获取长度后再访问,避免越界;
  • 对象:通过GetFieldID/GetMethodID访问,不直接操作内存。

3.日志与调试

  • 关键步骤添加日志,方便定位问题;
  • 复杂逻辑先写原型,通过调试确认正确性。

4.性能与安全平衡

  • 非性能敏感模块优先用 Java;
  • 敏感逻辑(如加密)用 C 实现,增加逆向难度;
  • 避免在 JNI 中做 UI 操作(效率低,且易出错)。

JNI 是 Android 开发中的 "高级技能",掌握它能让你在性能优化、底层交互等场景中得心应手。从简单的静态注册开始,逐步实践动态注册、对象操作,结合调试工具排查错误,你会发现 JNI 并没有想象中那么难。

最后记住:JNI 是 "工具" 而非 "目的"------ 用最少的 JNI 代码解决最关键的问题,才是高效开发的核心。

相关推荐
OKkankan43 分钟前
string类的模拟实现
开发语言·数据结构·c++·算法
fouryears_234173 小时前
适配器模式——以springboot为例
java·spring boot·适配器模式
汽车功能安全啊4 小时前
利用对称算法及非对称算法实现安全启动
java·开发语言·安全
paopaokaka_luck4 小时前
基于Spring Boot+Vue的吉他社团系统设计和实现(协同过滤算法)
java·vue.js·spring boot·后端·spring
Warren986 小时前
Java Stream流的使用
java·开发语言·windows·spring boot·后端·python·硬件工程
一笑的小酒馆6 小时前
Android12去掉剪贴板复制成功的Toast
android
一笑的小酒馆7 小时前
Android12App启动图标自适应
android
架构师沉默7 小时前
Java优雅使用Spring Boot+MQTT推送与订阅
java·开发语言·spring boot
tuokuac7 小时前
MyBatis 与 Spring Boot版本匹配问题
java·spring boot·mybatis
zhysunny8 小时前
05.原型模式:从影分身术到细胞分裂的编程艺术
java·原型模式