JNI(二):Java 里的一个 long,为什么能找到 C++ 对象?

JNI(二):Java 里的一个 long,为什么能找到 C++ 对象?

上一篇沿着 nativePollOnce() 解决了第一个断点:

text 复制代码
Java native 声明
        ↓ 动态注册
C++ 函数地址

但 C++ 入口里紧接着出现了第二个断点:

cpp 复制代码
NativeMessageQueue* nativeMessageQueue =
        reinterpret_cast<NativeMessageQueue*>(ptr);

Java 传来的是一个 long,C++ 为什么能把它变成 NativeMessageQueue*

如果只回答"mPtr 保存了指针",仍然不够。读者还会继续困惑:

  • Java MessageQueue 和 C++ NativeMessageQueue 是不是同一个对象的两种写法?
  • JNI 会不会自动为 Java 对象创建一个 C++ 对象?
  • jobject 是不是 Java 对象的内存地址?
  • Java 对象被 GC 后,Native 对象会不会一起释放?
  • JNIEnv*jobject 为什么不能随便跨线程、跨调用保存?

本文沿着四个方法建立完整的对象模型:

text 复制代码
nativeInit      创建关系
nativePollOnce  使用关系
nativeWake      使用关系
nativeDestroy   解除关系

**范围说明:**本文主线基于 Android 16 LegacyMessageQueueCombinedMessageQueue 可按平台 flag 和进程条件启用 Concurrent 实现,其队列结构和跨线程 mPtr 保护不属于本文范围;具体判定放在文末版本附注。

一、第一层对象模型:Java MessageQueue 与 NativeMessageQueue 是两个对象

Java 与 C++ 各有自己的对象:

text 复制代码
Managed Heap                         Native Memory / C++ Heap

Java MessageQueue                    NativeMessageQueue
┌──────────────────┐                 ┌────────────────────┐
│ Java 消息链表     │                 │ sp<Looper> mLooper │
│ mBlocked         │                 │ pollOnce()         │
│ mPtr ────────────┼──── 地址值 ────▶│ wake()             │
└──────────────────┘                 └────────────────────┘
       │                                       │
       │ ART / GC 管理                         │ C++ 引用计数管理
       ▼                                       ▼

需要先记住三句话:

Java MessageQueueNativeMessageQueue 是两个独立对象。
JNI 提供跨边界调用和引用规则,但不会自动创造两者的一一对应关系。
mPtr 是 Android Framework 主动设计的关联字段,不是 JNI 自动生成的能力。

这与 Binder、文件描述符等场景的"Java 持有一个数字,再用它找到 Native 资源"在设计思路上相似,但数字的具体含义不同:这里确实是进程内 Native 对象地址转换而来的值;Binder handle 和 fd 则由各自子系统解释,不能混为同一种指针。

二、nativeInit() 怎样创建 Native 对象?

Java 构造 MessageQueue 时:

java 复制代码
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

源码位置:MessageQueue 构造函数

nativeInit() 的 C++ 实现是:

cpp 复制代码
static jlong android_os_MessageQueue_nativeInit(
        JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }

    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

源码位置:android_os_MessageQueue_nativeInit()

按执行顺序翻译:

text 复制代码
1. new NativeMessageQueue()
   在进程的 Native 内存中创建 C++ 对象。

2. incStrong(env)
   手动增加一份与 Java mPtr 生命周期配对的强引用,
   使 NativeMessageQueue 在后续使用期间保持存活。

3. reinterpret_cast<jlong>(nativeMessageQueue)
   把指针值编码进 Java long 可承载的 jlong。

4. Java mPtr = nativeInit()
   Java 对象保存这个值,供后续 native 调用带回 C++。

mPtr 不是 NativeMessageQueue 本体,也不是 Java 对象地址。它只是 Java 侧保存的一枚 Native 句柄;在这个具体实现里,句柄值来自 NativeMessageQueue*

三、第二层对象模型:NativeMessageQueue 内部还关联独立的 Native Looper

构造函数继续建立当前线程与 Native Looper 的关系:

cpp 复制代码
NativeMessageQueue::NativeMessageQueue()
        : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}

源码位置:NativeMessageQueue::NativeMessageQueue()

这段代码也能纠正"JNI 会切到 Native 线程"的误解:

text 复制代码
Looper::getForThread()

取的是当前线程关联的 Native Looper;如果还没有,就创建一个并通过线程局部状态与当前线程关联。不是另起一个线程执行消息循环。

于是对象关系进一步变成:

text 复制代码
当前 Linux Looper 线程
        │
        ├─ Java Looper
        │      └─ Java MessageQueue
        │              └─ long mPtr
        │
        └─ Native Looper
               ▲
               └─ NativeMessageQueue 持有 sp<Looper>

Java MessageQueue 负责 Java 消息链表;Native Looper 负责 fd / eventfd 等待与唤醒,也可以调度 Native MessageEnvelopeNativeMessageQueue 是二者之间的适配层;第三篇会继续展开 Native Looper 怎样合并 Java Message 与 Native Message 的截止时间。

四、使用时为什么能把 jlong 转回指针?

等待路径:

cpp 复制代码
static void android_os_MessageQueue_nativePollOnce(
        JNIEnv* env, jobject obj, jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue =
            reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

唤醒路径:

cpp 复制代码
static void android_os_MessageQueue_nativeWake(
        JNIEnv* env, jclass clazz, jlong ptr) {
    NativeMessageQueue* nativeMessageQueue =
            reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->wake();
}

源码位置:nativePollOnce()nativeWake()

之所以能转回,不是 C++ 能从任意数字推导出对象,而是因为这个值最初就是同一进程中那个指针编码成的 jlong,并且对象在使用期间仍然存活。

这里隐含了三个必须同时成立的前提:

text 复制代码
ptr 来自正确的 nativeInit()
NativeMessageQueue 尚未销毁
调用发生在同一进程地址空间

只要破坏其中一个,reinterpret_cast 仍然可能"转换成功",但解引用时会进入未定义行为,常见结果就是 native crash。

因此,不要把这种做法理解为"所有 Java long 都可以安全变成 C++ 指针"。这是由 Framework 封装、生命周期受控的内部协议。

五、销毁时为什么是 decStrong(),不是简单 delete

Java 侧释放路径:

java 复制代码
private void dispose() {
    if (mPtr != 0) {
        nativeDestroy(mPtr);
        mPtr = 0;
    }
}

C++ 侧:

cpp 复制代码
static void android_os_MessageQueue_nativeDestroy(
        JNIEnv* env, jclass clazz, jlong ptr) {
    NativeMessageQueue* nativeMessageQueue =
            reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->decStrong(env);
}

源码位置:MessageQueue.dispose()nativeDestroy()

对于这里使用默认 strong lifetime 的 NativeMessageQueueincStrong()decStrong() 形成强引用计数上的增加---释放配对,最后一份强引用释放会触发对象销毁;RefBase 另有扩展 weak lifetime,不能把这句话外推成所有模式的绝对规则。传入的 env 不参与强引用计数值的计算,也不是 Java 对象的所有权凭证;启用 DEBUG_REFS 后,它还会作为引用跟踪 id,用于关联和诊断引用增减记录。

这里还要把 Native 侧的两个对象拆开:mPtr 直接关联的是 NativeMessageQueue,不是 Native Looper 本体。MessageQueue 基类通过成员 sp<Looper> mLooper 持有一份 Looper 强引用。NativeMessageQueue 的析构函数体本身为空;对象析构过程中,MessageQueue 基类成员 sp<Looper> mLooper 会自动析构,从而释放该对象持有的这一份 Looper 强引用。它不会直接强制销毁 Looper。与此同时,Looper::setForThread() 还会把同一个 Looper 保存进当前线程的 thread_local static sp<Looper> gThreadLocalLooper

因此,NativeMessageQueue 被销毁,不代表 Native Looper 一定同时销毁。只要线程局部的 gThreadLocalLooper 或其他 sp<Looper> 仍持有强引用,Looper 就可以继续存活;Looper 持有的 mWakeEventFd 句柄要等自身最终析构时才随 unique_fd 成员关闭,而句柄引用的 eventfd 对象与计数器由 Linux 内核管理。源码位置:MessageQueue::mLooperNativeMessageQueue 析构gThreadLocalLooper

这条链实际上包含三类需要分别判断的生命周期:

text 复制代码
Java MessageQueue      → ART / GC 与 Looper 引用关系
NativeMessageQueue     → mPtr 配对的 RefBase 强引用
Native Looper          → mLooper、gThreadLocalLooper 等 sp<Looper> 强引用

这比"Java GC 自动删除 C++ 对象"准确得多:

text 复制代码
Java GC
直接管理 Java MessageQueue 是否可达

C++ RefBase
直接管理 NativeMessageQueue 的强引用计数

dispose / nativeDestroy
协调两边生命周期

Java GC 不知道任意 new NativeMessageQueue() 分配了多少 Native 内存,也不知道何时可以安全释放。Framework 必须提供一条明确的销毁路径。

mPtr 设回 0 也不是装饰,但这一步本身不释放 Native 内存。真正释放配对强引用的是前一行 nativeDestroy(mPtr) → decStrong(env);随后赋值只是把 Java 句柄标记为失效:

text 复制代码
这个 Java 对象已经不再持有可用的 NativeMessageQueue。

还要注意两行之间的执行窗口:decStrong(env) 如果释放了最后一份强引用,NativeMessageQueue 可以在 nativeDestroy() 返回前就完成析构;此时 Java 字段尚未执行 mPtr = 0,仍暂时保留旧地址值。因此,mPtr != 0 不等于 Native 对象此刻必然有效,清零也不是锁、内存屏障或防止并发 use-after-free 的机制。真正的并发安全来自 Legacy 的 MessageQueue 锁与生命周期协议,或 Concurrent 的在途 mPtr 引用保护。

在正常的 Framework 调用路径中,这两套协议会保护实际使用过程;这里强调的只是脱离协议时,单独检查地址值是否非零不能作为对象有效性的并发证明,而不是说正常调用中的 mPtr 天然不可靠。

后续代码可以据此拒绝继续使用旧句柄,降低重复释放或 use-after-free 的风险。

到这里,mPtr 的创建、转换、使用与销毁主链已经闭环。至于 dispose() 为什么会执行,将在"六、dispose() 到底由谁触发?"继续说明。

进阶补充:Native Looper 比队列活得更久,fd callback 为什么不会 UAF?

这一节不影响理解 mPtr 主链,主要用于解释 fd callback 的生命周期安全;第一次阅读可以先跳到下一节。

如果 Native Looper 仍由 TLS 中的 gThreadLocalLooper 持有,而 NativeMessageQueue 已经析构,Looper 里原先注册的 fd callback 会不会回调悬空对象?AOSP 没有让 Looper 直接强持有原始队列 callback,而是插入了一层 WeakLooperCallback

text 复制代码
Native Looper
    │ 强持有
    ▼
WeakLooperCallback 代理
    │ wp<LooperCallback> 弱引用
    ▼
NativeMessageQueue

注册 fd 时创建的是代理对象:

cpp 复制代码
mLooper->addFd(
        fd,
        Looper::POLL_CALLBACK,
        looperEvents,
        sp<WeakLooperCallback>::make(this),
        reinterpret_cast<void*>(events));

事件发生后,代理先尝试把弱引用提升成临时强引用:

cpp 复制代码
int WeakLooperCallback::handleEvent(int fd, int events, void* data) {
    sp<LooperCallback> callback = mCallback.promote();
    if (callback != nullptr) {
        return callback->handleEvent(fd, events, data);
    }
    return 0;
}

这形成了完整的安全分支:

text 复制代码
wp.promote()
├─ 成功:临时取得 sp,保证 handleEvent() 执行期间对象存活
└─ 失败:NativeMessageQueue 已销毁,返回 0
                                      ↓
                 Looper 移除对应 fd 的 callback 注册

因此,Native Looper 可以比 NativeMessageQueue 活得更久,却不会仅凭旧 fd 注册解引用已经销毁的队列。源码位置:WeakLooperCallback 定义与实现setFileDescriptorEvents()Looper 处理 callback 返回值

六、dispose() 到底由谁触发?

在本文核对的 Android 16 LegacyMessageQueue 中,dispose() 有两个直接调用入口:

text 复制代码
正常退出路径:MessageQueue.next() 发现 mQuitting == true
GC 兜底路径:MessageQueue.finalize()

1. 正常退出:quit() 不直接调用 dispose()

Looper.quit()quitSafely() 都只是把退出请求转交给 MessageQueue.quit(safe)。核心动作只有设置退出标记,并叫醒可能正在等待的 Looper:

java 复制代码
void quit(boolean safe) {
    synchronized (this) {
        if (mQuitting) return;
        mQuitting = true;
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }
        nativeWake(mPtr);
    }
}

注意,此时还没有执行 nativeDestroy()。真正清理发生在 Looper 线程再次进入 next() 并走到退出分支时:

java 复制代码
if (mQuitting) {
    dispose();
    return null;
}

next() 返回 null 后,Looper.loop() 会结束。但 quit(false)quitSafely() 到达该分支的时机不同:

text 复制代码
quit(false)
→ 删除全部待处理消息
→ nativeWake
→ next() 发现 mQuitting
→ dispose()

quitSafely()
→ 删除未来消息,保留已经到期的消息
→ nativeWake
→ next() 继续返回并 dispatch 已到期消息
→ 没有可继续处理的消息后
→ dispose()

源码位置:MessageQueue.quit()next() 的退出分支Looper.quit()/quitSafely()

因此,主动退出路径相对明确,但清理不是在调用退出方法的那一行同步完成。quit(false) 要等 Looper 线程重新推进到 next()quitSafely() 还可能继续 dispatch 多条已经到期的消息,直到没有可继续返回的消息才执行 dispose()

2. GC 兜底:finalize() 会调用 dispose(),但时间不确定

同一份源码还保留了:

java 复制代码
@Override
protected void finalize() throws Throwable {
    try {
        dispose();
    } finally {
        super.finalize();
    }
}

这说明 Java MessageQueue 最终进入终结处理时,也会尝试释放 mPtr。但它只能看成兜底:对象何时变得不可达、何时执行 finalizer 都没有确定时限;只要 Looper、线程局部状态或其他对象仍然引用这条队列,GC 路径就不会发生。

这里是在解释这份 AOSP 源码的兜底行为,不是在推荐一般 JNI 代码依赖 finalize() 管理 Native 资源。

所以不能写成"Java 对象不可达后,Native 对象立即释放"。准确说法是:

在 Looper 线程能够继续正常推进消息循环的前提下,主动 quit/quitSafely 会使它进入 dispose(),但 quitSafely() 会先处理已经到期的消息;finalizer 提供非确定时间的兜底。nativeDestroy() 定义释放动作,但其执行时机取决于 Java 侧何时真正触发 dispose()

3. MessageQueue 生命周期为什么跟 Looper 和线程绑在一起?

生命周期归属只需记住三点:

text 复制代码
可退出 Looper:通常由 quit / quitSafely 驱动 dispose。
主线程 Looper:禁止 quit,通常随所在进程一直存在。
finalize:只提供触发时间不确定的兜底,不是推荐的资源管理方案。

"队列暂时没有消息"只会让线程等待,不会自动退出。第二篇的生命周期闭环因此由对象创建、Looper 主动退出和 GC 兜底三条事实共同组成。

七、补充模型:不要把 jobjectJNIEnv* 当成 mPtr

到这里,mPtr 的创建、使用、主动退出和 GC 兜底已经闭环。下面不再扩展另一条主线,只做一次反向对照:jobjectJNIEnv* 虽然也表现为指针形态,却不能按 mPtr 的规则保存。

jobject 不是 Java 对象裸地址

nativePollOnce() 收到的 jobject obj 表示当前 Java MessageQueue,但它是 ART 可识别的 JNI 引用,不是可以永久保存的 Java Heap 裸地址。传给 native 方法的对象参数默认是 local reference:

text 复制代码
local reference 只在当前线程、当前 native 调用期间有效。
跨越这次调用长期持有 Java 对象,需要 NewGlobalRef / DeleteGlobalRef。

所以方向要分开:Java 用 mPtr 保存 Native 句柄;Native 若要长期保存 Java 对象,必须使用 ART 能追踪的引用机制。

为什么 mPollObj 暂存 jobject 却没有变成 GlobalRef?

NativeMessageQueue::pollOnce() 会临时保存传入的 envpollObj

cpp 复制代码
void NativeMessageQueue::pollOnce(
        JNIEnv* env, jobject pollObj, int timeoutMillis) {
    mPollEnv = env;
    mPollObj = pollObj;

    mLooper->pollOnce(timeoutMillis);

    mPollObj = NULL;
    mPollEnv = NULL;
}

保存它们的真实用途,是在其他 fd 就绪时从 Native callback 回到 Java MessageQueue.dispatchEvents()。下面只保留事件转换后的 Java 回调点:

cpp 复制代码
int NativeMessageQueue::handleEvent(int fd, int looperEvents, void* data) {
    int events = /* 把 looperEvents 转成 Java 侧事件位 */;
    int newWatchedEvents = mPollEnv->CallIntMethod(
            mPollObj,
            gMessageQueueClassInfo.dispatchEvents,
            fd,
            events);
    if (!newWatchedEvents) return 0;
    // 若监听事件变化则更新 fd 注册。
    return 1;
}

完整实现见 NativeMessageQueue::handleEvent()。fd callback 仍发生在同一次 nativePollOnce() 内、同一个 Looper 线程上,并在返回 Java 前清空 mPollObj/mPollEnv,因此没有跨越 local reference 和当前 JNIEnv* 的有效窗口。若改成"返回以后由另一个线程回调",才需要 GlobalRef、回调线程自己的 JNIEnv* 以及新的释放规则。

JNIEnv* 为什么不能跨线程保存?

JNIEnv* 属于当前已附着线程,不能把线程 A 的值交给线程 B。只需记住这个对照:

text 复制代码
Java 线程调用 nativePollOnce
→ 已经附着,直接收到本线程 JNIEnv*,不需要再次 attach。

Native 自建线程要调用 Java
→ 通过 JavaVM AttachCurrentThread,取得该线程自己的 JNIEnv*。

完整引用表、Native 线程回调和 detach 规则属于 JNI 生命周期专题,不再向下展开。

八、把四种"指针形态"彻底分开

JNI 代码里经常同时出现几种看起来像指针的东西,最容易互相替换着理解。

形式 在本文中的含义 能否长期作为有效句柄/引用使用 谁理解它
Java long mPtr / JNI jlong Native 指针编码成的句柄值 数值可以保存;只有对应 Native 对象存活且并发协议成立时才是有效句柄 Framework 自己的协议
NativeMessageQueue* C++ 对象指针 只有持有有效所有权,或处于受保护的借用期内,才能安全解引用 C++
jobject ART 可识别的 Java 对象引用 local 引用仅在创建线程和当前 native 调用窗口内有效;长期需 GlobalRef ART / JNI
JNIEnv* 当前线程的 JNI 接口与上下文 只能由对应的已附着线程使用 ART / 当前线程

它们在源码中都表现为底层整数、指针或引用句柄,但解释者和生命周期完全不同。

最危险的 JNI bug,往往不是语法错误,而是把一种值按另一种值的规则保存和使用。

为什么必须区分?混淆这些值的规则,通常会导致 Native 泄漏、use-after-free、重复释放,以及跨线程错误使用 JNI 引用或 JNIEnv*

把这个模型迁移到其他 Java/Native 双对象时,只需检查六个问题:

text 复制代码
谁创建?
谁持有?
谁销毁?
销毁后怎样标记失效?
是否可能并发使用?
Native 是否长期持有 Java 对象?

九、本文闭环:mPtr 到底是什么?

现在可以准确回答:

Java MessageQueue 与 C++ NativeMessageQueue 是同一进程中的两个独立对象。nativeInit() 创建 Native 对象,并用 incStrong(env) 为 Java mPtr 关联关系建立一份手动强引用;nativePollOnce()nativeWake() 在对象存活期间把句柄值传回 C++。在 Looper 线程能够继续正常推进的前提下,主动 quit/quitSafely 会使其进入 next() 的退出分支;finalizer 则只是时间不确定的 GC 兜底。dispose() → nativeDestroy() → decStrong() 释放的是这份配对强引用,mPtr = 0 只把 Java 句柄标记为失效。强引用释放后,NativeMessageQueue 是否立即析构仍取决于是否存在其他强引用;它即使析构,也不能证明 Native Looper 同时销毁。Looper 中遗留的 fd 注册通过 WeakLooperCallback + wp.promote() 安全失效,不会直接回调悬空队列。三类对象的关系与生命周期由 Framework 显式协调,而不是 JNI 或 GC 自动完成。

至此,我们解释了"方法怎样连上"和"对象怎样找到"。第三篇把两者放回 Handler 的真实现场:

text 复制代码
为什么 nativePollOnce 能让线程等待?
为什么 nativeWake 能把它叫醒?
JNI 在这条链上负责哪一段?
为什么再往后的 Binder 不是 JNI 的另一种写法?

源码与延伸阅读

版本附注:Android 16 CombinedMessageQueue 的启用范围

这里的开关是 Framework 平台 flag useConcurrentMessageQueueInApps,不是 App 开发者可自行配置的普通应用选项。该 flag 开启时,非 Robolectric 进程可进入 Concurrent 实现;否则源码继续按进程名与 core UID 判断,并排除部分测试进程。

Legacy 模式并不禁止其他线程调用 nativeWake() 等 Native 方法:enqueueMessage() 本来就可以由发送线程执行,并在同一个 MessageQueue 锁内判断 needWake、使用 mPtr。Legacy 的跨线程路径依赖这把锁和原有生命周期约束。Concurrent 模式放宽了队列操作对同一把锁的依赖,因此 Looper 线程可以直接使用 mPtr,其他线程则必须在使用 Native 对象前后调用 incrementMptrRefs() / decrementMptrRefs(),保证并发 teardown 期间对象仍然存活。源码中的 mPtr 字段注释 直接列出了这两套协议;启用判断见 CombinedMessageQueue.computeUseConcurrent()

相关推荐
爱笑鱼2 小时前
JNI(一):nativePollOnce() 到底去了哪里?
android
hqk4 小时前
玩Android×AI Coding:从产品到代码,完整跑通一套 AI 软件研发工作流
android·ai编程·vibecoding
安卓修改大师4 小时前
安卓修改大师实战:深入解析Android主题Theme修改与沉浸式界面实现
android
stevenzqzq7 小时前
app性能日志工具
android
DB哥讲数据库7 小时前
【最新】MySQL9.7安装教程:图文详解(附MySQL安装包)
android·mysql·adb
IT乐手7 小时前
Android 写本地日志工具类
android·java
非典型Android程序员8 小时前
Launcher学习总结
android
Coffeeee9 小时前
Android功耗优化,哪些代码会让你的机子发热又耗电
android·架构·kotlin
xianrenli3810 小时前
Android14自动时区更新
android·学习·framework·aosp14·自动时区