在 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 调试技巧
-
日志输出 :使用 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"); // 错误日志 }
-
断点调试:
- 在 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 代码解决最关键的问题,才是高效开发的核心。