تنظیم بودجه حافظه برنامه

بودجه‌بندی حافظه برنامه به برنامه‌ها اجازه می‌دهد تا برای خود یک بودجه حافظه اعلام کنند، که به سیستم می‌گوید وقتی برنامه بیشتر از بودجه تعیین‌شده‌اش از حافظه استفاده می‌کند، میزان استفاده از حافظه را کاهش دهد. این امر به ویژه برای برنامه‌های سیستمی و برنامه‌های همراه یا برنامه‌هایی که دستگاه‌های با محدودیت حافظه را هدف قرار می‌دهند، مفید است، جایی که توسعه‌دهنده از مجموعه کاری حافظه مورد انتظار خود آگاه است و می‌خواهد مطمئن شود که برنامه‌اش بیش از حد از منابع رم مشترک سیستم استفاده نمی‌کند.

بودجه با استفاده از تخلیه حافظه و مبادله برای حذف صفحات حافظه‌ای که اخیراً استفاده نشده‌اند، متعادل نگه داشته می‌شود و ردپای حافظه برنامه را بر روی مجموعه کاری فعلی آن متمرکز می‌کند. هنگامی که یک برنامه از بودجه اعلام شده خود فراتر می‌رود، سیستم عامل به طور خاص آن برنامه را هدف قرار می‌دهد:

  1. صفحات پاک‌شده‌ی دارای پشتیبان فایل (مانند کدهای غیرفعال و فایل‌های نگاشت‌شده) ابتدا حذف می‌شوند، زیرا در صورت نیاز می‌توان آن‌ها را از حافظه‌ی ذخیره‌سازی دوباره خواند.
  2. صفحات کثیفِ دارای پشتیبان فایل، دوباره به حافظه نوشته شده و حذف می‌شوند.
  3. صفحات حافظه ناشناس (مانند تخصیص‌های هیپ) فشرده شده و به zRAM منتقل می‌شوند.

تا زمانی که بودجه از مجموعه کاری تجاوز نکند، برنامه به خوبی کار خواهد کرد و حافظه‌ای بیش از بودجه تعیین‌شده مصرف نمی‌کند. سیستم عامل حافظه استفاده نشده را حذف کرده و صفحات هیپ غیرفعال را برای جابجایی فشرده می‌کند تا تخصیص حافظه بدون خاتمه فرآیند، محدود باقی بماند.

اعلام بودجه در مانیفست اندروید

اعلام بودجه حافظه در 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>

این یک بودجه حافظه ساکن ۲۵۶ مگابایتی را در تمام فرآیندها و حالت‌های بسته تعیین می‌کند. وقتی ردپای حافظه برنامه از ۲۵۶ مگابایت فراتر رود، سیستم عامل صفحات حافظه غیرفعال را با استفاده از حذف و تعویض حذف می‌کند.

بودجه‌ها را بر اساس وضعیت فرآیند تغییر دهید

یک برنامه بسته به میزان دید کاربر، به مقادیر مختلفی از حافظه نیاز دارد:

  • پیش‌زمینه : این فرآیند میزبان یک فعالیت قابل مشاهده است که با کاربر در تعامل است. این حالت معمولاً به دلیل رابط کاربری و گرافیک فعال، بیشترین حجم را اشغال می‌کند.
  • قابل درک : فرآیند برای کاربر قابل درک است اما میزبان یک پنجره قابل مشاهده نیست (برای مثال، میزبان یک سرویس پخش رسانه در پیش زمینه، ناوبری گام به گام یا یک روش ورودی فعال).
  • پس‌زمینه : این فرآیند در حال اجرای وظایف پس‌زمینه، گیرنده‌ها یا همگام‌سازی داده‌ها است. انتظار می‌رود که حداقل ردپا را حفظ کند.

شما می‌توانید چندین عبارت <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 ) است. وقتی قابل مشاهده است، فرآیند با بودجه پیش‌زمینه ۱۸۰ مگابایتی کار می‌کند. وقتی کاربر در حالی که موسیقی همچنان پخش می‌شود، برنامه را ترک می‌کند، فرآیند وارد حالت perceptible می‌شود، که در آن بودجه ۶۴ مگابایتی برای موتور پخش و بافر صدا کافی است.
  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:additionalMbPerDensity ) : مگابایت‌ها را متناسب با نسبت چگالی نمایشگر نسبت به mdpi (1.0x / 160 dpi) اضافه می‌کند. این مورد زمانی مناسب است که استفاده از حافظه با چگالی رابط کاربری، مانند ذخیره سازی فایل‌های رستری با وضوح بالاتر یا فایل‌های رابط کاربری، مقیاس‌بندی شود:

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    در یک نمایشگر mdpi (1.0x)، بودجه برابر است با 180 + 16 × 1 = 196 مگابایت. در یک نمایشگر xxhdpi (3.0x)، بودجه به 180 + 16 × 3 = 228 مگابایت افزایش می‌یابد.

  • مقیاس‌بندی بر اساس وضوح فیزیکی نمایشگر ( 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.4 مگابایت به بودجه پایه اضافه می‌کند. در یک نمایشگر 1440p (1440 × 3120 ≈ 4.49M پیکسل)، این مقدار تقریباً 71.8 مگابایت اضافه می‌کند.

این دو ویژگی جایگزین هستند. ویژگی‌ای را انتخاب کنید که با عامل مقیاس‌بندی اصلی برنامه شما مطابقت داشته باشد و از ترکیب هر دو در یک عبارت خودداری کنید.

تخصص در طراحی دستگاه‌هایی با فرم فاکتور متفاوت

هنگام ارسال یک فایل APK بین تلفن‌ها، تبلت‌ها و Wear OS، از ویژگی android:feature برای تنظیم بودجه برای اهداف سخت‌افزاری مختلف استفاده کنید.

در ساعت‌های Wear OS، رم محدود است و رابط کاربری و مجموعه ویژگی‌های برنامه بسیار ساده‌تر است. می‌توانید بودجه‌ی محدودتری را برای ویژگی‌های 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 memory.current charge لینوکس نگاشت می‌شوند (که حافظه مشترک مانند Zygote را شامل نمی‌شود).

ویژگی قالب پیش‌فرض توضیحات
android:maxMb عدد صحیح (> 0) مورد نیاز محدودیت بودجه حافظه ساکن پایه بر حسب مگابایت.
android:state شمارشی هر حالت فرآیندی که این بودجه برای آن اعمال می‌شود: foreground ، perceptible یا background .
android:additionalMbPerDensity عدد صحیح (≥ 0) 0 مگابایت‌های اضافی برای اضافه کردن به ازای هر واحد نسبت تراکم نمایشگر نسبت به mdpi (1.0x).
android:additionalBytesPerDisplayPixel عدد صحیح (≥ 0) 0 بایت‌های اضافی اختصاص داده شده به ازای هر پیکسل نمایش فیزیکی (عرض × ارتفاع)، که برای بافرهای سطحی و بیت‌مپ‌ها مفید است.
android:feature رشته هر این بند را به دستگاه‌هایی که ویژگی‌های سخت‌افزاری خاصی را اعلام می‌کنند محدود می‌کند: watch ، automotive یا leanback .

APIهای زمان اجرا (گزینه پویای ثانویه)

اعلام بودجه به صورت ایستا در AndroidManifest.xml راه حل ترجیحی برای تقریباً همه برنامه‌ها است. با این حال، برای برنامه‌هایی با حجم کار پویا یا برای آزمایش‌های زمان اجرا، اندروید APIهای زمان اجرا SDK و NDK را به عنوان یک گزینه ثانویه ارائه می‌دهد.

رابط برنامه‌نویسی کاربردی (API) زمان اجرا به شما امکان می‌دهد:

  • میزان مصرف فعلی حافظه و بودجه‌های مؤثر را جستجو کنید.
  • بودجه فرآیند خود را به صورت پویا رو به پایین تنظیم کنید.
  • به رویدادهای مربوط به بودجه‌ی بیش از حد توجه کنید تا قبل از اینکه سیستم عامل مستقیماً عملیات بازیابی را انجام دهد، حافظه‌های پنهان را به طور پیشگیرانه حذف کنید.

رابط برنامه‌نویسی کاتلین ( MemoryBudgetManager )

سرویس سیستمی MemoryBudgetManager از اندروید ۱۷ 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()

به تماس‌های برگشتی ناشی از فشار بودجه‌ی اضافی گوش دهید

برنامه‌ها می‌توانند یک شنونده (listener) ثبت کنند تا در صورت تجاوز استفاده از حافظه از آستانه بودجه، به آنها اطلاع داده شود. این به برنامه اجازه می‌دهد تا قبل از اینکه سیستم عامل تأخیر بازیابی مستقیم را فعال کند، پاکسازی فعال در سطح برنامه (مانند پاک کردن حافظه‌های پنهان بیت‌مپ در حافظه) را انجام دهد:

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)

بهترین شیوه‌ها برای تماس‌های برگشتی با بودجه‌ی بیش از حد:

  • سریع باشید : عملیات احیا باید تسکین فوری ارائه دهد. محاسبات پیچیده در طول فشار، عملکرد را بدتر می‌کند.
  • از تخصیص‌ها اجتناب کنید : اشیاء جدید را تخصیص ندهید یا نخ‌های جدید را درون تابع فراخوانی شروع نکنید، زیرا انجام این کار می‌تواند باعث بازپس‌گیری مستقیم سیستم عامل شود.
  • روی اهداف با بازدهی بالا تمرکز کنید : حذف بیت‌مپ‌های بزرگ، بافرهای رندر یا بستن فایل‌های نگاشت‌شده در حافظه بسیار مؤثرتر از انتشار تعداد زیادی شیء کوچک است.

رابط برنامه‌نویسی کاربردی بومی NDK ( <android/memory_budget_manager.h> )

برنامه‌های بومی می‌توانند از API مربوط به C NDK که توسط 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. ناظر سطح بالا ( AMemoryBudgetManager_Watcher_create ) : رویدادها را در یک ALooper با قابلیت حذف خودکار مانع (debouncing) رصد می‌کند.
  2. توصیفگر فایل سطح پایین : 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);

مثال‌های API زمان اجرا

مثال‌های زیر نحوه پیاده‌سازی APIهای زمان اجرا را در کاتلین و سی‌پلاس‌پلاس نشان می‌دهند.

مثال کاتلین: ویرایشگر تصویر تطبیقی

این مثال یک برنامه ویرایش تصویر ( 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;
};