Android JNI 示例详解:Java 与 C++ 互调演示

1. Demo 概述

这个 Demo 演示了 Android JNI 开发中最常用的几类交互场景,分为两个文件:

  • Java 层Test.java(包名 com.qhsoft.jnitestdemo)------ 声明 native 方法、提供供 C 回调的方法
  • Native 层test.cpp ------ 各 native 方法的 C/C++ 实现

覆盖的核心场景:

场景 对应方法
Java 调 C · 返回字符串 showMsg
Java 调 C · 基本类型运算 sum
Java 调 C · 字符串拼接 append
C 回调 Java 方法 callC
遍历 Java 集合(ArrayList) sumList
遍历对象集合并构造新对象 sumAge
字符串 / 数组操作 lengthsumIntgetByteArray
在 C 中创建 Java 对象 getPerson

注意:Demo 中用到了 com.qhsoft.jnitestdemo.Person 类(含 nameage 字段及对应 getter/setter),该类不在此文档中。

2. Java 层:Test.java

要点:

  • static {} 代码块中通过 System.loadLibrary("TestDemo") 加载 Native 库 libTestDemo.so,类加载时执行一次
  • 所有 native 方法只有声明、没有实现,具体逻辑全部在 C 层完成
  • showJava 是一个普通 Java 方法,专门用来被 C 层回调(弹 Toast)
java 复制代码
package com.qhsoft.jnitestdemo;
​
import android.content.Context;
import android.widget.Toast;
​
import java.util.ArrayList;
import java.util.List;
​
public class Test {
​
    // 加载 Native 库(libTestDemo.so)
    static {
        System.loadLibrary("TestDemo");
    }
​
    private Context mContext;
​
    public Test(Context mContext) {
        this.mContext = mContext;
    }
​
    // 供 C 层回调的 Java 方法:弹一个 Toast
    public void showJava(String str) {
        Toast.makeText(mContext, str, Toast.LENGTH_LONG).show();
    }
​
    // ============ 声明的 native 方法(实现见 test.cpp) ============
​
    public native String showMsg();                     // 返回一个字符串
    public native void callC(String str);               // C 层回调 showJava
    public native float sum(float i, float j);          // 两个 float 求和
    public native String append(String a, String b);    // 拼接两个字符串
    public native int sumList(ArrayList<Integer> list); // 遍历 Integer 列表求和
    public native Person sumAge(List<Person> list);     // 汇总所有人年龄,返回新 Person
    public native int length(String str);               // 获取字符串长度
    public native int sumInt(int[] src);                // int 数组求和
    public native byte[] getByteArray(byte[] array);    // byte 数组每个元素 +10
    public native Person getPerson();                   // 在 C 中构造 Person 对象
}

3. Native 层:test.cpp

3.1 头文件与日志宏

  • 引入 JNI 头文件与 Android 日志头文件
  • LOG_D / LOG_E 封装了 __android_log_print,可在 Logcat 中用 Tag jni 过滤输出
arduino 复制代码
#include <jni.h>
#include <cstring>
#include <malloc.h>
#include <android/log.h>
#include <__locale>
​
using namespace std;
​
#define LOG_D(...)  __android_log_print(ANDROID_LOG_DEBUG, "jni", __VA_ARGS__)
#define LOG_E(...)  __android_log_print(ANDROID_LOG_ERROR, "jni", __VA_ARGS__)
​
extern "C" {

说明:所有函数都包在 extern "C" 中,避免 C++ 名称修饰导致 JNI 找不到符号;文档结尾应有对应的 }

3.2 返回字符串:showMsg

最简单的 JNI 用法:直接用 NewStringUTF 把 C 字符串转成 jstring 返回给 Java。

bash 复制代码
JNIEXPORT jstring JNICALL
Java_com_qhsoft_jnitestdemo_Test_showMsg(JNIEnv *env, jobject obj) {
    return env->NewStringUTF("欢迎世界");
}

3.3 C 回调 Java 方法:callC

演示 C 层主动调用 Java 方法 的完整三步:

  1. FindClass 拿到 Java 类(等价写法:GetObjectClass(obj)
  2. GetMethodID 拿到方法 ID ------ 注意签名 (Ljava/lang/String;)V 表示「参数 String、返回 void」
  3. CallVoidMethod 真正调用
ini 复制代码
JNIEXPORT void JNICALL Java_com_qhsoft_jnitestdemo_Test_callC
        (JNIEnv *env, jobject obj, jstring jstr) {
    jclass myclass = env->FindClass("com/qhsoft/jnitestdemo/Test");
//    jclass myclass = env->GetObjectClass(obj);   // 等价写法
    jmethodID mid = env->GetMethodID(myclass, "showJava", "(Ljava/lang/String;)V");
    env->CallVoidMethod(obj, mid, jstr);
}

3.4 基本类型运算:sum

基本类型(jint / jfloat 等)可以直接当 C 类型用,无需转换。

arduino 复制代码
JNIEXPORT jfloat JNICALL
Java_com_qhsoft_jnitestdemo_Test_sum(JNIEnv *env, jobject obj, jfloat i, jfloat j) {
    return i + j;
}

3.5 字符串拼接:append

  • GetStringUTFCharsjstring 转成 C 的 char*
  • GetStringLength 拿到字符长度,据此 malloc 结果缓冲区
  • strcpy + strcat 完成拼接,再包回 jstring 返回
scss 复制代码
JNIEXPORT jstring JNICALL
Java_com_qhsoft_jnitestdemo_Test_append(JNIEnv *env, jobject obj, jstring a, jstring b) {
    const char* cstr1 = env->GetStringUTFChars(a, JNI_FALSE);
    int alen = env->GetStringLength(a);
    char* rtn = (char *) malloc(alen + 1);  // "\0" 申请空间
​
    strcpy(rtn, cstr1);
​
    const char* cstr2 = env->GetStringUTFChars(b, JNI_FALSE);
    strcat(rtn, cstr2);
​
    return env->NewStringUTF(rtn);
}

3.6 遍历 ArrayList 求和:sumList

Java 的 ArrayList 在 C 里就是一个 jobject,操作方式是反射式调用

  1. GetObjectClass 拿到 List 类,再取 size()get() 两个方法 ID
  2. 循环内 CallObjectMethod 逐个取出元素(得到 Integer 对象)
  3. 对每个元素再反射调用 intValue() 拆箱累加
ini 复制代码
JNIEXPORT jint JNICALL
Java_com_qhsoft_jnitestdemo_Test_sumList(JNIEnv* env, jobject obj, jobject objectList) {
    jclass class_list = env->GetObjectClass(objectList);
    jmethodID jmethodId_size = env->GetMethodID(class_list, "size", "()I");
    jmethodID jmethodId_get  = env->GetMethodID(class_list, "get", "(I)Ljava/lang/Object;");
    int len = env->CallIntMethod(objectList, jmethodId_size);
    LOG_D("len:%d", len);
​
    int sum = 0;
    for (int i = 0; i < len; i++) {
        jobject jobject1 = env->CallObjectMethod(objectList, jmethodId_get, i);
        jclass jclass1 = env->GetObjectClass(jobject1);
        jmethodID intvalue_Id = env->GetMethodID(jclass1, "intValue", "()I");
        int x = env->CallIntMethod(jobject1, intvalue_Id);
        LOG_D("get获取到的值:%d", x);
        sum = sum + x;
    }
    return sum;
}

3.7 汇总对象年龄并返回新对象:sumAge

在 3.6 的基础上更进一步:

  • 遍历 List<Person>,反射调用每个元素的 getAge() 累加
  • 通过 FindClass("com/qhsoft/jnitestdemo/Person") + NewObject 在 C 中创建新 Person
  • GetFieldID + SetIntField / SetObjectField 直接给字段 agename 赋值
ini 复制代码
JNIEXPORT jobject JNICALL
Java_com_qhsoft_jnitestdemo_Test_sumAge(JNIEnv *env, jobject obj, jobject objectList) {
    jclass class_list = env->GetObjectClass(objectList);
    jmethodID jmethodId_size = env->GetMethodID(class_list, "size", "()I");
    jmethodID jmethodId_get  = env->GetMethodID(class_list, "get", "(I)Ljava/lang/Object;");
    int len = env->CallIntMethod(objectList, jmethodId_size);
    LOG_D("len:%d", len);
​
    int sumAge = 0;
    for (int i = 0; i < len; i++) {
        jobject jobject1 = env->CallObjectMethod(objectList, jmethodId_get, i);
        jclass jclass1 = env->GetObjectClass(jobject1);
        jmethodID getAge_Id = env->GetMethodID(jclass1, "getAge", "()I");
        int x = env->CallIntMethod(jobject1, getAge_Id);
        sumAge = sumAge + x;
    }
​
    // 创建结果对象:new Person(),并设置 age / name 字段
    jclass jclass2 = env->FindClass("com/qhsoft/jnitestdemo/Person");
    jmethodID jmethodId = env->GetMethodID(jclass2, "<init>", "()V");
    jobject jobject2 = env->NewObject(jclass2, jmethodId);
​
    jfieldID jfieldId = env->GetFieldID(jclass2, "age", "I");
    env->SetIntField(jobject2, jfieldId, sumAge);
​
    jfieldID jfieldId1 = env->GetFieldID(jclass2, "name", "Ljava/lang/String;");
    jstring name = env->NewStringUTF("hai");
    env->SetObjectField(jobject2, jfieldId1, name);
​
    return jobject2;
}

3.8 字符串长度:length

rust 复制代码
JNIEXPORT jint JNICALL
Java_com_qhsoft_jnitestdemo_Test_length(JNIEnv *env, jobject obj, jstring str) {
    return env->GetStringLength(str);
}

3.9 int 数组求和:sumInt

  • GetArrayLength 取数组长度
  • GetIntArrayElements 把 Java 数组转成 C 指针(获取的是数组副本或直接引用)
ini 复制代码
JNIEXPORT jint JNICALL
Java_com_qhsoft_jnitestdemo_Test_sumInt(JNIEnv *env, jobject obj, jintArray str) {
    int len = env->GetArrayLength(str);
    int* result = env->GetIntArrayElements(str, JNI_FALSE);
​
    int sum = 0;
    for (int i = 0; i < len; i++) {
        sum = sum + result[i];
    }
    return sum;
}

3.10 byte 数组处理:getByteArray

演示 JNI 数组操作的标准四步流程:取指针 → 处理 → 释放 → 回写

ini 复制代码
JNIEXPORT jbyteArray JNICALL
Java_com_qhsoft_jnitestdemo_Test_getByteArray(JNIEnv *env, jobject obj, jbyteArray j_array) {
    // 1. 获取数组指针和长度
    jbyte *c_array = env->GetByteArrayElements(j_array, 0);
    int len_arr = env->GetArrayLength(j_array);
​
    // 2. 具体处理:每个元素 +10
    jbyteArray c_result = env->NewByteArray(len_arr);
    jbyte buf[len_arr];
    for (int i = 0; i < len_arr; i++) {
        buf[i] = c_array[i] + 10;
    }
​
    // 3. 释放内存(必须调用,否则内存泄漏)
    env->ReleaseByteArrayElements(j_array, c_array, 0);
​
    // 4. 把结果写回新数组并返回
    env->SetByteArrayRegion(c_result, 0, len_arr, buf);
    return c_result;
}

3.11 在 C 中构造对象:getPerson

与 3.7 直接给字段赋值不同,这里反射调用的是 setter 方法setAge / setName),并用 DeleteLocalRef 主动释放局部引用。

ini 复制代码
JNIEXPORT jobject JNICALL
Java_com_qhsoft_jnitestdemo_Test_getPerson(JNIEnv *env, jobject obj) {
    jclass clazz = env->FindClass("com/qhsoft/jnitestdemo/Person");
    jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");
    jobject jobj = env->NewObject(clazz, mid_construct);
​
    jmethodID age_id = env->GetMethodID(clazz, "setAge", "(I)V");
    env->CallVoidMethod(jobj, age_id, 30);
​
    jmethodID name_id = env->GetMethodID(clazz, "setName", "(Ljava/lang/String;)V");
    jstring jstring1 = env->NewStringUTF("hai");
    env->CallVoidMethod(jobj, name_id, jstring1);
​
    env->DeleteLocalRef(clazz);
​
    return jobj;
}

4. 附:JNI 方法命名与常用 API 速查

方法命名规则Java_完整类名(点换下划线)_方法名,如 Java_com_qhsoft_jnitestdemo_Test_showMsg

本文用到的 JNI API:

API 用途
NewStringUTF 创建 jstring 返回给 Java
GetStringUTFChars / GetStringLength jstring 转 C 字符串 / 取长度
FindClass / GetObjectClass 按全限定名 / 按对象拿 jclass
GetMethodID / GetFieldID 拿方法 / 字段 ID(需传签名)
CallVoidMethod / CallIntMethod / CallObjectMethod 反射调用 Java 方法
NewObject 在 C 中创建 Java 对象(<init> 构造器)
SetIntField / SetObjectField 直接给对象字段赋值
GetArrayLength / GetIntArrayElements / ReleaseByteArrayElements / SetByteArrayRegion 数组:取长度、取指针、释放、回写
DeleteLocalRef 主动释放局部引用
相关推荐
geovindu1 小时前
java: Strategy Pattern
java·开发语言·后端·设计模式·策略模式·行为模式
yimindev1 小时前
从 BI 到 Agent:我如何用 Java 打造一个轻量级数据库语义网关 DatI
java
计算机毕设定制辅导-无忧学长1 小时前
《基于SpringBoot的家政服务管理平台的设计与实现》
java·vue.js·spring boot·计算机毕业设计选题推荐·家政服务管理平台
秋饼1 小时前
Spring AI 2.0 企业级文档ETL实战:从 DocumentReader 到 EmbeddingStore 的完整知识库数据管道
java·ai·技术分享·后端开发
吃饱了得干活1 小时前
Spring Boot + Redis 分布式锁:一个订单重复提交引发的六次迭代
java·redis·后端
xiaoqiMikko1 小时前
pom 里没有、代码里没调过,但 WebClient 默认用的就是 netty-resolver-dns
java·spring boot
wuminyu1 小时前
Kafka的写入延迟与磁盘IO抖动原理分析
java·linux·c语言·jvm·c++
aramae1 小时前
模拟实现strcpy(字符串拷贝)(C语言)
java·c语言·开发语言·算法
IT毕设实战小研1 小时前
基于大数据的跨国外派人员适应满意度与留存影响因素可视化分析
android·java·大数据·python·django·课程设计