הגדרת תקציבי זיכרון לאפליקציות

תקציבי זיכרון לאפליקציות מאפשרים לאפליקציות להצהיר על תקציב זיכרון משלהן, וכך המערכת יודעת לצמצם את השימוש בזיכרון כשהאפליקציה משתמשת ביותר מהתקציב שהוגדר לה. השיטה הזו שימושית במיוחד לאפליקציות מערכת ולאפליקציות שכלולות בחבילה, או לאפליקציות שמיועדות למכשירים עם מגבלות זיכרון. במקרים כאלה, המפתח יודע מהו הזיכרון הצפוי של קבוצת העבודה ורוצה לוודא שהאפליקציה לא תשתמש ביותר מדי משאבי זיכרון RAM משותפים של המערכת.

התקציב נשמר מאוזן באמצעות הוצאה מהזיכרון והחלפה כדי להסיר דפי זיכרון שלא נעשה בהם שימוש לאחרונה, וכך להתמקד בטביעת הרגל של הזיכרון של האפליקציה בסט העבודה הנוכחי שלה. כשאפליקציה חורגת מהתקציב שהוגדר לה, מערכת ההפעלה מכוונת את ההחזרים באופן ספציפי לאפליקציה הזו:

  1. דפים שגובו בקבצים (כמו קוד לא פעיל ונכסים ממופים) מפונים קודם כי אפשר לקרוא אותם מחדש מהאחסון אם צריך.
  2. דפים עם גיבוי בקובץ ששונו נכתבים בחזרה לאחסון ומוצאים מהזיכרון.
  3. דפי זיכרון אנונימיים (כמו הקצאות של ערימה) נדחסים ומועברים ל-zRAM.

כל עוד התקציב לא חורג מסט העבודה, האפליקציה תפעל בצורה טובה ולא תשתמש ביותר זיכרון מהתקציב שהוגדר לה. מערכת ההפעלה מפנה זיכרון שלא נמצא בשימוש ודוחסת דפי ערימה לא פעילים להחלפה, כדי שהקצאות הזיכרון יישארו מוגבלות בלי להפסיק את התהליך.

הצהרה על תקציבים במניפסט של Android

ההצהרה על תקציבי הזיכרון ב-AndroidManifest.xml היא השיטה העיקרית והמומלצת להגדרת תקציבים. הוא לא דורש קוד בזמן ריצה, הוא נכנס לתוקף מיד עם הפעלת התהליך ומספק חוזה ברור למערכת ההפעלה.

הצהרה על תקציב בסיסי

ברוב האפליקציות, מספיק להגדיר תקציב יחיד לאפליקציה. מצהירים על רכיב <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="foreground" בסעיף הראשון. פסוקית ללא android:state משמשת כברירת מחדל לכל הערכים הדינמיים. הסעיפים המגבילים יותר שמתחתיו מבטלים את התקציב כשהאפליקציה עוברת למצבים perceptible או background.

אפליקציות מרובות תהליכים

אם האפליקציה מחלקת את העבודה שלה בין כמה תהליכים, צריך להגדיר תקציבים ייעודיים לתהליכים באמצעות התג <process> בתוך <processes>.

לדוגמה, אפליקציה לסטרימינג של מוזיקה (com.example.radio):

  1. התהליך הראשי: מארח את ממשק המשתמש הגלוי ואת מנוע הפעלת האודיו (MediaSessionService עם שירות שפועל בחזית mediaPlayback). כשהתהליך גלוי, הוא פועל במסגרת התקציב של 180MB בחזית. כשהמשתמש יוצא מהאפליקציה בזמן שהמוזיקה ממשיכה להתנגן, התהליך עובר למצב perceptible, שבו תקציב של 64MB מספיק למנוע ההפעלה ולמאגר האודיו.
  2. תהליך הסנכרון (: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.59M פיקסלים), התוספת הזו היא של כ-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 מאפשר לכם:

  • שאילתה לגבי השימוש הנוכחי בזיכרון והתקציבים האפקטיביים.
  • התאמה דינמית של תקציב התהליך כלפי מטה.
  • כדאי להאזין לאירועים של חריגה מהתקציב כדי לצמצם באופן יזום את מטמון הנתונים לפני שמערכת ההפעלה מפעילה שחזור ישיר.

‫Kotlin API ‏ (MemoryBudgetManager)

שירות המערכת MemoryBudgetManager זמין החל מ-Android 17 QPR2 (גרסת Android 26Q4 SDK, רמת API‏ 37.2 / 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-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit
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)

שיטות מומלצות לשימוש בהתקשרות חוזרת כשהתקציב חורג:

  • מהירות: פעולות ההחזרה צריכות לספק הקלה מיידית. חישובים מורכבים במהלך עומס המחשוב מחמירים את הביצועים.
  • הימנעות מהקצאות: אל תקצו אובייקטים חדשים או תפעילו שרשורים חדשים בתוך פונקציית הקריאה החוזרת, כי פעולה כזו עלולה להפעיל באופן מיידי את התהליך הישיר של מערכת ההפעלה להחזרת זיכרון.
  • מתמקדים ביעדים עם פוטנציאל גבוה: פינוי של מפות סיביות גדולות, מאגרי עיבוד או סגירה של קבצים עם מיפוי זיכרון יעילים הרבה יותר משחרור של הרבה אובייקטים קטנים.

Native NDK API (<android/memory_budget_manager.h>)

אפליקציות Native יכולות להשתמש ב-C NDK API שנחשף על ידי libandroid.so.

הגדרת 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
AMemoryBudgetManager_clearProcessBudget();

מעקב אחרי אירועים של עומס על הזיכרון

ב-NDK יש שתי דרכים לעקוב אחרי אירועי זיכרון:

  1. רכיב High-Level Watcher‏ (AMemoryBudgetManager_Watcher_create): עוקב אחרי אירועים ב-ALooper עם ביטול כפילויות אוטומטי.
  2. 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 של זמן הריצה ב-Kotlin וב-C++.

דוגמה ל-Kotlin: כלי לעריכת תמונות שמותאם לכל המכשירים

בדוגמה הזו מוצגת אפליקציה לעריכת תמונות (com.example.imageeditor) שמעלה באופן דינמי את תקציב הזיכרון שלה כשמשתמש פותח בדף הבית קנבס לעריכה רב-שכבתית, ומנקה את התקציב הדינמי כשחוזרים לתצוגת הגלריה של התמונות הממוזערות. הוא גם רושם 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)
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     */
    fun enterEditingCanvas() {
        try {
            // Dynamically set budget to 256MB for the editing canvas
            budgetManager.processBudgetBytes = 256L * 1024L * 1024L
            Log.i(TAG, "Dynamic budget applied: 256MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        // Clear dynamic budget; restores the baseline manifest budget
        budgetManager.clearProcessBudget()
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

דוגמה ל-NDK C++‎: מנוע תלת-ממד מקורי

בדוגמה הזו מוצג מנוע משחק מקורי ב-C++‎ שמנהל תקציבי זיכרון על סמך רמת איכות הגרפיקה הפעילה, באמצעות AMemoryBudgetManager_Watcher_create ב-ALooper כדי לבטל את הטעינה של מפות מיפ (mipmap) של טקסטורות כשהתקציב חורג.

#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;
};