תקציבי זיכרון לאפליקציות מאפשרים לאפליקציות להצהיר על תקציב זיכרון משלהן, וכך המערכת יודעת לצמצם את השימוש בזיכרון כשהאפליקציה משתמשת ביותר מהתקציב שהוגדר לה. השיטה הזו שימושית במיוחד לאפליקציות מערכת ולאפליקציות בחבילה, או לאפליקציות שמיועדות למכשירים עם מגבלות זיכרון. במקרים כאלה, המפתח יודע מהו זיכרון העבודה הצפוי של האפליקציה ורוצה לוודא שהיא לא תשתמש ביותר מדי משאבים של זיכרון ה-RAM המשותף של המערכת.
כדי לשמור על איזון בתקציב, נעשה שימוש בהוצאה מהזיכרון והחלפה כדי להסיר דפי זיכרון שלא נעשה בהם שימוש לאחרונה, וכך להקטין את טביעת הרגל של הזיכרון של האפליקציה ולהתמקד בסט העבודה הנוכחי שלה. כשאפליקציה חורגת מהתקציב שהוגדר לה, מערכת ההפעלה מכוונת את ההחזרים באופן ספציפי לאפליקציה הזו:
- דפים שגובו בקבצים (כמו קוד לא פעיל ונכסים ממופים) מפונים קודם כי אפשר לקרוא אותם מחדש מהאחסון אם צריך.
- דפים עם גיבוי קבצים ששונו נכתבים בחזרה לאחסון ומוצאים מהזיכרון.
- דפי זיכרון אנונימיים (כמו הקצאות של ערימה) נדחסים ומועברים ל-zRAM.
כל עוד התקציב לא חורג מהסט הפעיל, האפליקציה תפעל בצורה טובה ולא תשתמש ביותר זיכרון מהתקציב שהוגדר לה. מערכת ההפעלה מפנה זיכרון שלא נמצא בשימוש ודוחסת דפי ערימה לא פעילים להחלפה, כדי שהקצאות הזיכרון יישארו מוגבלות בלי להפסיק את התהליך.
הצהרה על תקציבים במניפסט של Android
ההצהרה על תקציבי הזיכרון ב-AndroidManifest.xml היא השיטה העיקרית והמומלצת להגדרת תקציבים. הוא לא דורש קוד בזמן ריצה, הוא נכנס לתוקף מיד עם הפעלת התהליך ומספק חוזה ברור למערכת ההפעלה.
ההצהרות <memory-budget> נכנסות לתוקף במכשירים שמותקנת בהם גרסת Android 17 QPR2 (API ברמה 37.2) ומעלה. בגרסאות נמוכות יותר של Android, מנתח ה-manifest של הפלטפורמה מתעלם בבטחה מרכיבי XML לא מוכרים, כך שאפשר להשתמש ב-<memory-budget> בלי להשפיע על תאימות לדור קודם.
הצהרה על תקציב בסיסי
ברוב האפליקציות, הגדרה של תקציב יחיד לאפליקציה היא כל מה שצריך. מצהירים על רכיב <memory-budget> ישירות בתוך התג <application>:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Baseline budget for the application -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
הפעולה הזו מגדירה תקציב של 256MB לזיכרון תושב בכל התהליכים והמצבים של החבילה. כשגודל הזיכרון שבשימוש של האפליקציה חורג מ-256MB, מערכת ההפעלה מצמצמת את דפי הזיכרון הלא פעילים באמצעות פינוי והחלפה.
שינוי התקציבים לפי מצב התהליך
האפליקציה דורשת כמויות שונות של זיכרון בהתאם למידת החשיפה שלה למשתמשים:
- חזית: התהליך מארח פעילות גלויה שמתקיימת באינטראקציה עם המשתמש. בדרך כלל, המצב הזה תופס הכי הרבה מקום בגלל ממשק המשתמש והגרפיקה הפעילים.
- מורגש: התהליך מורגש למשתמש, אבל לא מוצג חלון גלוי (לדוגמה, אירוח של שירות שפועל בחזית להפעלת מדיה, מסלול מפורט או שיטת קלט פעילה).
- רקע: התהליך מריץ משימות ברקע, מקלטים או סנכרוני נתונים. הוא צפוי לשמור על טביעת רגל מינימלית.
אפשר להצהיר על כמה סעיפי <memory-budget> כדי להתאים למצבים האלה:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Default budget for visible foreground UI -->
<memory-budget android:maxMb="200" />
<!-- Tighter budget when playing audio in background -->
<memory-budget
android:maxMb="120"
android:state="perceptible" />
<!-- Minimal budget when fully in background -->
<memory-budget
android:maxMb="48"
android:state="background" />
</application>
</manifest>
אין צורך בסעיף בסיסי ללא android:state. אם מציינים רק סעיפים שספציפיים למדינה (לדוגמה, android:state="background"), מדינות אחרות לא מוגבלות על ידי תקציב האפליקציה. אם כוללים סעיף בלי android:state, הוא משמש כברירת מחדל למצבים לא מוגדרים (כמו פעילות באפליקציה בחזית), וסעיפים מגבילים יותר שמופיעים אחריו מבטלים אותו כשהאפליקציה עוברת למצבים perceptible או background.
אפליקציות מרובות תהליכים
אם האפליקציה מחלקת את העבודה שלה בין כמה תהליכים, צריך להגדיר תקציבים ייעודיים לתהליכים באמצעות התג <process> בתוך <processes>.
לדוגמה, אפליקציה לסטרימינג של מוזיקה (com.example.radio):
- התהליך הראשי: מארח את ממשק המשתמש הגלוי ואת מנוע הפעלת האודיו
(
MediaSessionServiceעם שירות שפועל בחזיתmediaPlayback). כשהתהליך גלוי, הוא פועל במסגרת תקציב של 180MB בפורגראונד. כשהמשתמש יוצא מהאפליקציה בזמן שהמוזיקה ממשיכה להתנגן, התהליך עובר למצבperceptible, שבו תקציב של 64MB מספיק למנוע ההפעלה ולמאגר האודיו. - תהליך הסנכרון (
:sync): תהליך ייעודי שפועל ברקע ומסנכרן מטא-נתונים וטוען אינדקסים. התהליך הזה פועל רק ברקע, ולכן לא צריך להצהיר במפורש עלstate="background". תקציב אחד חל על כל הקמפיינים.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.radio">
<application
android:label="@string/app_name">
<!-- Package baseline: main process with UI and audio playback -->
<memory-budget android:maxMb="180" />
<!-- Tighter budget when audio plays in the background -->
<memory-budget
android:maxMb="64"
android:state="perceptible" />
<!-- Dedicated background sync process -->
<processes>
<process android:process=":sync">
<memory-budget android:maxMb="32" />
</process>
</processes>
<service
android:name=".playback.AudioPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<service
android:name=".sync.PlaylistSyncService"
android:process=":sync"
android:exported="false" />
</application>
</manifest>
השימוש בזיכרון בכל תהליך משני נספר גם בתקציב התהליך וגם בתקציב החבילה שמכילה אותו. תהליך חווה עומס זיכרון בזמן הריצה אם הוא חורג מתקציב התהליך או מתקציב החבילה, לפי הסף שמגיעים אליו קודם.
התאמת תקציבים למסכים עם דחיסות גבוהה של פיקסלים
באפליקציות שבהן הזיכרון שבשימוש משתנה באופן משמעותי בהתאם למספר הפיקסלים שצריך לצייר במסך בכל פעם – כמו אפליקציית גלריית התמונות ששומרת במטמון מפות סיביות בגודל המסך – מערכת Android מספקת שני מנגנונים חלופיים להתאמה דינמית של התקציבים למפרט המסך:
הגדלה לפי קטגוריית צפיפות התצוגה (
android:additionalMbPerDensity): מוסיף מגה-בייט באופן יחסי ליחס הצפיפות של התצוגה ביחס ל-mdpi(1.0x / 160 dpi). האפשרות הזו מתאימה כששימוש הזיכרון גדל בהתאם לקטגוריות של צפיפות ממשק המשתמש, כמו שמירה במטמון של נכסי ממשק משתמש או של רכיבי drawable מסוג raster ברזולוציה גבוהה יותר:<!-- Baseline 180MB + 16MB per 1.0x density ratio --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />במסך
mdpi(1.0x), התקציב הוא 180 + 16 × 1 = 196MB. בתצוגה שלxxhdpi(3.0x), התקציב גדל ל-180 + 16 × 3 = 228MB.שינוי קנה מידה לפי רזולוציית התצוגה הפיזית (
android:additionalBytesPerDisplayPixel): הוספת בייטים ישירות לכל פיקסל בתצוגה הפיזית (רוחב × גובה). האפשרות הזו מתאימה במיוחד לאפליקציות שמקצות משטחי גרפיקה במסך מלא, מאגרי טיוח או מטמוני תמונות ברזולוציה מלאה, שבהם צריכת הזיכרון גדלה באופן ישיר עם מספר הפיקסלים הגולמיים במסך ולא עם סיווגים של צפיפות ממשק המשתמש:<!-- Baseline 128MB + 16 bytes per physical display pixel --> <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering --> <memory-budget android:maxMb="128" android:additionalBytesPerDisplayPixel="16" />בתצוגה ברזולוציית 1080p (1080 × 2400, כ-2.59 מיליון פיקסלים), התוספת הזו היא של כ-41.4MB לתקציב הבסיסי. במסך ברזולוציה של 1440p (1440 × 3120 ו-4.49M פיקסלים בקירוב), הוא מוסיף כ-71.8MB.
שני המאפיינים האלה הם חלופות. בוחרים את המאפיין שמתאים לגורם ההתאמה הראשי של האפליקציה, ולא משלבים את שניהם באותו סעיף.
התאמה לגורמי צורה של מכשירים
כששולחים קובץ APK לטלפונים, לטאבלטים ולמכשירי Wear OS, משתמשים במאפיין android:feature כדי להתאים את התקציבים ליעדי חומרה שונים.
בשעוני Wear OS, זיכרון ה-RAM מוגבל, וממשק המשתמש של האפליקציה והתכונות שלה פשוטים הרבה יותר. אתם יכולים להגדיר תקציב מצומצם יותר שמתאים במיוחד לתכונה watch:
<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />
<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
android:maxMb="48"
android:feature="watch" />
כלל ההכרעה: הסעיף האחרון הרלוונטי נכנס לתוקף
כשמגדירים כמה רכיבי <memory-budget> לאפליקציה או לתהליך, המערכת מעריכה אותם לפי הסדר שבו הם מוצהרים במניפסט. הסעיף התקציבי הרלוונטי האחרון הוא הסעיף שמיושם.
התקציב האחרון שרלוונטי הוא זה שייבחר, ולכן לסדר יש חשיבות. תמיד צריך להציב קודם את תקציב הבסיס הכללי ביותר, ואחריו חריגות ספציפיות יותר (כמו סעיפים שספציפיים למדינה או לחומרה).
מסמך עזר בנושא מאפייני XML
כל מאפייני גודל הזיכרון מבוטאים במגה-בייט (MB) וממופים לחיוב של cgroup ב-Linux memory.current (שלא כולל זיכרון משותף כמו Zygote).
| מאפיין | פורמט | ברירת מחדל | תיאור |
|---|---|---|---|
android:maxMb |
מספר שלם (> 0) | חובה | מגבלת הבסיס של הזיכרון התושב ב-MB. |
android:state |
ספירה | כל צבע | מצב התהליך שאליו התקציב הזה חל: foreground, perceptible או background. אם לא מציינים מצב, הסעיף משמש כברירת מחדל לכל מצב שלא צוין. |
android:additionalMbPerDensity |
מספר שלם (≥ 0) | 0 |
מספר המגה-בייט הנוסף שצריך להוסיף לכל יחידה של יחס צפיפות התצוגה ביחס ל-mdpi (1.0x). |
android:additionalBytesPerDisplayPixel |
מספר שלם (≥ 0) | 0 |
מספר הבייטים הנוספים שהוקצו לכל פיקסל במסך הפיזי (רוחב × גובה), שימושי עבור מאגרי נתונים זמניים של משטחים ומפות סיביות. |
android:feature |
מחרוזת | כל צבע | מגביל את הסעיף למכשירים שמצהירים על תכונות חומרה ספציפיות: watch, automotive או leanback. |
ממשקי API של זמן ריצה (אפשרות דינמית משנית)
הפתרון המועדף כמעט לכל האפליקציות הוא הגדרת תקציבים באופן סטטי ב-AndroidManifest.xml. עם זאת, לאפליקציות עם עומסי עבודה דינמיים או לניסויים בזמן ריצה, מערכת Android מספקת ממשקי API של SDK ו-NDK בזמן ריצה כאפשרות משנית.
Runtime API מאפשר לכם:
- שאילתה לגבי השימוש הנוכחי בזיכרון והתקציבים האפקטיביים.
- התאמה דינמית של תקציב התהליך כלפי מטה.
- כדאי להאזין לאירועים של חריגה מהתקציב כדי לצמצם את המטמון באופן יזום לפני שמערכת ההפעלה מפעילה שחזור ישיר.
Android SDK API (MemoryBudgetManager)
שירות המערכת MemoryBudgetManager זמין לאפליקציות שנכתבו ב-Kotlin וב-Java החל מ-Android 17 QPR2 (גרסת SDK משנית, רמת API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).
אחזור השירות
לפני שפותחים את MemoryBudgetManager, צריך לוודא שבמכשיר לא פועלת גרסה נמוכה יותר מ-Android 17 QPR2 באמצעות SDK_INT_FULL:
if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2) {
val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}
שימוש בשאילתות ותקציבים
// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes
// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes
הגדרה או ביטול של תקציבים באופן דינמי
אפשר להגדיר תקציב מצומצם יותר בזמן הריצה כדי להגביל את הזיכרון במהלך משימות קלות, או לנקות אותו כשהמשימה מסתיימת:
// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
// Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}
// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()
האזנה לקודים להתקשרות חזרה (callback) שמופעלים כשחורגים מהתקציב
אפליקציות יכולות לרשום מאזין כדי לקבל הודעה כששימוש הזיכרון חורג מסף התקציב. כך האפליקציה יכולה לבצע ניקוי יזום ברמת האפליקציה (למשל, ניקוי מטמוני bitmap בזיכרון) לפני שמערכת ההפעלה מפעילה השבתה ישירה של זמן האחזור:
val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
// Proactively evict caches to release memory
imageTileCache.evictAll()
}
// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)
// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)
שיטות מומלצות לשימוש בפונקציות קריאה חוזרת (callback) במקרים של חריגה מהתקציב:
- מהירות: פעולות השחזור צריכות לספק פתרון מיידי. חישובים מורכבים במהלך עומס המחשוב מחמירים את הביצועים.
- הימנעות מהקצאות: אל תקצו אובייקטים חדשים או תפעילו שרשורים חדשים בתוך פונקציית הקריאה החוזרת, כי פעולה כזו עלולה להפעיל באופן מיידי את התהליך הישיר של מערכת ההפעלה להחזרת זיכרון.
- מתמקדים ביעדים עם פוטנציאל גבוה: פינוי של מפות סיביות גדולות, מאגרי עיבוד או סגירה של קבצים עם מיפוי זיכרון יעילים הרבה יותר משחרור של הרבה אובייקטים קטנים.
Native NDK API (<android/memory_budget_manager.h>)
אפליקציות מקוריות יכולות להשתמש ב-API של C NDK שנחשף על ידי libandroid.so החל מ-Android 17 QPR2 (רמת API 37.2).
הגדרת CMake
find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})
הכללת נתוני השימוש בכותרות ובשאילתות
#include <android/memory_budget_manager.h>
// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();
// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
// Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
// No budget is currently active
}
הגדרה דינמית של תקציב מקומי
// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
const char* error_msg = AMemoryBudgetManager_resultToString(result);
// Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}
// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();
מעקב אחרי אירועים של עומס על הזיכרון
ב-NDK יש שתי דרכים לעקוב אחרי אירועי זיכרון:
- רכיב High-Level Watcher (
AMemoryBudgetManager_Watcher_create): עוקב אחרי אירועים ב-ALooperעם ביטול כפילויות אוטומטי. - Low-Level File Descriptor:
AMemoryBudgetManager_getProcessMemoryPressureFdמחזירה מתאר קובץ מקורי שאפשר לשלב ישירות בלולאה של מנועepollבהתאמה אישית.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
// High-yield eviction of unused native textures or geometry caches
purgeNativeTextureCaches();
}
// Register watcher on an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000 /* debounce_ms */,
&onMemoryPressure,
NULL /* userdata */
);
// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);
דוגמאות ל-Runtime API
בדוגמאות הבאות אפשר לראות איך ליישם את ממשקי ה-API של זמן הריצה באמצעות Android SDK API (שנכתב ב-Kotlin) ו-Native NDK API (שנכתב ב-C++).
דוגמה ל-Android SDK: עורך תמונות דינמי
בדוגמה הזו מוצגת אפליקציה לעריכת תמונות (com.example.imageeditor) שקובץ המניפסט שלה מגדיר מכסה של 256MB כדי להתאים לקנבס הרב-שכבתי שלה:
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
כשהמשתמש מעיין בגלריית התמונות הממוזערות הקלילה, האפליקציה משתמשת ב-Android SDK API ב-Kotlin כדי להקטין באופן דינמי את תקציב התהליך שלה ל-96MB.
כשהמשתמש פותח את אזור העריכה הרב-שכבתי, האפליקציה מוחקת את התקציב הדינמי כדי לשחזר את המקסימום של 256MB במניפסט. הוא גם רושם OnOverBudgetListener כדי לפנות זיכרון מטמון של מפות סיביות של תצוגות מקדימות במצבים של עומס.
package com.example.imageeditor.ui
import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.util.LruCache
class ImageEditorActivity : Activity() {
private lateinit var budgetManager: MemoryBudgetManager
// In-memory cache for rendered preview tiles (32MB limit)
private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
}
private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
previewCache.evictAll()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
budgetManager = getSystemService(MemoryBudgetManager::class.java)
}
override fun onStart() {
super.onStart()
// Register listener for process-level memory breaches
budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
// Constrain memory during lightweight gallery browsing
applyGalleryBudget()
}
override fun onStop() {
super.onStop()
budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
}
/**
* Called when the user enters the high-resolution editing canvas.
* Clears the dynamic budget, restoring the full 256MB manifest ceiling.
*/
fun enterEditingCanvas() {
// Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
budgetManager.clearProcessBudget()
Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
}
/**
* Called when the user exits the editor back to the thumbnail gallery.
* Re-applies the tighter dynamic budget.
*/
fun exitToGallery() {
previewCache.trimToSize(8 * 1024 * 1024)
applyGalleryBudget()
}
private fun applyGalleryBudget() {
try {
// Dynamically tighten budget to 96MB for the lightweight gallery view
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Could not apply dynamic budget", e)
}
}
companion object {
private const val TAG = "ImageEditor"
}
}
דוגמה ל-NDK C++: מנוע תלת-ממד מקורי
בדוגמה הזו מוצג מנוע משחק מקורי ב-C++ שמנהל תקציבי זיכרון על סמך רמת האיכות הגרפית הפעילה, בהנחה שבמניפסט של האפליקציה מוצהרת מכסת זיכרון של 512MB כדי להתאים לגרפיקה באיכות גבוהה (android:maxMb="512"). המנוע מצמצם באופן דינמי את התקציב עבור הגדרות קבועות מראש באיכות נמוכה יותר, ומשתמש ב-AMemoryBudgetManager_Watcher_create ב-ALooper כדי לבטל את הטעינה של מפות מיפמאפ של טקסטורות כשהתקציב חורג.
#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>
#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
class MemoryGovernor {
public:
MemoryGovernor() : mWatcher(nullptr) {}
~MemoryGovernor() {
stopMonitoring();
}
// Configures process budget based on user graphics quality settings
bool setQualityBudget(int qualityLevel) {
int64_t targetBytes = 0;
switch (qualityLevel) {
case 0: // Low (budget: 128MB)
targetBytes = 128LL * 1024 * 1024;
break;
case 1: // Medium (budget: 256MB)
targetBytes = 256LL * 1024 * 1024;
break;
case 2: // High (budget: 512MB)
targetBytes = 512LL * 1024 * 1024;
break;
default:
// Clear dynamic override and restore manifest limit
AMemoryBudgetManager_clearProcessBudget();
return true;
}
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
return false;
}
return true;
}
bool startMonitoring(ALooper* looper) {
if (!looper) return false;
// Monitor process budget events, debounced to at most once every 1000ms
mWatcher = AMemoryBudgetManager_Watcher_create(
looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000,
&MemoryGovernor::onPressureEvent,
this
);
return mWatcher != nullptr;
}
void stopMonitoring() {
if (mWatcher) {
AMemoryBudgetManager_Watcher_destroy(mWatcher);
mWatcher = nullptr;
}
}
void unloadUnusedTextures() {
LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
// Fast, high-yield eviction without allocating memory
}
private:
static void onPressureEvent(
int32_t event_mask,
const AMemoryBudgetEvents* events,
void* userdata
) {
auto* governor = static_cast<MemoryGovernor*>(userdata);
governor->unloadUnusedTextures();
}
AMemoryBudgetManagerWatcher* mWatcher;
};