文章内容
- 一、问题与结论
- 1.1 传统 JNI 与 fbjni 的四组写法对比
- 二、整体架构
- 三、初始化与动态注册
- 3.1 facebook::jni::initialize 做了什么
- 3.2 Environment::initialize
- 3.3 so 加载顺序:先加载 fbjni,再加载业务库的 so
- 3.4 makeNativeMethod 怎样把签名和入口绑在一起
- 四、智能引用
- 五、类型映射与编译期描述符推导
- 5.1 JavaClass 的 CRTP 入口
- 5.2 jtypetraits 把 C++ 类型映射到 descriptor
- 5.3 demo 里的 JPoint / JEventListener
- 六、HybridClass:Java 对象背后的 C++ 状态
- 6.1 HybridData 保存 native 指针
- 6.2 HybridClass 的两部分
- 6.3 两条创建路径
- 6.4 cthis() 取回 C++ 指针
- 6.5 NativeCounter 实现
- 七、异常翻译机制
- 7.1 C++ 异常转 Java 异常
- 7.2 Java 异常进入 C++
- 7.3 异常映射表
- 八、线程管理
- 8.1 JNIEnv 缓存在 TLS 里
- 8.2 ThreadScope 只清理自己挂载的线程
- 8.3 ThreadScope::WithClassLoader 解决应用类加载器问题
- 8.4 跨线程引用升格
- 九、Demo测试
- 9.1 从 UI 到 JNI 入口
- 9.2 registerNatives
- 9.3 native 调用经过哪些方法
- 十、Demo测试fbjni的三种主要模式
- 十一、小结
一、问题与结论
原生 JNI 的 API 数量有限,麻烦集中在边界状态:方法签名要手写字符串,引用所有权靠手动维护,C++ 异常不能越过 JNI 入口,自己创建的 native 线程还要先挂载到 JVM。任何一处漏掉,错误都可能推迟到运行时才出现,表现为 NoSuchMethodError、引用泄漏、std::terminate,或者拿不到可用的 JNIEnv*。
fbjni 不替换原有 JNI 接口,它在 C API 之上加了一层 C++ 类型系统和 RAII,代码分布在几个文件里:
detail/Registration.h:makeNativeMethoddetail/References.h:local_ref/global_ref/weak_ref/alias_refdetail/Exceptions.cpp:translatePendingCppExceptionToJavaExceptiondetail/Environment.cpp:ThreadScope
1.1 传统 JNI 与 fbjni 的四组写法对比
- 签名从手写字符串换成编译期推导。传统注册要同时维护方法名、签名字符串和函数指针:
// 传统 JNI:签名字符串手写,三项对不齐只能等运行时 NoSuchMethodErrorstatic jint nativeSum(JNIEnv*, jobject, jint a, jint b) { return a + b; }static JNINativeMethod methods[] = { {"sum", "(II)I", (void*)nativeSum},};env->RegisterNatives(clazz, methods, 1);fbjni 把签名绑定到 C++ 函数指针类型。makeNativeMethod 仍然产出 JNINativeMethod,但签名来自 makeDescriptor(&func),不再单独写一份签名字符串:
// fbjni:签名从 nativeSum 的函数指针类型直接推导,写错类型编译不过jint nativeSum(alias_ref<jobject>, jint a, jint b) { return a + b; }registerNatives("com/example/Foo", { makeNativeMethod("sum", nativeSum),});- 引用的释放从手动配对换成 RAII。裸 JNI 里,引用类型和释放责任都不在类型上:
// 传统 JNI:New/Delete 要手动配对jobject globalObj = env->NewGlobalRef(localObj);// ... 使用 globalObjenv->DeleteGlobalRef(globalObj); // 忘记这一行就是引用泄漏fbjni 的 owning reference 在析构时调用对应的 JNI release API:
// fbjni:生命周期绑定到 C++ 对象,析构自动 DeleteGlobalRefglobal_ref<jobject> globalObj = make_global(localObj);// 离开作用域自动释放,不需要手写清理代码- 异常在入口被捕获。JNI 入口不能让 C++ 异常继续向外传播:
// 传统 JNI:C++ 异常裸露到 JNI 边界,进程直接崩溃extern "C" JNIEXPORT void JNICALLJava_com_example_Foo_nativeMethod(JNIEnv*, jobject) { throw std::runtime_error("boom"); // std::terminate}fbjni 的动态注册入口会包一层 FunctionWrapper,在那里捕获异常并设置 Java pending exception:
// fbjni:FunctionWrapper 的 catch(...) 兜底,自动转成 RuntimeExceptionvoid nativeMethod(alias_ref<jobject>) { throw std::runtime_error("boom"); // 被翻译成 java.lang.RuntimeException}- 线程挂载交给
ThreadScope。裸 JNI 要自己维护AttachCurrentThread和DetachCurrentThread的配对:
// 传统 JNI:手动 Attach/Detach,忘记 Detach 或提前退出都会出问题JNIEnv* env;vm->AttachCurrentThread(&env, nullptr);// ... 调用 Java 方法vm->DetachCurrentThread(); // 忘记调用,或者中途 return 跳过这一行ThreadScope 把“是否由自己挂载”记在对象里,析构时只清理自己造成的状态。跨线程再访问应用类时还要用 WithClassLoader,原因见第八章:
// fbjni:RAII 守卫,析构自动 Detach,还能拿到应用类加载器ThreadScope::WithClassLoader([&] { javaObj->someMethod(); // 挂载和类加载器都由 fbjni 处理});二、整体架构
从依赖方向看,fbjni 可以按三层理解。

底层是 References、Environment、Exceptions。detail/References.h 定义 local_ref、global_ref、weak_ref、alias_ref,把 JNI 引用的所有权写进 C++ 类型。detail/Environment.cpp 保存全局 JavaVM*,管理线程本地的 JNIEnv* 缓存,并提供 ThreadScope。detail/Exceptions.h/.cpp 负责 C++ 异常和 Java Throwable 之间的转换。
中间层是 Meta 和 CoreClasses。detail/Meta.h:jtype_traits 把 C++ 类型映射到 JNI descriptor;detail/CoreClasses.h:JavaClass 给 Java 类提供 CRTP 包装,用户自定义 wrapper 都继承这一路径。
上层是 Registration 和 Hybrid。detail/Registration.h:makeNativeMethod 负责生成 JNINativeMethod;detail/Registration-inl.h:FunctionWrapper 和 FunctionWrapperWithJniEntryPoint 负责入口包装。detail/Hybrid.h:HybridClass 处理 Java 对象持有 C++ 对象的场景。
应用层用一个 demo 走通 sumPrimitives。它一次调用要穿过 JNI 边界两次(Java 进 C++ 一次,C++ 再调回 Java 的 doubler 一次),把动态注册、签名推导和反向回调串在同一条链上。
Java 侧定义 DemoCase,调用 NativeBridge.sumPrimitives:
DemoCase( "Primitives", "sumPrimitives", "byte/short/int/long into C++, which calls Java's doubler() back",) { "sumPrimitives(1, 2, 3, 4) = ${NativeBridge.sumPrimitives(1, 2, 3, 4)} " + "(1+2+3+4 + doubler(3))"},NativeBridge.java 里对应一个 native 声明,外加一个留给 C++ 回调的静态方法 doubler:
public static native long sumPrimitives(byte b, short s, int i, long l); @DoNotStripstatic long doubler(int i) { return 2L * i;}C++ 侧实现是 native_bridge.cpp:43-52。参数直接写 C++ 类型,不再出现 JNIEnv*;getStaticMethod<jlong(jint)>("doubler") 在编译期自动把 jlong(jint) 推导成 JNI 签名:
jlong JNativeBridge::sumPrimitives( alias_ref<JClass> clazz, jbyte b, jshort s, jint i, jlong l) { // C++ -> Java: look the static method up once, then call it. static const auto doubler = clazz->getStaticMethod<jlong(jint)>("doubler"); return b + s + i + l + doubler(clazz, i);}函数指针不再依靠 Java_com_example_... 命名约定,注册表按名字把它绑到 JNativeBridge::sumPrimitives,位置在 native_bridge.cpp:465:
makeNativeMethod("sumPrimitives", JNativeBridge::sumPrimitives),三、初始化与动态注册
3.1 facebook::jni::initialize 做了什么
所有注册工作最后都会经过 fbjni/cxx/fbjni/fbjni.cpp:26 的 facebook::jni::initialize:
jint initialize(JavaVM* const vm, std::function<void()>&& init_fn) noexcept { Environment::initialize(vm); try { init_fn(); } catch ([[maybe_unused]] const std::exception& e) { FBJNI_LOGE("error %s", e.what()); translatePendingCppExceptionToJavaException(); } catch (...) { translatePendingCppExceptionToJavaException(); } return JNI_VERSION_1_6;}Environment::initialize(vm) 先保存 JavaVM*,后续 Environment::current() 和线程挂载都从这个 VM 出发。init_fn() 是业务库传进来的注册逻辑,比如后面的 JNativeBridge::registerNatives()。注册过程中如果查类、查方法或绑定 native 方法失败,fbjni 可能抛出 C++ 异常;这个异常不能从 JNI_OnLoad 继续向外冒,因为 initialize 声明了 noexcept,所以必须在内部捕获,再调用 translatePendingCppExceptionToJavaException() 把它设置成 Java pending exception,让 Java 侧按普通异常路径抛出去。
3.2 Environment::initialize
detail/Environment.cpp:116 实现了一次性初始化逻辑:
JavaVM* ROAI g_vm = nullptr; struct EnvironmentInitializer final { EnvironmentInitializer(JavaVM* const vm) noexcept { FBJNI_ASSERT(!g_vm); FBJNI_ASSERT(vm); g_vm = vm; }}; /* static */void Environment::initialize(JavaVM* const vm) noexcept { static EnvironmentInitializer init(vm);}局部静态变量 init 承担一次性初始化。C++11 起静态局部变量初始化有线程安全保证,因此同一进程里第一次调用会设置 g_vm,后续调用不会重新构造 EnvironmentInitializer。
3.3 so 加载顺序:先加载 fbjni,再加载业务库的 so
demo 的 FooJniLoader.java:
public static synchronized void load() { if (sLoaded) { return; } if (!NativeLoader.isInitialized()) { NativeLoader.init(new SystemDelegate()); } NativeLoader.loadLibrary("fbjni"); NativeLoader.loadLibrary("foojni"); sLoaded = true;}libfbjni.so 先进入进程,触发 fbjni 自己的 fbjni/cxx/fbjni/OnLoad.cpp:32:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return facebook::jni::initialize(vm, [] { HybridDataOnLoad(); JNativeRunnable::OnLoad(); ThreadScope::OnLoad(); registerNatives( "com/facebook/jni/FbjniExceptionConfig", {makeNativeMethod( "setFixReleaseThrowableLeak", setFixReleaseThrowableLeak_jni)}); });}这一步注册 HybridData、NativeRunnable、ThreadScope 需要的基础 native 方法。随后 libfoojni.so 的 app/src/main/cpp/foojni_onload.cpp:14-19 注册 demo 自己的两个入口类:
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { return facebook::jni::initialize(vm, [] { foojni::JNativeBridge::registerNatives(); foojni::NativeCounter::registerNatives(); });}FooJniLoader.java(Demo) 的注释也说明了顺序约束:在调用任何触碰 HybridData 的业务代码之前,libfbjni.so 必须已经加载。libfoojni.so 再调用 facebook::jni::initialize 时,Environment::initialize 不会重设 g_vm,实际执行的是业务闭包里的 JNativeBridge::registerNatives() 和 NativeCounter::registerNatives()。

3.4 makeNativeMethod 怎样把签名和入口绑在一起
detail/Registration.h:88-107 是动态注册的入口宏:
#define makeNativeMethod2(name, func) \ {const_cast<char*>(name ""), \ const_cast<char*>(::facebook::jni::detail::makeDescriptor(&func).c_str()), \ ::facebook::jni::detail::exceptionWrapJNIMethod<decltype(&func), &func>( \ &func)} #define makeNativeMethodN(a, b, c, count, ...) makeNativeMethod##count#define makeNativeMethod(...) \ FBJNI_MACRO_EXPAND(makeNativeMethodN(__VA_ARGS__, 3, 2)(__VA_ARGS__))makeNativeMethod2 同时生成三个字段:方法名、descriptor、函数入口。descriptor 来自 detail::makeDescriptor(&func);入口来自 exceptionWrapJNIMethod<decltype(&func), &func>。事实层面,它传给 JVM 的仍然是一个普通的 JNINativeMethod;推断层面,它减少的是“函数指针改了但签名字符串忘改”的风险。
自动参数转换的包装在 detail/Registration-inl.h:85-115:
template <typename F, typename C, typename R, typename... Args>struct FunctionWrapper { using jniRet = typename Converter<R>::jniType; static jniRet call( JNIEnv* env, jobject obj, typename Converter<Args>::jniType... args, F funcPtr) { detail::JniEnvCacher jec(env); try { return CallWithJniConversions<F, R, JniType<C>, Args...>::call( static_cast<JniType<C>>(obj), args..., funcPtr); } catch (...) { translatePendingCppExceptionToJavaException(); return CreateDefault<jniRet>::create(); } }};入口收到 JVM 传进来的 JNIEnv*、jobject 和 JNI 参数类型后,先构造 JniEnvCacher。业务函数在 CallWithJniConversions::call 里执行,参数会按 Converter<T> 转换成 C++ 侧声明的类型。catch (...) 是异常边界:一旦业务函数抛异常,fbjni 设置 Java pending exception,并返回该 JNI 返回类型的默认值。
四、智能引用
JNI 裸指针 jobject 本身看不出引用种类,也看不出释放责任。fbjni 在 detail/References.h 里给出四种常用引用:local_ref<T> 持有局部引用,global_ref<T> 持有全局引用,weak_ref<T> 持有弱全局引用,alias_ref<T> 只是非 owning 视图。
几个符号标出了所有权边界:adopt_local 声明在 detail/References.h:107,表示接管已有 local reference;weak_ref 类从 detail/References.h:415 开始,提供弱全局引用语义;alias_ref 从 detail/References.h:584 开始,不拥有底层引用。换成函数签名时,alias_ref<T> 表示调用期间借用,global_ref<T> 表示对象会跨过当前 JNI 调用栈继续存活。

demo 在 app/src/main/cpp/native_bridge.cpp:21-29 保存了两个长生命周期引用容器:
global_ref<JString>& globalStash() { static auto* stash = new global_ref<JString>(); return *stash;} weak_ref<jobject>& weakStash() { static auto* stash = new weak_ref<jobject>(); return *stash;}这段代码故意把容器放在 new 出来的对象里,使其不依赖进程退出时的静态析构。源码注释给出的理由是:进程退出或库卸载时再跑 JNI release 不安全。stashGlobal 在 native_bridge.cpp:385-391 把 alias_ref<JString> 升格成 global_ref<JString>;readGlobal 在 393-399 用 make_local 返回局部引用。弱引用路径在 stashWeak 和 isWeakAlive,读之前通过 lockLocal() 判断对象是否还活着。
五、类型映射与编译期描述符推导
5.1 JavaClass<T> 的 CRTP 入口
用户包装 Java 类时,入口是 detail/CoreClasses.h:209 的 JavaClass<T>:
// Given a class:// struct Foo : JavaClass<Foo> {// static constexpr auto kJavaDescriptor = "Lcom/example/package/Foo;";// };template <typename T, typename Base = JObject, typename JType = void>class JavaClass : public Base { using JObjType = typename detail::JTypeFor<T, Base, JType>; public: using _javaobject = typename JObjType::_javaobject; using javaobject = typename JObjType::javaobject; using JavaBase = JavaClass; static alias_ref<JClass> javaClassStatic(); static local_ref<JClass> javaClassLocal(); protected: template <typename... Args> static local_ref<T> newInstance(Args... args) { return detail::newInstance<T>(args...); } javaobject self() const noexcept;};T 是用户自己的 wrapper 类型。
例如JPoint : JavaClass<JPoint> 这种 CRTP 写法让 javaClassStatic()、javaClassLocal()、newInstance 都能和具体 Java 类型绑定。kJavaDescriptor 由子类提供。
不过这个包装有边界:JavaClass 能在 C++ 类型层面固定 descriptor 和方法签名,Java 类是否真的存在、构造函数是否匹配,仍然要到运行时查 jclass、jmethodID 时才能确认。CoreClasses.h:223-229 的注释也提醒,newInstance 只能在运行时检查 Java 构造函数是否匹配。
5.2 jtype_traits 把 C++ 类型映射到 descriptor
detail/Meta.h:283-322 是 descriptor 推导的核心:
template <typename T>struct jtype_traits { private: using Repr = ReprType<T>; static constexpr auto descriptor() { constexpr auto len = Repr::kJavaDescriptor ? detail::constexpr_strlen(Repr::kJavaDescriptor) : Repr::get_instantiated_java_descriptor().size(); // ... } public: static constexpr descriptorType kDescriptor = descriptor(); static constexpr basenameType kBaseName = base_name();};用户自定义 JavaClass 类型走 Repr::kJavaDescriptor 或 get_instantiated_java_descriptor();基础类型靠宏特化:
DEFINE_FIELD_AND_ARRAY_TRAIT(jboolean, Z)DEFINE_FIELD_AND_ARRAY_TRAIT(jbyte, B)DEFINE_FIELD_AND_ARRAY_TRAIT(jint, I)DEFINE_FIELD_AND_ARRAY_TRAIT(jlong, J)// ...因此 jint 对应 I,jlong 对应 J,数组类型对应带 [ 的 descriptor。makeDescriptor(&func) 拼的就是参数和返回值的 jtype_traits<T>::kDescriptor。如果某个 C++ 参数类型没有映射,编译期就会失败;
如果 Java 侧实际方法签名和推导结果不一致,失败会发生在运行时查方法或注册 native 方法时异常。
5.3 demo 里的 JPoint / JEventListener
app/src/main/cpp/java_types.h:18-78 给出了普通 Java 类 wrapper:
struct JPoint : JavaClass<JPoint> { static constexpr auto kJavaDescriptor = "Lcom/example/foojni/nativebridge/Point;"; static local_ref<JPoint> create(jint x, jint y) { return newInstance(x, y); } jint getX() const { static const auto field = javaClassStatic()->getField<jint>("x"); return getFieldValue(field); } jint sum() const { static const auto method = javaClassStatic()->getMethod<jint()>("sum"); return method(self()); }};getField<jint>("x") 和 getMethod<jint()>("sum") 把字段类型、方法返回值和参数列表写进模板参数。查到的 fieldID、methodID 用 static const auto 缓存,避免每次调用都重新查。JEventListener 在同一文件 84-105,也是继承 JavaClass<JEventListener>,接口和普通类在 fbjni 侧没有不同包装路径:能拿到 class 和 method descriptor,就能按同样方式调用。
六、HybridClass:Java 对象背后的 C++ 状态
JavaClass<T> 适合“C++ 里拿一个类型安全的 Java 句柄”。如果 Java 对象要拥有一份 C++ 状态,比如 native 资源、计数器或长期保存的 listener,就需要 HybridClass<T>。
6.1 HybridData 保存 native 指针
detail/Hybrid.h:36-40 定义了桥接用的 Java 类 wrapper:
struct HybridData : public JavaClass<HybridData> { constexpr static auto kJavaDescriptor = "Lcom/facebook/jni/HybridData;"; static local_ref<HybridData> create(); static local_ref<HybridData> create(BaseHybridClass* ptr);};Java 侧对应 com.facebook.jni.HybridData,内部保存 native pointer。demo 的 app/src/main/java/com/example/foojni/nativebridge/NativeCounter.java:21 持有字段 private final HybridData mHybridData;,字段名必须是 mHybridData,因为 fbjni 会按约定查这个字段。
6.2 HybridClass<T> 的两部分
detail/Hybrid.h:167-269 的主要结构如下:
template <typename T, typename Base = detail::BaseHybridClass>class HybridClass : public detail::HybridTraits<Base>::CxxBase { public: struct JavaPart : JavaClass<JavaPart, typename detail::HybridTraits<Base>::JavaBase> { using HybridType = T; T* cthis() const; friend class HybridClass; friend T; }; using jhybridobject = typename JavaPart::javaobject; protected: static local_ref<detail::HybridData> makeHybridData( std::unique_ptr<T> cxxPart) { auto hybridData = detail::HybridData::create(); setNativePointer(hybridData, std::move(cxxPart)); return hybridData; } public: template <typename... Args> static local_ref<JavaPart> newObjectCxxArgs(Args&&... args) { auto cxxPart = std::unique_ptr<T>(new T(std::forward<Args>(args)...)); // ... 走 setNativePointer 或 JavaPart::newInstance(hybridData) }};T 是 C++ 业务对象,JavaPart 是与 Java 对象对应的 JavaClass。这种拆分让 C++ 状态和 Java 类型标识分开:业务类 NativeCounter 继承 HybridClass<NativeCounter>,Java 侧 NativeCounter 通过 HybridData 持有它。
6.3 两条创建路径
detail/Hybrid.h:254:newObjectCxxArgs 是 C++ 驱动路径:先构造 T,再创建 Java part,并把 HybridData 放进去。demo 的 NativeCounter::createFromCpp 位于 app/src/main/cpp/native_counter.cpp:25-30,调用 newObjectCxxArgs(initial)。
Java 驱动路径在 NativeCounter.java:24-25 和 native_counter.cpp:17-23:Java 构造函数调用 initHybrid(initial),C++ 侧返回 makeCxxInstance(initial) 创建的 HybridData。
两条创建路径方向不同,但最终结构一致:Java NativeCounter 对象都会通过 HybridData 关联一个对应的 C++ NativeCounter 实例。
6.4 cthis() 取回 C++ 指针
Java 引用进入 C++ 成员方法时,需要从 mHybridData 里取回 T*。detail/Hybrid.h:342-349:
template <typename T, typename B>inline T* HybridClass<T, B>::JavaPart::cthis() const { detail::BaseHybridClass* result = nullptr; static const auto hybridDataField = detectHybrid<T, B>(this->getClass()); result = getHybridDataFromField(this, hybridDataField); return static_cast<T*>(result);}这里用 static_cast<T*>,不是 dynamic_cast。源码注释给出的理由是默认 -fno-rtti。所以类型安全依赖注册路径和 Java 对象类型约束:这个 Java 对象必须确实由对应的 HybridClass<T> 管理。
6.5 NativeCounter 实现
app/src/main/cpp/native_counter.cpp 是 demo 中的 HybridClass 示例:
std::atomic<int> NativeCounter::sLiveInstances{0}; NativeCounter::NativeCounter(jint initial) : value_(initial) { sLiveInstances.fetch_add(1, std::memory_order_relaxed);} NativeCounter::~NativeCounter() { sLiveInstances.fetch_sub(1, std::memory_order_relaxed);} local_ref<NativeCounter::jhybriddata> NativeCounter::initHybrid( alias_ref<jhybridobject>, jint initial) { return makeCxxInstance(initial);} local_ref<NativeCounter::jhybridobject> NativeCounter::createFromCpp( alias_ref<jclass>, jint initial) { return newObjectCxxArgs(initial);} void NativeCounter::addFrom(alias_ref<jhybridobject> other) { value_ += cthis(other)->value_;} void NativeCounter::registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", NativeCounter::initHybrid), makeNativeMethod("createFromCpp", NativeCounter::createFromCpp), makeNativeMethod("add", NativeCounter::add), // ... });}这段同时覆盖几种场景:initHybrid 是 Java 构造函数触发的路径,createFromCpp 是 C++ 工厂路径,addFrom 接收另一个 Java hybrid 引用后用 cthis(other) 取出它的 C++ 部分。sLiveInstances 在构造和析构时增减,配合 NativeCounter.java:72-79 的 close()/isValid() 可以观察 C++ 半身是否已经销毁。

七、异常翻译机制
7.1 C++ 异常转 Java 异常
detail/Exceptions.cpp:306-346 是 C++ 到 Java 的转换表:
local_ref<JThrowable> convertCppExceptionToJavaException( std::exception_ptr ptr) { local_ref<JThrowable> current; try { std::rethrow_exception(ptr); } catch (JniException& ex) { current = ex.releaseThrowable(); } catch (const std::ios_base::failure& ex) { current = JIOException::create(ex.what()); } catch (const std::invalid_argument& ex) { current = JIllegalArgumentException::create(ex.what()); } catch (const std::bad_alloc& ex) { current = JOutOfMemoryError::create(ex.what()); } catch (const std::out_of_range& ex) { current = JArrayIndexOutOfBoundsException::create(ex.what()); } catch (const std::runtime_error& ex) { current = JRuntimeException::create(ex.what()); } catch (const std::exception& ex) { current = JCppException::create(ex.what()); } catch (...) { // 兜底成 JUnknownCppException,尽量带上 RTTI 里的类型名 } addCppStacktraceToJavaException(current, ptr); return current;}除了常见的 std::runtime_error、std::invalid_argument 等分支,还包含 std::system_error -> JCppSystemErrorException 和 const char* -> JUnknownCppException。
demo 的 native_bridge.cpp:283-293 分别抛 std::runtime_error 和 std::invalid_argument,Java 侧 case 在 FbjniDemo.kt:163-172。如果要抛应用自定义 Java 异常,demo 使用 throwNewJavaException,位置是 native_bridge.cpp:295-300:
void JNativeBridge::throwDemoExceptionFromCpp( alias_ref<JClass>, std::string m) { throwNewJavaException( "com/example/foojni/nativebridge/DemoException", "%s", m.c_str());}throwNewJavaException 会先构造包装了 Java throwable 的 C++ 异常。把 pending exception 设置回 JNI 环境detail/Exceptions.cpp:378:translatePendingCppExceptionToJavaException,由 FunctionWrapper::call 的 catch (...) 调用。
7.2 Java 异常进入 C++
反方向由 detail/Exceptions.h:77:JniException 承担:
class JniException : public std::exception { public: JniException(); explicit JniException(alias_ref<jthrowable> throwable); local_ref<JThrowable> getThrowable() const noexcept; local_ref<JThrowable> releaseThrowable() noexcept; const char* what() const noexcept override; private: global_ref<JThrowable> throwable_;};当 C++ 调 Java 方法后发现 Java pending exception,fbjni 会抛出 JniException。它继承 std::exception,但内部保存的是 global_ref<JThrowable>,因此 C++ 侧可以读取原 Java 异常。
Demo演示:
std::string JNativeBridge::catchJavaException(alias_ref<JClass> clazz) { try { static const auto method = clazz->getStaticMethod<void()>("alwaysThrows"); method(clazz); } catch (const JniException& e) { auto throwable = e.getThrowable(); auto message = throwable->getMessage(); return "caught:" + throwable->getClass()->toString() + ":" + (message ? message->toStdString() : std::string("<none>")); } return "no-exception";}alwaysThrows 位于 NativeBridge.java:141-144,抛出 IllegalStateException。C++ 侧捕获 JniException 后读取 Throwable 的 class 和 message。
7.3 异常映射表
按 detail/Exceptions.cpp:306-346,源码的映射关系如下:
| C++ 异常 | Java 异常 |
|---|---|
JniException |
复用原 Throwable |
std::invalid_argument |
IllegalArgumentException |
std::bad_alloc |
OutOfMemoryError |
std::out_of_range |
ArrayIndexOutOfBoundsException |
std::ios_base::failure |
IOException |
std::system_error |
CppSystemErrorException(fbjni 自定义) |
std::runtime_error |
RuntimeException |
其他 std::exception |
CppException(fbjni 自定义) |
const char* |
UnknownCppException |
| 未知类型 | UnknownCppException |

八、线程管理
8.1 JNIEnv* 缓存在 TLS 里
JNIEnv* 不能跨线程使用。fbjni 在 detail/Environment.cpp:144-179 建立线程本地缓存:
tls_key_t getTLKey() { static tls_key_t key = makeKey(); return key;} inline JNIEnv* cachedOrNull() { detail::TLData* pdata = getTLData(getTLKey()); return (pdata ? pdata->env : nullptr);}FunctionWrapper::call 进入时构造 JniEnvCacher,把本次 JNI 入口带进来的 JNIEnv* 放进 TLS;退出时析构清掉。这样业务函数深处可以用 Environment::current(),不需要层层传 JNIEnv*。
8.2 ThreadScope 只清理自己挂载的线程
native 自己开的线程需要挂到 JVM。detail/Environment.cpp:268-314:
ThreadScope::ThreadScope() : thisAttached_(false) { if (g_vm == nullptr) { throw std::runtime_error( "fbjni is uninitialized; no thread can be attached."); } JNIEnv* env; auto result = getEnv(&env); if (result == JNI_OK) { return; // 已经挂载过了,什么都不做 } // ... 设置 TLS,调用 attachCurrentThread() data_.attached = true; thisAttached_ = true;} ThreadScope::~ThreadScope() { if (!thisAttached_) { return; } g_vm->DetachCurrentThread(); setTLData(key, nullptr);}构造函数先用 getEnv(&env) 判断当前线程是否已经附着到 JVM;已经附着就直接返回,thisAttached_ 保持 false。仅当当前对象执行了 AttachCurrentThread,析构时才会 DetachCurrentThread。这个设计使嵌套使用 ThreadScope 时不会把外层或 JVM 已有的挂载状态误删。
8.3 ThreadScope::WithClassLoader 解决应用类加载器问题
裸 AttachCurrentThread 后,线程通常只能可靠访问系统类加载器路径。fbjni 的 detail/Environment.cpp:378-385:
void ThreadScope::WithClassLoader(std::function<void()>&& runnable) { if (cachedOrNull() == nullptr) { ThreadScope ts; JThreadScopeSupport::runStdFunction(std::move(runnable)); } else { runnable(); }}没有缓存 JNIEnv* 时,它先创建 ThreadScope,再通过 Java 侧 ThreadScopeSupport.runStdFunction 执行闭包。推断上,这样做的目的是让闭包执行时经过一个由应用类加载器加载的 Java 帧,挂载线程本身只是顺带的结果,后续查应用类更稳。
8.4 跨线程引用升格
`app/src/main/cpp/native_bridge.cpp:
CODE35listener 是 alias_ref<JEventListener>,只借用当前调用栈里的引用。lambda 进入新线程时,原局部引用不能再假定有效,所以先用 make_global(listener)。线程体里使用 ThreadScope::WithClassLoader,是为了在 raw std::thread 上回调应用接口 EventListener。
Java 创建的线程路径不同。native_bridge.cpp:446-454:
CODE36JThread::create 会把 C++ lambda 包成 NativeRunnable,交给 java.lang.Thread 持有。这个线程由 Java 创建,本来就在 JVM 体系内运行,因此和 raw std::thread 那条路径的挂载责任不同。
九、Demo测试
9.1 从 UI 到 JNI 入口
图 ① 从 MainActivity#onCreate:41-50 开始,经 Compose UI 里的 DemoScreen、CaseRow,到 FbjniDemo#runCase-IoAF18A:53-56。runCase 先调用 NativeBridge.ensureLoaded(),这会触发 NativeBridge 的 static initializer,最终进入 FooJniLoader#load:24-33。

9.2 registerNatives
图 ② 开始进入 foojni_onload.cpp#JNI_OnLoad:14,随后执行传给 facebook::jni::initialize 的 lambda,再到 JNativeBridge::registerNatives():462。trace 中能看到注册路径的几个关键调用:
CODE37SimpleFixedString<44>::c_str() 取的是 JNativeBridge::kJavaDescriptor,JClass::registerNatives 最后落到 _JNIEnv::RegisterNatives。宏展开已经在编译阶段把函数、签名和入口准备好。

NativeCounter::registerNatives():81 走 Hybrid 分支:
CODE38
这里的差别在 HybridClass<NativeCounter>::javaClassLocal():它从 T::kJavaDescriptor 去掉开头 L 和结尾 ; 后调用 findClassLocal。两类注册最后仍然进入 _JNIEnv::RegisterNatives,只是 Java class 的取得路径不同。

9.3 native 调用经过哪些方法
图 ④ 到图 ⑥ 对应一次 NativeBridge.sumPrimitives。trace 主干如下:
CODE39
这条链和第三章的 FunctionWrapper 相匹配:FunctionWrapperWithJniEntryPoint::call 是 JNI 可见入口,FunctionWrapper::call 缓存 JNIEnv* 并包住异常,CallWithJniConversions::call 做参数转换,最后才到 JNativeBridge::sumPrimitives。业务函数内部又用 getStaticMethod<jlong(jint)>("doubler") 找 Java 静态方法,并从 C++ 回调 NativeBridge.doubler。



结果再从 native 返回 FbjniDemo.runCase,由 UI 展示:

UML时序图中[native] References-inl.h 的调用次数最高,集中在 alias_ref 构造、ReprStorage 访问和 local_ref 析构。这个是因为:JNI 边界的高频成本主要落在引用包装和生命周期处理上。Registration-inl.h、Meta-inl.h、MetaConvert.h 的出现次数少得多,集中在入口和类型转换处,说明注册、异常包装、参数转换没有扩散进每层业务逻辑。
十、Demo测试fbjni的三种主要模式
JavaClass模式。app/src/main/cpp/java_types.h里的JPoint、JEventListener,以及native_bridge.h:22:JNativeBridge都是 wrapper:它们不拥有 C++ 状态,只给 Java 类、接口、静态方法、实例方法和字段访问提供类型化入口。NativeBridge.java是 Java 侧的静态 native 接口,native_bridge.cpp:462-520用makeNativeMethod一次性注册。HybridClass模式。NativeCounter在native_counter.h:21继承HybridClass<NativeCounter>,Java 侧NativeCounter.java:21持有HybridData mHybridData。它适合 Java 对象拥有 C++ 状态的场景:value_、listener_、析构时的sLiveInstances都在 C++ 实例。动态注册模式。
foojni_onload.cpp:14-19是业务库的 JNI 入口,里面调用两个registerNatives()。项目没有依赖Java_com_example_...命名约定,Java 方法名和 C++ 函数的绑定都在注册表里完成。
十一、小结
开头四个问题在 fbjni 里都有对应的优化点:
- 签名字符串和函数指针的对齐由
jtype_traits、makeDescriptor、makeNativeMethod收敛; - 引用所有权由
local_ref、global_ref、weak_ref、alias_ref四种类型表达; - 跨界的异常由
FunctionWrapper和Exceptions.cpp翻译; - 线程挂载交给
JniEnvCacher、ThreadScope和WithClassLoader。
JNI 边界仍然存在,fbjni 做的是把容易靠记忆维护的部分放进类型、对象生命周期和统一入口。
本文由 AgentWorkFlow 生成