MediaCodec 全链路深度剖析(一):开篇 —— 当你点下"播放",究竟发生了什么?
系列导读:本系列将从 Kotlin/Java 应用层出发,一路向下,穿越 JNI、Native framework、Codec2、HAL、驱动,直至 SoC 内部的 VPU/GPU/NPU 硬件电路,完整拆解一帧视频从磁盘到屏幕的代码链路与数据流转。
本文是第 1 篇 · 开篇总览,不涉及具体代码细节,目的是建立一张「全景地图」,让你知道自己在下面哪一层、该往哪个方向挖。
一、为什么要写这个系列
过去这些年,我对 MediaCodec 的理解基本停留在 API 使用层面——知道怎么在多个线程间(音频解码线程、视频解码线程、UI线程)串起整个流程,能理解与 MediaExtractor、AudioTrack、MediaMuxer 之间的协作,也了解 H264 的理论原理相关,但一旦有比较深入地问下去,我就答不上来了:
queueInputBuffer之后,那段 H.264 码流到底经过了几个进程?- “硬解”到底是谁在解?
ACodec、CCodec、OMX、Codec2它们是什么关系、又是怎么演化出来的? - 为什么
Surface输出可以”零拷贝”?这个”零”是真的零,还是营销话术? DMA-BUF、fence、Gralloc、BufferQueue这些名词我都听过,但它们到底怎么咬合在一起?- 走到最底下,
VPU这颗硬件电路,是怎么被 App 的一行 Kotlin 代码”遥控”起来的?
这些问题或者遇到疑难 bug(偶现花屏、特定机型掉帧、跨设备兼容性),没有底层视角,只能靠猜;遇到性能优化,没有全链路认知,也只能做一些表面功夫。
去翻市面上的 MediaCodec 文章,大多数是”十分钟入门”的——贴一段 demo 代码、列一下状态机、讲讲同步 vs 异步模式,就结束了。这些内容对刚入门的人有用,但对已经写了几年音视频的人,信息密度低到几乎为零,而且彼此之间高度同质化。
真正能把「App 代码 → JNI → Native framework → HAL → 驱动 → 硬件」这条链路一路贯通的文章比较少。毕竟很少人对这所有的方面都了解,做底层硬件的可能不太了解上层的应用,做应用的不太了解硬件底层的逻辑。好在当前 AI 的发展帮忙能够比较好地去学习了解这些问题了,本系列文章很多资源内容也是借助 AI 搜集整理资料的能力完成的。
本系列试图做的事:不讲解如何使用 MediaCodec,只讲解原理相关。把安卓音视频碎片串成一条完整的链路,从 App 代码一行一行地跟踪到硬件寄存器,知其然更知其所以然。
适合读这个系列的人
| 你是谁? | 会收获什么 |
|---|---|
| 写短视频/直播/播放器的 App 工程师 | 理解 MediaCodec API 背后的”陷阱”来源,写出更稳定的代码 |
| 做音视频 SDK / 特效引擎的开发者 | 明白 Surface、BufferQueue、EGLImage 的真实语义,设计零拷贝管线 |
| 做系统 / ROM / HAL 的工程师 | 把 Codec2、OMX、Gralloc、Fence 这些散点连成系统视图 |
| 对底层原理有好奇心的爱好者 | 看到一颗 SoC 内部 CPU/GPU/VPU/NPU 是如何协作的 |
前置要求:
- 对音视频编码基础了解,强烈推荐这个项目提供的内容 digital_video_introduction
- 能读懂中等难度的 C/C++
- 熟悉 Kotlin/Java 语法,了解 MediaCodec 的基本使用
- 对”线程”、”IPC”、”进程”有基本概念
一个直击灵魂的提问
在正式展开之前,先做一个思想实验。假设你写了如下最朴素的播放代码:
val extractor = MediaExtractor().apply { setDataSource("/sdcard/demo.mp4") } |
点击运行,屏幕上出现了画面。
问题来了:从你调用 releaseOutputBuffer(index, true) 那一刻算起,到这一帧的像素真正点亮 LCD 的某一行,中间发生了多少次进程切换、多少次内存拷贝、多少次 CPU ↔ GPU ↔ VPU 协同?
答案是:
0 次像素拷贝(理想情况)、3 次跨进程、7 次关键跳跃、至少 4 种硬件单元参与。
看不懂这些数字没关系——整个系列就是用来解释它们的。为什么要知道这些?
《SICP》里面的核心思想 “程序即数据,数据即程序,二者边界模糊,所有计算机程序存在的目的都是为了处理、转换或构造数据,最终达到对复杂系统进行抽象和模拟的目的”。对于我们来讲只要能理解各个数据的流向就能理解整个系统。
二、MediaCodec 的真实身份
4.1 它是什么?
MediaCodec 不是一个解码器,而是 Android 为你提供的”访问解码/编码硬件”的统一 API 门面。
更准确地说:
MediaCodec是一个字节流的状态机 + 缓冲池的调度器。
它自己不懂 H.264,不懂 HEVC,不懂 AV1。它做的事情是:
- 接管你塞进来的一段输入字节(可能是 H.264 的一个 NAL、也可能是 AAC 的一帧);
- 把它交给真正懂这种格式的后端实现(ACodec/OMX 或 CCodec/Codec2);
- 后端再通过 HAL 把数据推给硬件电路(VPU)或软件库(如 libavcodec);
- 拿回解码好的 YUV,通过输出 Buffer 或 Surface 交还给你。
4.2 它不是什么?
| 误解 | 事实 |
|---|---|
| “MediaCodec 自己做解码” | ❌ 它只是门面,真正解码的是 VPU 或软件库 |
| “createDecoderByType 就会调起硬解” | ❌ Android 会按能力表挑选组件,可能挑到 OMX.google.h264.decoder(软解) |
| “硬解一定比软解快” | ❌ 启动开销大,短视频首帧软解更快;长视频稳态才体现硬解优势 |
| “Surface 输出没有拷贝” | ⚠️ 准确说是「通常没有 CPU 可见的像素拷贝」,DMA 搬运和格式转换仍可能发生 |
4.3 一个比喻
把 MediaCodec 想象成麦当劳的点餐柜台:
- 你(App)只管点一个”巨无霸”(送入 H.264 码流)
- 柜台(MediaCodec)把订单传到后厨(Codec2/ACodec)
- 后厨可能让烤箱(VPU)或者人工厨师(CPU 软解)来做
- 做好的汉堡(YUV 帧)通过传送带(BufferQueue)送到你面前
- 整个过程你看不见厨房内部,但知道下单的 API 长什么样
系列后续的工作,就是掀开厨房的帘子,逐个工位讲清楚。
三、MediaCodec 全链路
六层架构速览
flowchart TB
subgraph L1["🟦 L1 · Java API"]
A1["📱 App 进程
━━━━━━━━━━━━━━
MediaCodec.java
MediaExtractor · MediaMuxer
Surface · SurfaceTexture"]
end
subgraph L2["🟩 L2 · JNI 桥接"]
B1["📱 App 进程
━━━━━━━━━━━━━━
android_media_MediaCodec.cpp
JMediaCodec 包装对象
ByteBuffer · Surface 跨界"]
end
subgraph L3["🟨 L3 · Native Framework"]
direction LR
C1["ACodec
━━━━━━━━
OMX 路径
legacy"]
C0["⚙️ mediacodec 进程
━━━━━━━━━━━━━━
MediaCodec.cpp
ALooper + AHandler
状态机"]
C2["CCodec
━━━━━━━━
Codec2 路径
modern"]
C0 -.-> C1
C0 -.-> C2
end
subgraph L4["🟧 L4 · HAL 层"]
direction LR
D1["🔧 vendor 进程
━━━━━━━━━━━━
OMX HAL
IOMX.hal"]
D2["🔧 vendor 进程
━━━━━━━━━━━━
Codec2 HAL
IComponentStore.aidl"]
end
subgraph L5["🟥 L5 · Kernel 驱动"]
E1["🐧 内核态
━━━━━━━━━━━━━━━━━━━━
V4L2 · ION · DMA-BUF · iommu"]
end
subgraph L6["🖤 L6 · SoC 硬件"]
direction LR
F1["💻 CPU
━━━━
控制
软解"]
F2["🎬 VPU
━━━━
硬件
编解码"]
F3["🎨 GPU
━━━━
OES 纹理
Shader"]
F4["🧠 NPU/DSP
━━━━━━
AI 超分
插帧"]
F5["📺 DPU/HWC
━━━━━━
屏幕合成"]
end
A1 --> B1
B1 --> C0
C1 --> D1
C2 --> D2
D1 --> E1
D2 --> E1
E1 --> F1
E1 --> F2
E1 --> F3
E1 --> F4
E1 --> F5
classDef l1 fill:#E3F2FD,stroke:#1976D2,stroke-width:2px,color:#000
classDef l2 fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#000
classDef l3 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#000
classDef l4 fill:#FFE0B2,stroke:#EF6C00,stroke-width:2px,color:#000
classDef l5 fill:#F3E5F5,stroke:#6A1B9A,stroke-width:2px,color:#000
classDef l6 fill:#FFCDD2,stroke:#C62828,stroke-width:2px,color:#000
class A1 l1
class B1 l2
class C0,C1,C2 l3
class D1,D2 l4
class E1 l5
class F1,F2,F3,F4,F5 l6
各层职责一览
| 层 | 进程 | 关键代码 | 本质职责 |
|---|---|---|---|
| L1 Java API | App 进程 | MediaCodec.java |
给 App 写代码的入口,状态机封装 |
| L2 JNI | App 进程 | android_media_MediaCodec.cpp |
把 Java 对象翻译成 C++ 对象;持有 native 指针 |
| L3 Native Framework | mediacodec 进程(沙箱隔离) | MediaCodec.cpp、CCodec.cpp |
真正的解码大脑,调度 Looper/Handler |
| L4 HAL | vendor 进程 | c2.qti.* / c2.mtk.* / OMX.qcom.* |
厂商私有实现,写寄存器、发中断 |
| L5 Kernel | 内核态 | V4L2-M2M、ION、iommu | 暴露硬件资源、管理 DMA-BUF |
| L6 Hardware | 硅片 | VPU / GPU / NPU / DPU | 真正的电路,消耗电能产出像素 |
三个最昂贵的跨越(对性能影响最大):
- L2 → L3 —
App 进程 → mediacodec 进程(Binder IPC) - L3 → L4 —
mediacodec 进程 → vendor 进程(HIDL/AIDL 跨进程) - L4 → L5 — 用户态到内核态(ioctl/syscall)
一个隐藏知识点:Android 8.0(Treble)之后,MediaCodec 被拆到独立进程
mediacodec中运行,这是为了应对 StageFright 漏洞(CVE-2015-1538)的历史教训——用进程沙箱换安全性,代价是多一次 Binder 调用。
一帧数据的”七次跳跃”
用一张”一帧生命周期”时序图来串联上面六层:
sequenceDiagram
autonumber
participant App as App 线程
participant JNI as JNI 层
participant MC as mediacodec 进程
participant HAL as vendor HAL 进程
participant VPU as VPU 硬件
participant GPU as GPU
participant SF as SurfaceFlinger
participant HWC as HWC/DPU
App ->> JNI: 1. queueInputBuffer(H.264 NAL)
JNI ->> MC: 2. Binder IPC(kWhatQueueInputBuffer)
MC ->> HAL: 3. HIDL/AIDL(c2_queue)
HAL ->> VPU: 4. ioctl + DMA-BUF 描述符
Note over VPU: 硬件解码(几毫秒)
产出 YUV 到 DMA-BUF
VPU -->> HAL: 5. 中断 + dma-fence
HAL -->> MC: 6. 输出 GraphicBuffer
MC -->> JNI: 7. onOutputBufferAvailable
JNI -->> App: releaseOutputBuffer(true)
App ->> GPU: (可选) 特效渲染到 FBO
GPU ->> SF: queueBuffer 到 BufferQueue
SF ->> HWC: 合成(GPU 或 DPU 直出)
HWC -->> App: VSYNC · 像素点亮屏幕
对应的「7 次关键跳跃」是:
| # | 跳跃 | 机制 | 成本 |
|---|---|---|---|
| 1 | Kotlin → Java | 字节码直接调用 | 几乎为 0 |
| 2 | Java → Native | JNI | 纳秒级 |
| 3 | App 进程 → mediacodec 进程 | Binder IPC | 微秒级 |
| 4 | mediacodec → vendor HAL | HIDL/AIDL | 微秒级 |
| 5 | vendor → kernel | ioctl | 几微秒 |
| 6 | kernel → 硬件电路 | 寄存器 + 中断 | 纳秒级触发,毫秒级执行 |
| 7 | VPU 输出 → GPU/DPU 消费 | dma-fence + DMA-BUF 共享 | 零拷贝(理想) |
关键秘密:跳跃 3/4/5 是控制信号(很小,几百字节),跳跃 7 是像素数据(很大,Full HD 一帧 YUV420 约 3MB)——控制路径多次跨界没关系,像素路径一次拷贝都不能多。
这就是为什么 Android 花了十年打磨 Gralloc + BufferQueue + DMA-BUF + fence 这一整套”零拷贝基础设施”——我们会在 M6/M9 详细拆解。
硬件协同:一颗 SoC 是如何”开派对”的
很多人以为视频播放只是”解码 → 显示”两步。真实情况要热闹得多:
flowchart LR
MP4[MP4 文件] -->|demux| CPU1[CPU
解封装]
CPU1 -->|H.264 NAL| VPU[VPU
硬件解码]
VPU -->|YUV 帧| GPU[GPU
特效/色彩空间转换]
GPU -->|RGB 纹理| NPU[NPU
超分/插帧]
NPU -->|增强帧| DPU[DPU/HWC
合成]
DPU -->|扫描行| LCD[📺 屏幕]
CPU2[CPU
全局调度与 fence 等待] -. 控制 .- VPU
CPU2 -. 控制 .- GPU
CPU2 -. 控制 .- NPU
CPU2 -. 控制 .- DPU
style CPU1 fill:#E3F2FD,stroke:#1976D2
style CPU2 fill:#E3F2FD,stroke:#1976D2
style VPU fill:#FFCDD2,stroke:#C62828
style GPU fill:#FFF9C4,stroke:#F9A825
style NPU fill:#E8F5E9,stroke:#388E3C
style DPU fill:#FFE0B2,stroke:#EF6C00
五大硬件单元的「戏份」
| 硬件 | 典型代号 | 在视频链路里干什么 | 被谁调用 |
|---|---|---|---|
| CPU | ARM Cortex-A | demux、调度、fence 等待、软解 fallback | 所有层都用 |
| VPU | Qualcomm Venus / MTK VDEC / 三星 MFC | 硬件编解码主力 | Codec2 HAL → 驱动 |
| GPU | Adreno / Mali / Xclipse | 色彩空间转换、Shader 特效、UI 合成 fallback | OpenGL ES / Vulkan |
| NPU/DSP | Hexagon / APU / NPU | AI 超分、插帧、去噪、HDR 转换 | NNAPI / 私有 SDK |
| DPU/HWC | MDSS / DISP | 免 GPU 直接合成,最省电 | SurfaceFlinger → HWC2 |
关键洞察:
- 稳态播放时 CPU 几乎不干活 — 因为 VPU→GPU→DPU 之间通过 DMA-BUF + fence 自治流转
- GPU 有三种角色 — 特效处理(App 用)、色彩转换(SF 用)、合成 fallback(HWC 不能直出时兜底)
- NPU 是近 3 年才加入的新玩家 — 超分、插帧是最典型的业务
这些内容将在 M9 硬件分工全景 中用两万字铺开讲,包括为什么 HWC 能直出 NV12 而 App 层却必须经过 GPU。
解码与编码:两条相反的路
| 方向 | 输入 | 处理 | 输出 | 谁来保存 |
|---|---|---|---|---|
| 解码(播放) | H.264 码流 | VPU 解码 → GPU 特效 | YUV / RGB 帧 | SurfaceFlinger 合成显示 |
| 编码(录制/导出) | YUV / RGB 帧 | GPU 渲染 → VPU 编码 | H.264 码流 | MediaMuxer 封装进 MP4 |
特效视频的核心链路是「解码 → 特效 → 编码」的一个完整闭环:
flowchart LR
A[MP4 输入] --> B[MediaExtractor]
B --> C[MediaCodec 解码器
硬解 → YUV]
C --> D[SurfaceTexture
OES 外部纹理]
D --> E[GPU Shader
LUT/滤镜/贴纸]
E --> F[MediaCodec 编码器
输入 Surface]
F --> G[H.264 码流]
G --> H[MediaMuxer]
H --> I[MP4 输出]
style C fill:#FFF9C4,stroke:#F9A825
style F fill:#FFF9C4,stroke:#F9A825
style E fill:#FFCDD2,stroke:#C62828
关键技巧:编码器的输入 Surface + 解码器的输出 SurfaceTexture 构成一条纯 GPU 零拷贝管线,整个路径 CPU 几乎不参与像素搬运。这是现代短视频 App 性能的秘密武器。
总结
MediaCodec 不是解码器,而是六层架构的门面:上层 API 一次调用,背后是 3 次跨进程、7 次关键跳跃、4 种硬件协同。其中前六跳传的是控制信号,最后一跳靠 DMA-BUF + fence 完成像素零拷贝——这正是 Android 花十年打磨出的护城河。看懂了这张地图,就能解释”硬解为何变软解”、”同样 1080p60 为何别人更省电”这类表层 API 问题。