החל מ-Android 17 (רמת API 37) ומעדכונים דרך Google Play System Updates (מודולי Mainline APEX), Android מציגה מפענחי אודיו של תוכנה בתהליך לפורמטים של אודיו דחוס, כולל Opus ו-AAC.
הפעלת מפענחי אודיו ישירות בתהליך של האפליקציה ולא בתהליך מערכת נפרד בסביבת ארגז חול מאפשרת לאפליקציות להפחית את זמן האחזור של פענוח האודיו בכ-40%, להפחית את השימוש ביחידת העיבוד המרכזית (CPU) ולהאריך את חיי הסוללה במהלך הפעלה רציפה של מדיה, הודעות קוליות ומשחקים.
פענוח מחוץ לתהליך לעומת פענוח בתוך התהליך
בעבר, מערכת Android הפעילה את כל מפענחי המדיה של תוכנת הפלטפורמה בתוך דימון (daemon) ייעודי של מערכת ארגז חול (mediaswcodec), כדי להגן על האפליקציות ועל מערכת ההפעלה מפני זרמי ביטים של מדיה עם נתונים פגומים. עם זאת, ארגז חול מחוץ לתהליך יוצר תקורה מדידה של ביצועים:
- השהיה של תקשורת בין תהליכים (IPC): כל מסגרת של קלט אודיו שמתווספת לתור של המפענח וכל מאגר של אודיו PCM מפוענח שמוחזר דורשים סריאליזציה של IPC ב-Binder בין תהליכים והחלפת הקשר.
- תקורה של זיכרון משותף: העברות של מאגר נתונים זמני בין תהליכים דורשות סנכרון של זיכרון משותף (
C2Buffer) וניהול מטמון. - תזמון של תחרות: בעומס גבוה של CPU, תחרות על תזמון של שרשור בין תהליך האפליקציה לבין
mediaswcodecעלולה לגרום למחסור במאגר ולגמגום של אודיו בצינורות אודיו עם השהיה נמוכה (כמו DAW, תקשורת VoIP ומשחקים אינטראקטיביים).
הפעלת מפענחי מדיה בתהליך פותרת ישירות את צווארי הבקבוק האלה בביצועים:
- מבטל את זמן האחזור של IPC: עוקף את עסקאות ה-IPC של Binder ואת החלפות ההקשר, ומקטין את זמן האחזור של פענוח מקצה לקצה בכ-40%.
- מצמצם את התקורה של המאגר: מעביר מאגרי שמע ישירות בתוך מרחב הזיכרון של האפליקציה המקומית, בלי לדרוש מיפויים של זיכרון משותף בין תהליכים.
- מונע שינויים לא צפויים בתזמון: מבצע פענוח בשרשורים של האפליקציה עצמה (כמו שרשורי AAudio או Oboe בזמן אמת), וכך מונע הפסקות פתאומיות באודיו וגלישות של מאגר הנתונים מתחת לקיבולת שלו במקרים של עומס כבד על המערכת.
בטיחות הזיכרון באמצעות Rust
בעבר, מערכת Android ביצעה בידוד של מפענחי תוכנה בתהליך נפרד (mediaswcodec) כי מפענחים מעבדים זרמי ביטים לא מהימנים מקובצי מדיה חיצוניים, ולכן ניצול לרעה של פגיעות בזיכרון (כמו גלישת חוצץ) היה בעיה אבטחה משמעותית.
מערכת Android יכולה להעביר בבטחה מפענחי תוכנה ישירות לתהליך של האפליקציה שקוראת להם, על ידי הטמעה שלהם בשפות בטוחות לזיכרון כמו Rust או על ידי בידוד שלהם באמצעות Lightweight Fault Isolation (LFI).
כדי לבטל בבטחה את התקורה של ארגז חול (Sandbox) מחוץ לתהליך עבור AAC
(c2.android.inproc.aac.decoder), Android מספק מפענח שמוטמע ב-Rust טהור או שמוגן על ידי רכיבי FFI (ממשק פונקציות זרות) בטוחים של Rust.
השפה Rust נחשבת בטוחה לפענוח בתהליך מהסיבות הבאות:
- בטיחות זיכרון בזמן קומפילציה: בדיקות הבעלות, ההשאלה והתוקף המחמירות של Rust מבטיחות שלא תהיה גישה לזיכרון אחרי שהוא ישוחרר או ישונה בזמן שהוא משותף.
- ביטול נקודות חולשה נפוצות: Rust מונעת לחלוטין גלישות חוצץ (buffer overflow) ב-heap, התנגשויות ב-stack, קריאות וכתיבות של מערכים מחוץ לגבולות, שגיאות use-after-free ונקודות חולשה של double-free בזמן הקומפילציה.
- תקורה אפסית של ארגז חול בזמן ריצה: בגלל שבטיחות הזיכרון מוכחת מתמטית בזמן הקומפילציה ומאומתת באמצעות בדיקת גבולות, מפענחי Rust פועלים במהירות מקורית בתוך תהליך האפליקציה בלי לדרוש מעברים של טבלת דפים או הקשרים של ארגז חול בחומרה.
בטיחות הזיכרון באמצעות בידוד תקלות קל משקל (LFI)
למפענחי אודיו מורכבים מדור קודם של C/C++ שלא נכתבו מחדש ב-Rust – במיוחד למפענח Opus (c2.android.inproc.opus.decoderמופעל על ידי libopus) – מערכת Android מספקת בטיחות בתהליך באמצעות בידוד תקלות קל משקל (LFI).
ה-LFI נחשב בטוח למפענחי C/C++ מהסיבות הבאות:
- ארגז חול לזיכרון ליניארי עם הגנה ברמת החומרה: LFI מהדר את ספריית רכיבי ה-Codec של C/C++ לקוד מכונה ליניארי שנגזר מ-WebAssembly, שמוגבל למשבצת זיכרון ליניארי של 4GB עם הגנה ברמת החומרה.
- Linux Memory Protection Keys (
pku): LFI משתמש ב-Linux Memory Protection Keys (pku/pkey_mprotect) כדי לחלק את ההרשאות של מרחב הכתובות. הספרייה המבודדת לא יכולה לקרוא או לכתוב זיכרון מחוץ למקום המוקצה לה בארגז החול של 4GB. - יירוט של קריאות למערכת: אסור לבצע קריאות למערכת של מערכת ההפעלה המארחת (כמו קלט/פלט של קבצים, גישה לרשת או שליטה בתהליך) בתוך ארגז החול. כל קריאה לספרייה שלא נתמכת מנותבת ל-stub מינימלי (
lfi_stubs.c). - שחזור בטוח של תקלות: אם זרם ביטים של Opus שנוצר בצורה לא תקינה או זדונית מנסה לבצע קריאה או כתיבה מחוץ לגבולות של ארגז החול הליניארי, זמן הריצה של LFI לוכד את התקלה בצורה בטוחה במרחב המשתמש ומחזיר שגיאת פענוח נקייה של
C2_CORRUPTEDבלי לגרום לקריסת אפליקציית המארח.
ארכיטקטורות ומכשירים נתמכים עבור LFI
התכונה LFI לא דורשת הוראות חומרה מותאמות אישית במעבד, והיא נתמכת בארכיטקטורות של מעבדים מסוג ARM64 (aarch64) ו-x86_64:
- מכשירי ARM64 (
aarch64): רוב המכשירים הניידים לצרכנים – כולל טלפונים ניידים (כמו מכשירי Pixel עם מערכות על שבב Google Tensor, Qualcomm Snapdragon ו-MediaTek), מכשירים מתקפלים, טאבלטים ויחידות מידע ובידור לרכב – פועלים על מעבדי ARM64 ותומכים במפענחי LFI בתהליך. - מכשירי x86_64: אמולטורים של Android שפועלים בתחנות עבודה של מפתחים עם מעבדי Intel או AMD, וגם מכשירי Chromebook עם ChromeOS שמריצים אפליקציות ל-Android באמצעות ARC (Android Runtime for ChromeOS), פועלים בארכיטקטורות 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). - Opus (
c2.android.inproc.opus.decoder): מפענח מנות של קונטיינר Ogg Opus או Matroska רגילות עם דגימה מחדש פנימית ל-PCM של 48kHz.
הצטרפות למפענחים בתהליך
נכון לעכשיו, מפענחים בתהליך לא מוגדרים כברירת המחדל של המערכת כשמתקשרים אל 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 מתכוננת באופן פעיל להעביר את מפענחי הזיכרון הבטוחים האלה בתוך התהליך, כך שיהפכו למפענחי ברירת המחדל של המערכת עבור סוגי ה-MIME של האודיו המתאימים בגרסאות פלטפורמה עתידיות (החל מ-Opus LFI ו-AAC Rust ב-Android 18 או בעדכונים עתידיים של מודול Mainline).
אחרי שמפענח בתהליך הופך לרכיב ברירת המחדל של Codec 2.0 עבור סוג ה-MIME שלו, קריאות רגילות ל-MediaCodec.createDecoderByType(...) מנותבות אוטומטית דרך ההטמעה בתהליך, ומספקות חביון פענוח נמוך בכ-40% בלי שנדרשים שינויים בקוד האפליקציה.