自 Android 17 (API 級別 37) 起,Android 透過 Google Play 系統更新 (Mainline APEX 模組) 推出程序內軟體音訊解碼器,用於壓縮音訊格式,包括 Opus 和 AAC。
應用程式直接在程序中執行音訊解碼器,而非在獨立的沙箱系統程序中執行,因此可將音訊解碼延遲時間縮短約 40%、降低 CPU 使用率,並在持續播放媒體、語音訊息和遊戲時延長電池續航力。
程序外與程序內解碼
過去,Android 會在專屬的沙箱系統 Daemon (mediaswcodec) 中執行所有平台軟體媒體解碼器,保護應用程式和 OS 免受格式錯誤的媒體位元串流影響。不過,程序外沙箱會產生可評估的效能負擔:
- 處理序間通訊 (IPC) 延遲時間:每個排入解碼器佇列的音訊輸入影格,以及每個傳回的解碼 PCM 音訊緩衝區,都需要跨處理序繫結器 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) 提供程序內安全防護。
LFI 對 C/C++ 解碼器來說是安全的,原因如下:
- 硬體保護的線性記憶體沙箱:LFI 會將 C/C++ 轉碼器程式庫編譯成 WebAssembly 衍生的線性機器碼,並限制在硬體保護的 4 GB 線性記憶體插槽中。
- Linux 記憶體保護金鑰 (
pku):LFI 使用 Linux 記憶體保護金鑰 (pku/pkey_mprotect) 分割位址空間權限。隔離的程式庫無法讀取或寫入指派的 4 GB 沙箱槽以外的記憶體。 - 系統呼叫攔截:沙箱內不允許任何主機 OS 系統呼叫 (例如檔案 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%,且不需變更任何應用程式程式碼。