☰
JNI揭秘:Android跨语言协作的翻译官
2026/10/22 1:17:07 网站建设 项目流程

一、JNI 是什么?

Android 应用主要用Java / Kotlin编写,但有些代码是用C / C++编写的,比如:

  • 游戏引擎(Unity 的核心就是 C++)
  • 音视频编解码
  • 已有的 C++ 代码库

这两种语言不能直接互相调用。

JNI(Java Native Interface)就是让它们能互相调用的一套规则和工具。


二、打个比方:翻译官

想象一个中国人和一个美国人合作:

比喻实际
中国人Java 代码
美国人C/C++ 代码
翻译官JNI

他们之间有几个"沟通障碍",翻译官要一一解决:

  1. 说的话不一样→ 数据类型不同(Java 的String和 C 的char*不一样)
  2. 管东西的方式不一样→ Java 有垃圾回收器自动管理内存,C 要手动管理
  3. 出错的处理方式不一样→ Java 用异常,C 没有这一套

后面会逐个讲这几点。


三、先看一个最简单的例子

目标:Java 调用 C++ 写的加法函数。

第 1 步:Java 这边声明"这个方法由 C++ 实现"

packagecom.example;publicclassMathUtil{static{System.loadLibrary("mymath");// 加载 libmymath.so}publicstaticnativeintadd(inta,intb);// native 表示:实现在 C++ 里}
  • native:告诉 Java"我只有声明,具体代码去 C++ 那边找"。
  • loadLibrary("mymath"):把编译好的 C++ 库libmymath.so加载进来。

第 2 步:C++ 这边写实现

#include<jni.h>extern"C"JNIEXPORT jint JNICALLJava_com_example_MathUtil_add(JNIEnv*env,jclass clazz,jint a,jint b){returna+b;}

这一大串看着吓人,拆开看其实很简单:

部分意思
Java_com_example_MathUtil_add函数名是按规则拼出来的:Java_+ 包名 + 类名 + 方法名,Java 就靠这个名字找到它
jintJNI 版的int,和 Java 的int对应
JNIEnv* env翻译官本人,后面所有"翻译"操作都要通过它
jclass clazz是哪个 Java 类调用的(静态方法传类,普通方法传对象)
a, b就是 Java 传过来的两个参数
extern "C"让 C++ 不要改函数名,否则 Java 找不到

第 3 步:Java 正常调用

intresult=MathUtil.add(3,5);// 得到 8,实际是 C++ 算的

背后发生了什么

Java 调用 MathUtil.add(3, 5) ↓ 发现是 native 方法 → 去已加载的 libmymath.so 里找 Java_com_example_MathUtil_add ↓ 找到后调用它,顺便把"翻译官" env 交给它 ↓ C++ 计算 3 + 5,返回 8 ↓ 结果交回 Java

这就是 JNI 最核心的过程。后面的内容都是在这个基础上处理更复杂的情况。


四、难题 1:数据类型不同

简单类型:直接对应,不用操心

JavaC++ 里写成
intjint
longjlong
floatjfloat
booleanjboolean

本质上就是同一种数字,换了个名字。

字符串:需要"翻译"

Java 的String和 C 的char*在内存里的样子完全不同,必须让翻译官转换:

// Java: public static native void hello(String name);extern"C"JNIEXPORTvoidJNICALLJava_com_example_MathUtil_hello(JNIEnv*env,jclass clazz,jstring name){constchar*cName=env->GetStringUTFChars(name,nullptr);// ① 借:Java 字符串 → C 字符串printf("你好,%s\n",cName);env->ReleaseStringUTFChars(name,cName);// ② 还:用完必须还回去}

可以理解为:向翻译官"借"一份 C 能看懂的副本,用完要"还"。忘记还就会内存泄漏。

反过来,C 字符串变成 Java 字符串:

jstring result=env->NewStringUTF("来自 C++ 的问候");

五、难题 2:Java 的对象,C++ 不能直接拿

这是 JNI 最容易出错的地方。

为什么不能直接拿?

Java 有垃圾回收器(GC),它会定期整理内存,对象的位置可能会被搬走。

所以 C++ 拿到的不是对象本身,而是一张**“寄存牌”**(JNI 叫"引用",类型是jobject):

C++ 拿着寄存牌(jobject) → 交给翻译官 → 翻译官去找到对象当前的真实位置

寄存牌有两种

类型比喻有效期
局部引用(默认)一次性寄存牌这次 native 函数返回后就作废
全局引用长期寄存牌一直有效,直到你主动注销

典型错误

staticjobject gCallback;// 想把 Java 对象存起来,以后再用extern"C"JNIEXPORTvoidJNICALLJava_com_example_Foo_setCallback(JNIEnv*env,jobject thiz,jobject callback){gCallback=callback;// ❌ 一次性寄存牌,函数返回就作废了gCallback=env->NewGlobalRef(callback);// ✅ 换成长期寄存牌}// 不用的时候env->DeleteGlobalRef(gCallback);// ✅ 记得注销,否则泄漏

记住一句话:要在函数之外继续用的 Java 对象,必须转成全局引用。


六、难题 3:翻译官是每个线程"专属"的

JNIEnv(翻译官)每个线程有自己的一个,不能借给别的线程用。

可以把它想成工牌:每个员工一张,不能借给别人。

情况怎么办
从 Java 调用进来的 C++ 函数参数里已经给了env,直接用
C++ 自己创建的新线程没有工牌,要先去"办一张":AttachCurrentThread
JavaVM*gVM;// 虚拟机对象:整个进程只有一个,可以随便保存// 在 C++ 自己创建的线程里JNIEnv*env;gVM->AttachCurrentThread(&env,nullptr);// 办工牌// ... 使用 env 调用 Java ...gVM->DetachCurrentThread();// 离职前交还工牌

JavaVM可以全局保存,JNIEnv不能跨线程保存。

Unity 里也一样:在 C# 的子线程中调用AndroidJavaObject之前,要先调用AndroidJNI.AttachCurrentThread(),原因就是这个。


七、反过来:C++ 调用 Java

比如 C++ 处理完数据,要通知 Java。

假设 Java 有这样一个方法:

publicclassCallback{publicvoidonResult(intcode){...}}

C++ 调用它分三步:找类 → 找方法 → 调用。

jclass clazz=env->GetObjectClass(callbackObj);// ① 找到类jmethodID mid=env->GetMethodID(clazz,"onResult","(I)V");// ② 找到方法env->CallVoidMethod(callbackObj,mid,200);// ③ 调用,传入 200

第 ② 步里的"(I)V"叫方法签名,用来描述参数和返回值:

(I)V = ( 参数是 int ) 返回 void
字母含义
Iint
Jlong
Zboolean
Vvoid
Ljava/lang/String;String

不用手写签名:对编译出的 class 文件执行javap -s 类名可以直接看到。


八、难题 4:Java 抛异常,C++ 不会停

Java 方法如果抛了异常,回到 C++ 时不会中断 C++ 代码,C++ 会若无其事地继续往下跑。

所以调用完 Java 方法后,要主动检查:

env->CallVoidMethod(obj,mid);if(env->ExceptionCheck()){// 检查刚才是否出了异常env->ExceptionClear();// 处理掉return;// 不要继续执行}

九、新手最常遇到的 4 个报错

报错原因解决
UnsatisfiedLinkError: dlopen failed.so 没打进包,或 CPU 架构不对(如缺少arm64-v8a)检查 APK 里的lib/目录
UnsatisfiedLinkError: No implementation foundC++ 函数名拼错,或漏了extern "C"对照包名、类名、方法名检查
程序在调用时崩溃,提示引用无效把局部引用存起来后再用改用NewGlobalRef
local reference table overflow循环里创建了大量局部引用没释放循环内调用DeleteLocalRef

十、一张图总结

Java / Kotlin C / C++ ┌─────────────────────────┐ ┌─────────────────────────┐ │ native int add(a, b); │ ───────→ │ Java_..._add(env, ...) │ │ │ JNI 调用 │ │ │ void onResult(int code) │ ←─────── │ env->CallVoidMethod(...) │ └─────────────────────────┘ └─────────────────────────┘ 翻译官 JNIEnv 负责: ① 转换类型(String ↔ char*) ② 管理"寄存牌"(局部引用 / 全局引用) ③ 每个线程一个,新线程要先 Attach ④ 异常不会自动中断,要主动检查

一句话总结

JNI 就是 Java 和 C++ 之间的翻译官:Java 用native声明方法,C++ 按约定的名字实现;C++ 通过JNIEnv来操作 Java 的字符串、对象和方法。
新手只要记住三条规则:借的字符串要还、要长期保存的对象转成全局引用、新线程先 Attach,就能避开大部分坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询