تتيح ميزانيات ذاكرة التطبيق للتطبيقات تحديد ميزانية ذاكرة خاصة بها، ما يطلب من النظام تقليل استخدام الذاكرة عندما يستخدم التطبيق أكثر من الميزانية المحدّدة. ويكون ذلك مفيدًا بشكل خاص للتطبيقات المضمّنة أو تطبيقات النظام أو التطبيقات التي تستهدف الأجهزة ذات الذاكرة المحدودة، حيث يعرف المطوّر مجموعة العمل المتوقّعة للذاكرة ويريد التأكّد من أنّ تطبيقه لا يستخدم الكثير من موارد ذاكرة الوصول العشوائي المشتركة في النظام.
يتم الحفاظ على توازن الميزانية من خلال استخدام إخلاء الذاكرة والتبديل لإزالة صفحات الذاكرة التي لم يتم استخدامها مؤخرًا، مع التركيز على مساحة الذاكرة التي يشغلها التطبيق في مجموعة العمل الحالية. عندما يتجاوز أحد التطبيقات ميزانيته المحدّدة، يستهدف نظام التشغيل استرداد الموارد من هذا التطبيق تحديدًا:
- يتم أولاً إخلاء الصفحات المستندة إلى الملفات (مثل الرموز غير النشطة والأصول التي تم ربطها) لأنّه يمكن إعادة قراءتها من مساحة التخزين عند الحاجة.
- تتم إعادة كتابة الصفحات غير النظيفة التي يتم الاحتفاظ بنسخ احتياطية منها في الملفات إلى مساحة التخزين وإزالتها.
- يتم ضغط صفحات الذاكرة المجهولة (مثل عمليات تخصيص الذاكرة المؤقتة) وتبديلها إلى 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>
يؤدي ذلك إلى ضبط ميزانية ذاكرة مقيمة تبلغ 256 ميغابايت على مستوى جميع العمليات والحالات للحزمة. عندما يتجاوز استهلاك الذاكرة للتطبيق 256 ميغابايت، يقلّص نظام التشغيل صفحات الذاكرة غير النشطة باستخدام الإخلاء والتبديل.
اختلاف الميزانيات حسب حالة العملية
يحتاج التطبيق إلى مقادير مختلفة من الذاكرة حسب مستوى ظهوره للمستخدمين:
- التطبيق في المقدّمة: تستضيف العملية نشاطًا مرئيًا يتفاعل مع المستخدم. عادةً ما تكون هذه الحالة هي الأكبر حجمًا بسبب واجهة المستخدم النشطة والرسومات.
- يمكن للمستخدم ملاحظتها: يمكن للمستخدم ملاحظة العملية ولكنّها لا تستضيف نافذة مرئية (على سبيل المثال، استضافة خدمة تعمل في المقدّمة لتشغيل الوسائط أو اتجاهات مفصّلة أو طريقة إدخال نشطة).
- الخلفية: تعمل العملية على تشغيل مهام أو أدوات استقبال أو عمليات مزامنة بيانات في الخلفية. ومن المتوقّع أن تحافظ على الحدّ الأدنى من التأثير البيئي.
يمكنك تعريف عدّة عبارات <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):
- العملية الرئيسية: تستضيف واجهة المستخدم المرئية ومحرّك تشغيل الصوت
(
MediaSessionServiceمعmediaPlaybackخدمة تعمل في المقدّمة). وعندما تكون مرئية، تعمل العملية ضمن ميزانية المقدّمة البالغة 180 ميغابايت. عندما يغادر المستخدم التطبيق أثناء استمرار تشغيل الموسيقى، تنتقل العملية إلى الحالةperceptible، حيث تكون ميزانية 64 ميغابايت كافية لمشغّل الموسيقى ومخزن الصوت المؤقت. - عملية المزامنة (
: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 نقطة لكل بوصة). يكون هذا الخيار مناسبًا عندما يتناسب استخدام الذاكرة مع حِزم كثافة واجهة المستخدم، مثل تخزين الرسومات النقطية ذات الدقة العالية أو مواد عرض واجهة المستخدم مؤقتًا:<!-- 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.59 مليون بكسل)، يضيف ذلك حوالي 41.4 ميغابايت إلى الميزانية الأساسية. على شاشة بدقة 1440p (1440 × 3120 بكسل، أي حوالي 4.49 مليون بكسل)، يضيف حوالي 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) في Linux memory.current (التي تستبعد الذاكرة المشتركة، مثل 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. |
واجهات برمجة التطبيقات في وقت التشغيل (الخيار الديناميكي الثانوي)
يُعدّ تحديد الميزانيات بشكل ثابت في AndroidManifest.xml هو الحلّ المفضّل
لجميع التطبيقات تقريبًا. ومع ذلك، بالنسبة إلى التطبيقات التي تتضمّن أحمال عمل ديناميكية أو للتجربة أثناء وقت التشغيل، يوفّر نظام التشغيل Android واجهات برمجة تطبيقات لحزمة تطوير البرامج (SDK) ووحدة التطوير الأصلية (NDK) كخيار ثانوي.
تتيح لك واجهة برمجة التطبيقات لوقت التشغيل إجراء ما يلي:
- طلب البحث عن استخدام الذاكرة الحالي والميزانيات الفعّالة
- عدِّل ميزانية العملية بشكلٍ ديناميكي إلى قيمة أقل.
- استمع إلى أحداث تجاوز الميزانية لتقليل حجم الذاكرة المؤقتة بشكل استباقي قبل أن يفعّل نظام التشغيل عملية استرداد الذاكرة المباشر.
واجهة برمجة التطبيقات Kotlin (MemoryBudgetManager)
تتوفّر خدمة تابعة لنظام التشغيل MemoryBudgetManager بدءًا من
Android 17 QPR2 (إصدار حزمة تطوير البرامج (SDK) لنظام Android 26Q4، مستوى واجهة برمجة التطبيقات 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()
الاستماع إلى عمليات معاودة الاتصال التي تشير إلى تجاوز الميزانية
يمكن للتطبيقات تسجيل أداة معالجة ليتم إعلامها عندما يتجاوز استخدام الذاكرة الحدّ الأقصى المسموح به. يتيح ذلك للتطبيق إجراء تنظيف استباقي على مستوى التطبيق (مثل محو ذاكرات التخزين المؤقت للصور النقطية في الذاكرة) قبل أن يؤدي نظام التشغيل إلى بدء عملية استرداد الذاكرة المباشر:
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>)
يمكن للتطبيقات الأصلية استخدام واجهة برمجة تطبيقات 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 طريقتَين لتتبُّع أحداث الذاكرة:
- 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);
أمثلة على واجهة برمجة التطبيقات في وقت التشغيل
توضّح الأمثلة التالية كيفية تنفيذ واجهات برمجة التطبيقات في وقت التشغيل باستخدام لغتَي 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"
}
}
مثال على C++ في NDK: محرّك ثلاثي الأبعاد أصلي
يوضّح هذا المثال محرّك ألعاب مخصّصًا للغة 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;
};