Kể từ Android 17 (cấp độ API 37) và được cập nhật thông qua Bản cập nhật hệ thống Google Play (các mô-đun APEX Mainline), Android giới thiệu bộ giải mã âm thanh phần mềm trong quy trình cho các định dạng âm thanh nén, bao gồm Opus và AAC.
Bằng cách chạy trực tiếp bộ giải mã âm thanh trong quy trình của ứng dụng thay vì trong một quy trình hệ thống riêng biệt được cách ly, các ứng dụng có thể giảm độ trễ giải mã âm thanh khoảng 40%, giảm mức sử dụng CPU và kéo dài thời lượng pin trong quá trình phát nội dung nghe nhìn liên tục, nhắn tin thoại và chơi trò chơi.
Giải mã ngoài quy trình so với giải mã trong quy trình
Trước đây, Android chạy tất cả các bộ giải mã nội dung nghe nhìn của phần mềm nền tảng trong một trình nền hệ thống chuyên dụng, được cách ly (mediaswcodec) để bảo vệ các ứng dụng và hệ điều hành khỏi các luồng bit nội dung nghe nhìn bị lỗi. Tuy nhiên, việc tạo hộp cát ngoài quy trình sẽ làm tăng mức hao tổn hiệu suất có thể đo lường được:
- Độ trễ giao tiếp giữa các quy trình (IPC): Mọi khung hình đầu vào âm thanh được đưa vào hàng đợi của bộ giải mã và mọi bộ đệm âm thanh PCM đã giải mã được trả về đều yêu cầu quá trình chuyển đổi tuần tự IPC Binder giữa các quy trình và chuyển đổi ngữ cảnh.
- Mức hao tổn bộ nhớ dùng chung: Việc chuyển vùng đệm giữa các tiến trình yêu cầu đồng bộ hoá bộ nhớ dùng chung (
C2Buffer) và quản lý bộ nhớ đệm. - Tranh chấp về việc lập lịch: Khi CPU chịu tải lớn, tranh chấp về việc lập lịch luồng giữa quy trình ứng dụng và
mediaswcodeccó thể gây ra tình trạng thiếu bộ nhớ đệm và âm thanh bị giật trong các quy trình âm thanh có độ trễ thấp (chẳng hạn như DAW, giao tiếp VoIP và trò chơi tương tác).
Việc chạy bộ giải mã nội dung nghe nhìn trong quy trình sẽ trực tiếp giải quyết những nút thắt hiệu suất này:
- Loại bỏ độ trễ IPC: Bỏ qua các giao dịch IPC của Trình liên kết và các công tắc ngữ cảnh, giảm độ trễ giải mã từ đầu đến cuối khoảng 40%.
- Giảm thiểu chi phí chung của vùng đệm: Truyền trực tiếp các vùng đệm âm thanh trong không gian bộ nhớ ứng dụng cục bộ mà không yêu cầu ánh xạ bộ nhớ dùng chung liên tiến trình.
- Ngăn hiện tượng giật khi lập lịch: Thực thi việc giải mã trên các luồng ưu tiên riêng của ứng dụng (chẳng hạn như luồng AAudio hoặc Oboe theo thời gian thực), ngăn tình trạng mất âm thanh và thiếu hụt bộ nhớ đệm khi hệ thống chịu tải lớn.
Tính an toàn của bộ nhớ với Rust
Trước đây, Android đã tách biệt các bộ giải mã phần mềm trong một quy trình riêng biệt (mediaswcodec) vì các bộ giải mã xử lý các luồng bit không đáng tin cậy từ các tệp nội dung nghe nhìn bên ngoài, khiến các lỗ hổng bảo mật làm hỏng bộ nhớ (chẳng hạn như tràn vùng đệm) trở thành mối lo ngại lớn về bảo mật.
Android có thể di chuyển an toàn các bộ giải mã phần mềm trực tiếp vào quy trình ứng dụng gọi bằng cách triển khai chúng bằng các ngôn ngữ an toàn về bộ nhớ như Rust hoặc cách ly chúng bằng tính năng Cô lập lỗi đơn giản (LFI).
Để loại bỏ an toàn chi phí hộp cát ngoài quy trình cho AAC (c2.android.inproc.aac.decoder), Android cung cấp một bộ giải mã được triển khai bằng Rust thuần tuý hoặc được bảo vệ bằng các cơ chế Giao diện hàm ngoài (FFI) Rust an toàn.
Rust được coi là an toàn cho quá trình giải mã trong quy trình vì những lý do sau:
- Tính năng an toàn bộ nhớ tại thời gian biên dịch: Quyền sở hữu, hoạt động mượn và kiểm tra thời gian tồn tại nghiêm ngặt của Rust đảm bảo rằng bộ nhớ không thể truy cập sau khi được giải phóng hoặc bị đột biến trong khi được chia sẻ.
- Loại bỏ các lỗ hổng bảo mật thường gặp: Rust hoàn toàn ngăn chặn tình trạng tràn vùng đệm heap, stack smash, đọc và ghi mảng ngoài giới hạn, lỗi sử dụng sau khi giải phóng bộ nhớ và các lỗ hổng bảo mật giải phóng bộ nhớ hai lần tại thời gian biên dịch.
- Không tốn chi phí hộp cát trong thời gian chạy: Vì độ an toàn của bộ nhớ được chứng minh bằng toán học tại thời gian biên dịch và được xác minh thông qua kiểm tra ranh giới, nên các bộ giải mã Rust thực thi ở tốc độ gốc trong quy trình ứng dụng mà không yêu cầu chuyển đổi bảng trang hoặc ngữ cảnh hộp cát phần cứng.
Tính năng an toàn bộ nhớ với tính năng Cách ly lỗi đơn giản (LFI)
Đối với các bộ giải mã âm thanh C/C++ cũ phức tạp chưa được viết lại bằng Rust – cụ thể là bộ giải mã Opus (c2.android.inproc.opus.decoderdo libopus cung cấp) – Android cung cấp tính năng an toàn trong quy trình bằng cách sử dụng Tính năng cách ly lỗi đơn giản (LFI).
LFI được coi là an toàn cho các bộ giải mã C/C++ vì những lý do sau:
- Hộp cát bộ nhớ tuyến tính được bảo vệ bằng phần cứng: LFI biên dịch thư viện codec C/C++ thành mã máy tuyến tính có nguồn gốc từ WebAssembly, được giới hạn trong một khe bộ nhớ tuyến tính 4 GB được bảo vệ bằng phần cứng.
- Khoá bảo vệ bộ nhớ Linux (
pku): LFI sử dụng Khoá bảo vệ bộ nhớ Linux (pku/pkey_mprotect) để phân vùng quyền không gian địa chỉ. Thư viện biệt lập không thể đọc hoặc ghi bộ nhớ bên ngoài khe hộp cát 4 GB được chỉ định. - Chặn lệnh gọi hệ thống: Không cho phép các lệnh gọi hệ thống của hệ điều hành máy chủ (chẳng hạn như I/O tệp, quyền truy cập mạng hoặc quyền kiểm soát quy trình) bên trong hộp cát. Mọi lệnh gọi thư viện không được hỗ trợ đều được định tuyến đến các phần giữ chỗ giả tối thiểu (
lfi_stubs.c). - Khôi phục lỗi an toàn: Nếu một luồng bit Opus bị lỗi hoặc có hại cố gắng đọc hoặc ghi ngoài giới hạn trong hộp cát tuyến tính, thì thời gian chạy LFI sẽ bẫy lỗi một cách an toàn trong không gian người dùng và trả về lỗi giải mã
C2_CORRUPTEDrõ ràng mà không làm hỏng ứng dụng lưu trữ.
Các thiết bị và cấu trúc được hỗ trợ cho LFI
LFI không yêu cầu các chỉ dẫn phần cứng CPU tuỳ chỉnh và được hỗ trợ trên các kiến trúc bộ xử lý ARM64 (aarch64) và x86_64:
- Thiết bị ARM64 (
aarch64): Hầu hết các thiết bị di động tiêu dùng (bao gồm cả điện thoại di động (chẳng hạn như các thiết bị Pixel có SoC Google Tensor, Qualcomm Snapdragon và MediaTek SoC), thiết bị có thể gập lại, máy tính bảng và các thiết bị thông tin giải trí trên ô tô) đều chạy trên bộ xử lý ARM64 và hỗ trợ bộ giải mã trong quy trình LFI. - Thiết bị x86_64: Trình mô phỏng Android chạy trên máy trạm dành cho nhà phát triển có CPU Intel hoặc AMD, cũng như Chromebook ChromeOS chạy các ứng dụng Android bằng ARC (Android Runtime for ChromeOS), thực thi trên kiến trúc x86_64 và hỗ trợ hộp cát LFI.
Bộ giải mã âm thanh trong quy trình có sẵn
| Định dạng âm thanh | Loại MIME | Tên thành phần trong quy trình | Triển khai |
|---|---|---|---|
| Opus | audio/opus |
c2.android.inproc.opus.decoder |
C/C++ libopus có LFI |
| AAC | audio/mp4a-latm |
c2.android.inproc.aac.decoder |
Rust an toàn về bộ nhớ (C2ApexAacDec) |
- AAC (
c2.android.inproc.aac.decoder): Xử lý luồng ADTS (với tính năng phân tích cú pháp tiêu đề ADTS động) và Đơn vị truy cập (AU) thô với tính năng theo dõi dấu thời gian trình bày chính xác (chỉ xử lý AU một khung hình trong trường hợpAAC_PACKAGING_RAW). - Opus (
c2.android.inproc.opus.decoder): Giải mã các gói vùng chứa Ogg Opus hoặc Matroska tiêu chuẩn bằng cách lấy mẫu lại nội bộ thành PCM 48 kHz.
Chọn sử dụng bộ giải mã trong quy trình
Hiện tại, bộ giải mã trong quy trình không phải là mặc định của hệ thống khi gọi MediaCodec.createDecoderByType().
Nền tảng này giữ lại mức ưu tiên lựa chọn Codec 2.0 cao hơn cho các bộ giải mã ngoài quy trình cũ trong quá trình xác minh hệ sinh thái.
Để chọn sử dụng bộ giải mã trong quy trình ngay hôm nay để phát hoặc kiểm thử độ trễ thấp, hãy tạo thực thể rõ ràng cho codec theo tên thành phần bằng cách sử dụng MediaCodec.createByCodecName().
Khởi tạo theo tên thành phần
Ví dụ sau đây minh hoạ cách tạo thực thể rõ ràng cho trình giải mã AAC trong quy trình, quay lại trình giải mã mặc định nếu thành phần trong quy trình không có trên thiết bị:
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); } }
Định cấu hình Jetpack Media3 hoặc ExoPlayer
Nếu ứng dụng của bạn sử dụng Jetpack Media3 hoặc ExoPlayer, bạn có thể tạo một MediaCodecSelector tuỳ chỉnh để ưu tiên các bộ giải mã trong quy trình khi chúng có sẵn trên thiết bị:
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; } }
Lộ trình cho bộ giải mã hệ thống mặc định
Android đang tích cực chuẩn bị chuyển đổi các bộ giải mã an toàn về bộ nhớ trong quy trình này để trở thành bộ giải mã mặc định của hệ thống cho các loại MIME âm thanh tương ứng trong các bản phát hành nền tảng sắp tới (bắt đầu bằng Opus LFI và AAC Rust trong Android 18 hoặc các bản cập nhật mô-đun Mainline sắp tới).
Sau khi bộ giải mã trong quy trình trở thành thành phần Codec 2.0 mặc định cho loại MIME của bộ giải mã đó, các lệnh gọi tiêu chuẩn đến MediaCodec.createDecoderByType(...) sẽ tự động định tuyến thông qua quá trình triển khai trong quy trình, giúp giảm độ trễ giải mã khoảng 40% mà không cần thay đổi mã ứng dụng.