系列导读:第二篇结尾留下一个钩子——真正的状态机在 Native 层,Java 侧只是个薄壳。这一篇沿着 codec.start()dequeueOutputBuffer() 这种”看上去同步”的调用一路下钻,回答一个问题:

Java 的同步调用,在 JNI 之下到底是怎么落到 Native、又是怎么”原地等回结果”的?

主角是 AOSP foundation 库里那套 ALooper / AHandler / AMessage,加上 android_media_MediaCodec.cpp 这一层 JNI 桥。本篇刻意只聚焦同步 API 这一条链——异步 Callback(setCallback / onOutputBufferAvailable)不进行介绍。

1. 为什么 Native 不复用 Java 的 Looper

翻开 MediaCodec.cppstart() 长这样:

status_t MediaCodec::start() {
sp<AMessage> msg = new AMessage(kWhatStart, this);
sp<AMessage> response;
return PostAndAwaitResponse(msg, &response);
}

三行代码,三个问题:

  • AMessage 不是 android.os.Message,那是什么?
  • “同步调用”为什么长得像异步?
  • 系统已经有 Java Looper 了,为什么还要在 Native 再造一套?

第三个问题先回答:AOSP 的 foundation 库早于 ART 存在,很多系统组件(mediaservercameraserver)跑在根本没有 JVM 的进程里。Java 的 Handler/Looper 需要 ART 支撑,Native 用不了。所以 AOSP 在 frameworks/av/media/libstagefright/foundation/ 下维护了自己的一套,名字都加了 A 前缀:

AOSP C++ Java 对应 作用
ALooper Looper 消息队列 + 循环线程
AHandler Handler 接收回调的对象
AMessage Message 消息本身,带 what 和 KV 参数

两套有一个关键差异:AMessage 不用 arg1 / arg2 / obj,用的是字典——setInt32("index", 3)/setInt64("timeUs", 100)/setObject("buffer", buffer)。好处是参数多了不用扩字段,坏处是取值要查表。

更重要的另一个差异:AMessage 原生支持同步等待,Java Handler 不支持。这是 PostAndAwaitResponse 能存在的基础。

2. CodecLooper:每个 codec 实例独享一条线程

每个 MediaCodec 实例在 Native 侧独享一个 Looper。初始化时会看到:

mCodecLooper = new ALooper;
mCodecLooper->setName("CodecLooper");
mCodecLooper->start(
/* runOnCallingThread = */ false, // 起新线程
/* canCallJava = */ false,
ANDROID_PRIORITY_AUDIO); // 优先级 -16
mCodecLooper->registerHandler(this); // MediaCodec 自己是 AHandler

三个细节值得记:

参数/特性 含义 为什么这么设
canCallJava = false 这条线程不期望调 Java 真正要跨进 Java 的是 JMediaCodec 自带的另一条线程,两者职责分离
ANDROID_PRIORITY_AUDIO(-16) 比普通前台线程高 音视频同步容不下 Looper 线程被挤占
一实例一 Looper 不共享 同进程开 4 个解码器就有 4 个 CodecLooper,每个状态机一条线程

3. PostAndAwaitResponse:同步感从哪里来

start() 内部投了一条消息就返回了,外层却”同步”拿到了结果。机制是这样的:

// 调用方:投完消息就在 token 上睡觉
PostAndAwaitResponse(msg, &response);

// 处理方:onMessageReceived 里干完活 postReply 唤醒对方
response->postReply(replyToken);

3.1 AReplyToken:一次调用的”信箱”

核心数据结构叫 AReplyToken,一个调用对应一个,只有三个字段:

字段 作用
mReply 处理方填进来的回复,初始为空
mReplied 是否已回复,调用方循环检查的断言
mLooper 归属的 Looper,不参与同步

token 自己不带锁。整个进程里所有 token 共用一把全局锁和一个全局条件变量,都挂在 gLooperRoster 单例上。唤醒时用 broadcast,被唤醒的线程自己检查 mReplied 是不是轮到自己。

3.2 核心逻辑

// 调用方
status_t ALooper::awaitResponse(const sp<AReplyToken> &replyToken, sp<AMessage> *response) {
Mutex::Autolock autoLock(mRepliesLock); // ① 锁住"回复"专用锁
CHECK(replyToken != NULL); // ② 校验 token 合法
while (!replyToken->retrieveReply(response)) { // ③ 循环尝试取回复
{
Mutex::Autolock autoLock(mLock); // ④ 加另一把锁,检查 Looper 状态
if (mThread == NULL) {
return -ENOENT; // ⑤ Looper 已停止 → 失败返回
}
}
mRepliesCondition.wait(mRepliesLock); // ⑥ 条件变量阻塞,等待被唤醒
}
return OK; // ⑦ 成功拿到回复
// 处理方(在 Looper 线程 onMessageReceived 内)
status_t ALooper::postReply(const sp<AReplyToken> &replyToken, const sp<AMessage> &reply) {
Mutex::Autolock autoLock(mRepliesLock);
status_t err = replyToken->setReply(reply);
if (err == OK) {
mRepliesCondition.broadcast(); // 唤醒
}
return err;
}

status_t AReplyToken::setReply(const sp<AMessage> &reply) {
if (mReplied) {
ALOGE("trying to post a duplicate reply");
return -EBUSY;
}
mReply = reply;
mReplied = true;
return OK;
}

两个细节:

  • broadcast 而不是 signal:一把条件变量看守所有 token,被唤醒的线程要自己检查 mReplied,不是自己就接着睡
  • while 而不是 ifCondition::wait 从睡眠中返回,不代表”回复一定到了”——一方面 POSIX 允许虚假唤醒(没有任何 signal/broadcast 也可能自己醒),另一方面这里用的是 broadcast,别人收到回复时我也会被一起叫醒。所以必须用 while 包住 wait,醒来后重新检查 mReplied,没轮到自己就继续睡

3.3 一次 codec.start() 的完整时序

sequenceDiagram
    participant App as App 线程
    participant JNI as JNI (JMediaCodec)
    participant Q as Looper Queue
    participant L as CodecLooper 线程

    App->>JNI: native_start()
    JNI->>JNI: new AMessage(kWhatStart)
    JNI->>JNI: createReplyToken + setReplyToken
    JNI->>Q: msg.post()
    JNI->>JNI: wait on mRepliesCondition
    Q->>L: 取出 msg
    L->>L: onMessageReceived: 推状态机到 STARTED
    L->>L: postReply: 写信箱 + broadcast
    L-->>JNI: 唤醒
    JNI->>JNI: 读 mReply 得到 status_t
    JNI-->>App: start() 返回 OK

注意整条链路上没有任何一把锁保护 mState:所有入口(App 线程、底层回调线程、Looper 自己)都只 post 消息,状态机只在 CodecLooper 这一条线程上读写——这是后面状态机能”无锁”的根本原因,第四篇会展开。

4. JMediaCodec:JNI 桥的薄壳

Java 侧 codec.start() 走的不是直接下 Native,而是经过一层 JMediaCodec

Java: codec.start()
└─ native_start() // JNI 入口
└─ JMediaCodec::start() // 持有 sp<MediaCodec>
└─ mCodec->start() // 真正的 Native 实现
└─ PostAndAwaitResponse // 跨 Looper 线程

4.1 三层对象:谁是谁

讨论”codec.start() 在 JNI 之下发生了什么”之前,先把整条链路上到底有几个对象、各自归谁管画清楚——这张图后面整篇文章都会反复用到:

%%{init: {'flowchart': {'subGraphTitleMargin': {'top': 14, 'bottom': 14}, 'nodeSpacing': 50, 'rankSpacing': 70}}}%%
flowchart LR
    subgraph J["Java 层(ART)"]
        JM["MediaCodec.java
mNativeContext: long"] end subgraph B["JNI 桥
android_media_MediaCodec.cpp"] JNI["JMediaCodec
: public RefBase
sp<MediaCodec> mCodec"] end subgraph N["Native 层(libstagefright)"] MC["MediaCodec.cpp
真正的状态机
持有 CodecLooper"] end JM -. "mNativeContext
存 JMediaCodec*" .-> JNI JNI == "sp<> 强引用" ==> MC classDef java fill:#BBDEFB,stroke:#1565C0,stroke-width:2px,color:#0D47A1 classDef jni fill:#FFF59D,stroke:#F57F17,stroke-width:2px,color:#3E2723 classDef native fill:#FFCC80,stroke:#E65100,stroke-width:2px,color:#BF360C class JM java class JNI jni class MC native style J fill:#E3F2FD,stroke:#1976D2,stroke-width:1.5px,color:#0D47A1 style B fill:#FFF9C4,stroke:#F9A825,stroke-width:1.5px,color:#5D4037 style N fill:#FFE0B2,stroke:#EF6C00,stroke-width:1.5px,color:#BF360C

三个对象,一一对应不会变:

对象 归属 职责
Java MediaCodec.java ART / GC 管理 用户面对的 API;只是个壳
JNI JMediaCodec sp<> 引用计数管理 翻译 Java 调用 ↔ Native 调用
Native MediaCodec.cpp JMediaCodec::mCodec 强引用 真正的状态机、CodecLooper、PostAndAwaitResponse

绑定关系靠 Java 侧的一个 private long mNativeContext 字段——里面存的就是 JMediaCodec* 指针的整数值(64-bit 系统下 8 字节)。native_setup 时通过 SetLongField 写入;之后每个 JNI 入口的第一步都是 getMediaCodec(env, thiz) 把这个 long 强转回 JMediaCodec* 用:

sp<JMediaCodec> codec = getMediaCodec(env, thiz);
if (codec == NULL) {
throwExceptionAsNecessary(env, INVALID_OPERATION);
return;
}
codec->start(); // 同步路径上 JNI 做的几乎就是这一件事

整个 android_media_MediaCodec.cpp 里 95% 的 JNI 入口都是这个模板:取 JMediaCodec → 调一次方法 → 把 status_t 翻成 Java 异常或返回值。本篇主线讲的”Java 同步调用怎么落到 Native”,这一层就是承接点,但本身没有任何业务逻辑

4.2 唯一一把 sLock:与前文”无锁”形成对照

第 3 节强调过:Native 那一层的 mState无锁的,依靠 CodecLooper 单线程串行避免竞争。但翻开 JNI 文件,会看到一把全局静态锁:

static Mutex sLock;

static sp<JMediaCodec> getMediaCodec(JNIEnv *env, jobject thiz) {
Mutex::Autolock _l(sLock);
return (JMediaCodec*)env->GetLongField(thiz, gFields.context);
}

static sp<JMediaCodec> setMediaCodec(
JNIEnv *env, jobject thiz, const sp<JMediaCodec> &codec) {
Mutex::Autolock _l(sLock);
sp<JMediaCodec> old = (JMediaCodec*)env->GetLongField(thiz, gFields.context);
if (codec != NULL) codec->incStrong(thiz);
if (old != NULL) old->decStrong(thiz);
env->SetLongField(thiz, gFields.context, (jlong)codec.get());
return old;
}

为什么 Native 都做到无锁了,JNI 这一薄层反而要加锁?因为这两层面对的调用者根本不一样

调用方 是否需要锁
Native MediaCodec.cppmState 只有 CodecLooper 一条线程能改 不需要
JNI mNativeContext 字段 任意 Java 线程都能直接调 JNI 入口 必须加锁

具体的并发场景就是 release vs 其他方法。假设没有 sLock,两条线程会这样交错:

sequenceDiagram
    participant A as 线程 A(release)
    participant F as mNativeContext
(Java 字段) participant B as 线程 B(queueInputBuffer) A->>F: 读出 old 指针 B->>F: 读出同一个 old 指针 A->>A: old->decStrong()
JMediaCodec 析构 A->>F: 写入 nullptr Note over B: B 手里的指针已经悬空 B->>B: codec->queueInputBuffer(...)
use-after-free

问题点很清楚:A 还没把字段清空、对象还没析构之前,B 已经把指针读走了;等 A 析构完,B 手上那份指针就成了野指针。

sLockgetMediaCodecsetMediaCodec 串起来后,B 只可能看到两种结果:

  • 要么 A 还没开始:B 拿到一个完整的 sp<JMediaCodec>sp<> 一旦构造就持有强引用,A 那边的 decStrong 至多让引用计数减 1,对象不会在 B 手里被析构;
  • 要么 A 已经做完:B 拿到 nullptr,提前返回错误。

两种结果都安全,不会出现”拿到一个正在死的对象” 这种中间态。

注意这把锁只保护 mNativeContext 这一个 8 字节字段——一进一出粒度极小,对吞吐几乎没影响。真正的状态机仍然在下层用消息驱动维持无锁。可以这样记:

JNI 这一层的 sLock,护的是”Java 对象 ↔ Native 对象的指针绑定”;
Native 那一层的无锁,靠的是”所有改动都走 Looper 串行”。
两层各管各的,组合起来就是 MediaCodec 那种”看上去多线程随便调,实际从不出竞争”的体验。

JMediaCodec 还承担第二件事——把 Native 事件反向转回 Java Callback——那是异步 API 的路径,涉及 Global Ref、AttachCurrentThreadjmethodID 缓存等一整套 JNI 跨语言回调技巧,本篇不展开。)

5. 一次完整闭环:dequeueOutputBuffer

看一次 dequeueOutputBuffer 这个带 timeout 的同步 API 的完整来回,把前面所有零件串起来:

sequenceDiagram
    participant App as App 线程
    participant JNI as JMediaCodec
    participant MC as MediaCodec.cpp
    participant CL as CodecLooper

    App->>JNI: dequeueOutputBuffer(timeout)
    JNI->>MC: mCodec->dequeueOutputBuffer
    MC->>CL: post kWhatDequeueOutputBuffer
    MC->>MC: PostAndAwaitResponse 阻塞

    Note over CL: 队列里没有可用 buffer 就先不回
    Note right of CL: 底层数据准备好后
另一条消息入队 CL->>CL: 把 buffer 放进 mAvailPortBuffers CL->>CL: 取出之前 pending 的 dequeue msg 处理 CL->>MC: postReply(携带 index) MC-->>JNI: 条件变量唤醒 JNI-->>App: 返回 index

两条关键:

  • 带 timeout 的同步等待PostAndAwaitResponse 内部走条件变量的 timed wait——超时直接返回 INFO_TRY_AGAIN_LATER,不等到天荒地老
  • 消息按 FIFO 串行处理:dequeue msg 投早了没数据就先挂着,等数据到位的消息进来后被一并取出来回复。整条同步语义不依赖任何锁保护字段,全靠 CodecLooper 单线程串行 + token 唤醒

总结

本篇沿着一次”看上去同步”的 Java 调用下钻到 Native 侧,讲清了四件事:

  • AOSP 为什么自造 A 系列原语(ALooper / AHandler / AMessage),以及和 Java Handler 的关键差异——原生支持同步等待
  • CodecLooper 如何用”每实例一条线程串行处理”换来状态机的天然无锁
  • PostAndAwaitResponse 怎样通过 AReplyToken + 条件变量,把异步消息包装成 Java 看到的”同步调用”
  • JMediaCodec / sLock 为什么只锁 mNativeContext 一个字段,就能避免 release 与并发调用之间的 use-after-free

一句话收束:下层 Looper 串行做到无锁,上层 JNI 只锁一个字段防悬空——这就是 MediaCodec 同步 API 在 Native 侧的全部秘密。