使用进程内解码器优化音频性能

从 Android 17(API 级别 37)开始,并通过 Google Play 系统更新(Mainline APEX 模块)进行更新,Android 引入了适用于压缩音频格式(包括 Opus 和 AAC)的进程内软件音频解码器。

通过直接在应用进程(而非单独的沙盒化系统进程)中运行音频解码器,应用可将音频解码延迟时间缩短大约 40%,降低 CPU 利用率,并延长持续媒体播放、语音消息和游戏期间的电池续航时间。

进程外解码与进程内解码

过去,Android 会在专用的沙盒化系统守护程序 (mediaswcodec) 中运行所有平台软件媒体解码器,以保护应用和操作系统免受格式错误的媒体比特流的影响。不过,进程外沙盒化会带来可测量的性能开销:

  • 进程间通信 (IPC) 延迟时间:排队到解码器的每个音频输入帧以及返回的每个已解码 PCM 音频缓冲区都需要跨进程 Binder IPC 序列化和上下文切换。
  • 共享内存开销:进程间缓冲区切换需要共享内存 (C2Buffer) 同步和缓存管理。
  • 调度争用:在 CPU 负载较重的情况下,应用进程与 mediaswcodec 之间的线程调度争用可能会导致低延迟音频流水线(例如 DAW、VoIP 通信和互动游戏)中出现缓冲区不足和音频卡顿。

在进程内运行媒体解码器可直接解决以下性能瓶颈:

  • 消除 IPC 延迟:绕过 Binder IPC 事务和上下文切换,将端到端解码延迟降低约 40%。
  • 最大限度地减少缓冲区开销:直接在本地应用内存空间内传递音频缓冲区,而无需跨进程共享内存映射。
  • 防止调度抖动:在应用自己的优先级线程(例如实时 AAudio 或 Oboe 线程)上执行解码,从而防止在系统负载较重时出现音频丢失和缓冲区欠载。

使用 Rust 确保内存安全

过去,Android 会将软件解码器隔离在单独的进程 (mediaswcodec) 中,因为解码器会处理来自外部媒体文件的不可信比特流,从而使内存损坏漏洞(例如缓冲区溢出)成为一个主要的安全问题。

Android 可以通过使用 Rust 等内存安全语言实现软件解码器,或使用轻量级故障隔离 (LFI) 隔离软件解码器,从而安全地将软件解码器直接移至调用应用进程。

为了安全地消除 AAC (c2.android.inproc.aac.decoder) 的进程外沙盒开销,Android 提供了以纯 Rust 实现或受安全 Rust 外部函数接口 (FFI) 框架保护的解码器。

Rust 被认为可以安全地进行进程内解码,原因如下:

  • 编译时内存安全:Rust 的严格所有权、借用和生命周期检查可确保在内存释放或共享时发生变化后无法访问。
  • 消除常见漏洞:Rust 可在编译时完全防止堆缓冲区溢出、堆栈粉碎、数组越界读写、释放后使用错误和双重释放漏洞。
  • 零运行时沙盒开销:由于内存安全性在编译时间已通过数学方法证明,并通过边界检查进行了验证,因此 Rust 解码器在应用进程内以原生速度执行,无需页表转换或硬件沙盒上下文。

利用轻量级故障隔离 (LFI) 实现内存安全

对于尚未用 Rust 重写的复杂旧版 C/C++ 音频解码器(特别是 Opus 解码器 [c2.android.inproc.opus.decoder,由 libopus 提供支持]),Android 使用轻量级故障隔离 (LFI) 提供进程内安全性。

对于 C/C++ 解码器,LFI 被视为安全,原因如下:

  • 硬件保护的线性内存沙盒化:LFI 将 C/C++ 编解码器库编译为 WebAssembly 派生的线性机器代码,这些代码仅限于硬件保护的 4 GB 线性内存槽。
  • Linux 内存保护密钥 (pku):LFI 使用 Linux 内存保护密钥(pku / pkey_mprotect)来划分地址空间权限。隔离的库无法在其分配的 4 GB 沙盒槽之外读取或写入内存。
  • 系统调用拦截:沙盒内不允许进行任何主机操作系统系统调用(例如文件 I/O、网络访问或进程控制)。任何不受支持的库调用都会路由到最小虚拟桩 (lfi_stubs.c)。
  • 安全故障恢复:如果格式错误或对抗性 Opus 比特流尝试在线性沙盒内进行越界读写,LFI 运行时会在用户空间中安全地捕获故障,并返回干净的 C2_CORRUPTED 解码错误,而不会导致宿主应用崩溃。

LFI 支持的架构和设备

LFI 不需要自定义 CPU 硬件指令,并且在 ARM64 (aarch64) 处理器架构上受支持。大多数消费类移动设备(包括搭载 Google Tensor SoC、Qualcomm Snapdragon 和 MediaTek SoC 的 Pixel 设备等手机、可折叠设备、平板电脑和车载信息娱乐系统)都运行在 ARM64 处理器上,并支持 LFI 进程内解码器。

可用的进程内音频解码器

音频格式 MIME 类型 进程内组件名称 实现
Opus audio/opus c2.android.inproc.opus.decoder C/C++ libopus(含 LFI)
AAC audio/mp4a-latm c2.android.inproc.aac.decoder 内存安全的 Rust (C2ApexAacDec)
  • AAC (c2.android.inproc.aac.decoder):处理 ADTS 流(通过动态 ADTS 标头解析)和原始访问单元 (AU),并进行准确的呈现时间戳跟踪(如果是 AAC_PACKAGING_RAW,则仅进行单帧 AU 处理)。
  • Opus (c2.android.inproc.opus.decoder):解码标准 Ogg Opus 或 Matroska 容器数据包,并内部重新采样为 48 kHz PCM。

选择启用进程内解码器

目前,调用 MediaCodec.createDecoderByType() 时,进程内解码器不是系统默认解码器。在生态系统验证期间,平台会为旧版进程外解码器保留较高的 Codec 2.0 选择优先级。

如需立即选择启用进程内解码器以进行低延迟播放或测试,请使用 MediaCodec.createByCodecName() 按组件名称明确实例化编解码器。

按组件名称实例化

以下示例演示了如何显式实例化进程内 AAC 解码器,如果设备上没有进程内组件,则回退到默认解码器:

Kotlin

val INPROC_AAC_CODEC = "c2.android.inproc.aac.decoder"

fun createAudioDecoder(): MediaCodec {
    return try {
        // Attempt to opt in to the low-latency in-process AAC decoder
        MediaCodec.createByCodecName(INPROC_AAC_CODEC)
    } catch (e: IllegalArgumentException) {
        // Fall back to the default platform AAC decoder
        MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
    }
}

Java

private static final String INPROC_AAC_CODEC = "c2.android.inproc.aac.decoder";

public MediaCodec createAudioDecoder() throws IOException {
    try {
        // Attempt to opt in to the low-latency in-process AAC decoder
        return MediaCodec.createByCodecName(INPROC_AAC_CODEC);
    } catch (IllegalArgumentException e) {
        // Fall back to the default platform AAC decoder
        return MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
    }
}

配置 Jetpack Media3 或 ExoPlayer

如果您的应用使用 Jetpack Media3 或 ExoPlayer,您可以创建自定义 MediaCodecSelector,以便在设备上提供进程内解码器时优先使用这些解码器:

Kotlin

class InprocPreferredMediaCodecSelector : MediaCodecSelector {
    override fun getDecoderInfos(
        mimeType: String,
        requiresSecureDecoder: Boolean,
        requiresTunnelingDecoder: Boolean
    ): List<MediaCodecInfo> {
        val defaultInfos = MediaCodecUtil.getDecoderInfos(
            mimeType,
            requiresSecureDecoder,
            requiresTunnelingDecoder
        )
        val inprocNames = setOf(
            "c2.android.inproc.opus.decoder",
            "c2.android.inproc.aac.decoder"
        )
        // Sort in-process decoders to the top of the selection list
        return defaultInfos.sortedByDescending { it.name in inprocNames }
    }
}

Java

public class InprocPreferredMediaCodecSelector implements MediaCodecSelector {
    private static final Set<String> INPROC_NAMES = new HashSet<>(Arrays.asList(
        "c2.android.inproc.opus.decoder",
        "c2.android.inproc.aac.decoder"
    ));

    @Override
    public List<MediaCodecInfo> getDecoderInfos(
            String mimeType,
            boolean requiresSecureDecoder,
            boolean requiresTunnelingDecoder) throws MediaCodecUtil.DecoderQueryException {
        List<MediaCodecInfo> defaultInfos = new ArrayList<>(
            MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder, requiresTunnelingDecoder)
        );
        // Sort in-process decoders to the top of the selection list
        defaultInfos.sort((a, b) -> {
            boolean aInproc = INPROC_NAMES.contains(a.name);
            boolean bInproc = INPROC_NAMES.contains(b.name);
            return Boolean.compare(bInproc, aInproc);
        });
        return defaultInfos;
    }
}

默认系统解码器路线图

Android 正在积极准备在即将发布的平台版本(从 Android 18 中的 Opus LFI 和 AAC Rust 或即将发布的 Mainline 模块更新开始)中,将这些内存安全的进程内解码器转换为各自音频 MIME 类型的系统默认解码器。

当进程内解码器成为其 MIME 类型的默认 Codec 2.0 组件后,对 MediaCodec.createDecoderByType(...) 的标准调用将自动通过进程内实现进行路由,从而在无需更改任何应用代码的情况下,将解码延迟时间缩短约 40%。