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

从 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 解码器 [由 libopus 提供支持]),Android 使用轻量级故障隔离 (LFI) 提供进程内安全性。c2.android.inproc.opus.decoder

对于 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) 和 x86_64 处理器架构上受支持:

  • ARM64 (aarch64) 设备:大多数消费类移动设备(包括手机 [例如搭载 Google Tensor SoC、Qualcomm Snapdragon 和 MediaTek SoC 的 Pixel 设备]、可折叠设备、平板电脑和车载信息娱乐单元)都运行在 ARM64 处理器上,并支持 LFI 进程内解码器。
  • x86_64 设备:在搭载 Intel 或 AMD CPU 的开发者工作站上运行的 Android 模拟器,以及使用 ARC(ChromeOS 的 Android 运行时)运行 Android 应用的 ChromeOS Chromebook,均在 x86_64 架构上执行,并支持 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%,而无需进行任何应用代码更改。