系列导读:本系列将从 Kotlin/Java 应用层出发,一路向下,穿越 JNI、Native framework、Codec2、HAL、驱动,直至 SoC 内部的 VPU/GPU/NPU 硬件电路,完整拆解一帧视频从磁盘到屏幕的代码链路与数据流转。

本文是第 1 篇 · 开篇总览,不涉及具体代码细节,目的是建立一张「全景地图」,让你知道自己在下面哪一层、该往哪个方向挖。


一、为什么要写这个系列

过去这些年,我对 MediaCodec 的理解基本停留在 API 使用层面——知道怎么在多个线程间(音频解码线程、视频解码线程、UI线程)串起整个流程,能理解与 MediaExtractor、AudioTrack、MediaMuxer 之间的协作,也了解 H264 的理论原理相关,但一旦有比较深入地问下去,我就答不上来了:

  • queueInputBuffer 之后,那段 H.264 码流到底经过了几个进程?
  • “硬解”到底是谁在解?ACodecCCodecOMXCodec2 它们是什么关系、又是怎么演化出来的?
  • 为什么 Surface 输出可以”零拷贝”?这个”零”是真的零,还是营销话术?
  • DMA-BUFfenceGrallocBufferQueue 这些名词我都听过,但它们到底怎么咬合在一起?
  • 走到最底下,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") }
val format = extractor.getTrackFormat(videoTrackIndex)
val codec = MediaCodec.createDecoderByType("video/avc").apply {
configure(format, surfaceView.holder.surface, null, 0)
start()
}
// ... queueInputBuffer / releaseOutputBuffer ...

点击运行,屏幕上出现了画面。

问题来了:从你调用 releaseOutputBuffer(index, true) 那一刻算起,到这一帧的像素真正点亮 LCD 的某一行,中间发生了多少次进程切换多少次内存拷贝多少次 CPU ↔ GPU ↔ VPU 协同

答案是:

0 次像素拷贝(理想情况)、3 次跨进程、7 次关键跳跃、至少 4 种硬件单元参与。

看不懂这些数字没关系——整个系列就是用来解释它们的。为什么要知道这些?

《SICP》里面的核心思想 “程序即数据,数据即程序,二者边界模糊,所有计算机程序存在的目的都是为了处理、转换或构造数据,最终达到对复杂系统进行抽象和模拟的目的”。对于我们来讲只要能理解各个数据的流向就能理解整个系统。


二、MediaCodec 的真实身份

4.1 它是什么?

MediaCodec 不是一个解码器,而是 Android 为你提供的”访问解码/编码硬件”的统一 API 门面。

更准确地说:MediaCodec 是一个字节流的状态机 + 缓冲池的调度器

它自己不懂 H.264,不懂 HEVC,不懂 AV1。它做的事情是:

  1. 接管你塞进来的一段输入字节(可能是 H.264 的一个 NAL、也可能是 AAC 的一帧);
  2. 把它交给真正懂这种格式的后端实现(ACodec/OMX 或 CCodec/Codec2);
  3. 后端再通过 HAL 把数据推给硬件电路(VPU)或软件库(如 libavcodec);
  4. 拿回解码好的 YUV,通过输出 BufferSurface 交还给你。

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.cppCCodec.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 真正的电路,消耗电能产出像素

三个最昂贵的跨越(对性能影响最大):

  1. L2 → L3App 进程 → mediacodec 进程(Binder IPC)
  2. L3 → L4mediacodec 进程 → vendor 进程(HIDL/AIDL 跨进程)
  3. 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 问题。