
一、JNI 是什么Android 应用主要用Java / Kotlin编写但有些代码是用C / C编写的比如游戏引擎Unity 的核心就是 C音视频编解码已有的 C 代码库这两种语言不能直接互相调用。JNIJava Native Interface就是让它们能互相调用的一套规则和工具。二、打个比方翻译官想象一个中国人和一个美国人合作比喻实际中国人Java 代码美国人C/C 代码翻译官JNI他们之间有几个沟通障碍翻译官要一一解决说的话不一样→ 数据类型不同Java 的String和 C 的char*不一样管东西的方式不一样→ Java 有垃圾回收器自动管理内存C 要手动管理出错的处理方式不一样→ 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 这边写实现#includejni.hexternCJNIEXPORT jint JNICALLJava_com_example_MathUtil_add(JNIEnv*env,jclass clazz,jint a,jint b){returnab;}这一大串看着吓人拆开看其实很简单部分意思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 正常调用intresultMathUtil.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 里写成intjintlongjlongfloatjfloatbooleanjboolean本质上就是同一种数字换了个名字。字符串需要翻译Java 的String和 C 的char*在内存里的样子完全不同必须让翻译官转换// Java: public static native void hello(String name);externCJNIEXPORTvoidJNICALLJava_com_example_MathUtil_hello(JNIEnv*env,jclass clazz,jstring name){constchar*cNameenv-GetStringUTFChars(name,nullptr);// ① 借Java 字符串 → C 字符串printf(你好%s\n,cName);env-ReleaseStringUTFChars(name,cName);// ② 还用完必须还回去}可以理解为向翻译官借一份 C 能看懂的副本用完要还。忘记还就会内存泄漏。反过来C 字符串变成 Java 字符串jstring resultenv-NewStringUTF(来自 C 的问候);五、难题 2Java 的对象C 不能直接拿这是 JNI 最容易出错的地方。为什么不能直接拿Java 有垃圾回收器GC它会定期整理内存对象的位置可能会被搬走。所以 C 拿到的不是对象本身而是一张**“寄存牌”**JNI 叫引用类型是jobjectC 拿着寄存牌jobject → 交给翻译官 → 翻译官去找到对象当前的真实位置寄存牌有两种类型比喻有效期局部引用默认一次性寄存牌这次 native 函数返回后就作废全局引用长期寄存牌一直有效直到你主动注销典型错误staticjobject gCallback;// 想把 Java 对象存起来以后再用externCJNIEXPORTvoidJNICALLJava_com_example_Foo_setCallback(JNIEnv*env,jobject thiz,jobject callback){gCallbackcallback;// ❌ 一次性寄存牌函数返回就作废了gCallbackenv-NewGlobalRef(callback);// ✅ 换成长期寄存牌}// 不用的时候env-DeleteGlobalRef(gCallback);// ✅ 记得注销否则泄漏记住一句话要在函数之外继续用的 Java 对象必须转成全局引用。六、难题 3翻译官是每个线程专属的JNIEnv翻译官每个线程有自己的一个不能借给别的线程用。可以把它想成工牌每个员工一张不能借给别人。情况怎么办从 Java 调用进来的 C 函数参数里已经给了env直接用C 自己创建的新线程没有工牌要先去办一张AttachCurrentThreadJavaVM*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 clazzenv-GetObjectClass(callbackObj);// ① 找到类jmethodID midenv-GetMethodID(clazz,onResult,(I)V);// ② 找到方法env-CallVoidMethod(callbackObj,mid,200);// ③ 调用传入 200第 ② 步里的(I)V叫方法签名用来描述参数和返回值(I)V ( 参数是 int ) 返回 void字母含义IintJlongZbooleanVvoidLjava/lang/String;String不用手写签名对编译出的 class 文件执行javap -s 类名可以直接看到。八、难题 4Java 抛异常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对照包名、类名、方法名检查程序在调用时崩溃提示引用无效把局部引用存起来后再用改用NewGlobalReflocal 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就能避开大部分坑。