Menetapkan anggaran memori aplikasi

Anggaran memori aplikasi memungkinkan aplikasi mendeklarasikan anggaran memori untuk dirinya sendiri, yang memberi tahu sistem untuk memangkas penggunaan memori aplikasi saat aplikasi menggunakan lebih banyak dari anggaran yang ditetapkan. Hal ini sangat berguna untuk aplikasi sistem dan aplikasi bawaan atau untuk aplikasi yang menargetkan perangkat dengan batasan memori, yang mana developer mengetahui set kerja memori yang diharapkan dan ingin memastikan aplikasi mereka tidak menggunakan terlalu banyak resource RAM bersama sistem.

Anggaran tetap seimbang dengan menggunakan pengusiran dan pertukaran memori untuk menghapus halaman memori yang belum digunakan baru-baru ini, sehingga fokus jejak memori aplikasi pada set kerjanya saat ini. Jika aplikasi melampaui anggaran yang dideklarasikan, sistem operasi akan menargetkan pengambilan kembali secara khusus pada aplikasi tersebut:

  1. Halaman yang didukung file bersih (seperti kode tidak aktif dan aset yang dipetakan) akan dikeluarkan terlebih dahulu karena dapat dibaca ulang dari penyimpanan jika diperlukan.
  2. Halaman yang didukung file yang kotor ditulis kembali ke penyimpanan dan dikeluarkan.
  3. Halaman memori anonim (seperti alokasi heap) dikompresi dan ditukar ke zRAM.

Selama anggaran tidak melebihi set kerja, aplikasi akan berfungsi dengan baik dan tidak menggunakan lebih banyak memori daripada yang ada dalam anggaran yang ditetapkan. Sistem operasi mengeluarkan memori yang tidak digunakan dan memadatkan halaman heap yang tidak aktif ke swap sehingga alokasi memori tetap terbatas tanpa menghentikan proses.

Mendeklarasikan anggaran dalam manifes Android

Mendeklarasikan anggaran memori di AndroidManifest.xml adalah metode utama dan yang direkomendasikan untuk menentukan anggaran. Fitur ini tidak memerlukan kode runtime, langsung berlaku saat proses dimulai, dan memberikan kontrak yang jelas untuk sistem operasi.

Deklarasi <memory-budget> berlaku di perangkat yang menjalankan Android 17 QPR2 (level API 37.2) dan yang lebih tinggi. Pada versi Android yang lebih rendah, parser manifest platform dengan aman mengabaikan elemen XML yang tidak dikenali, sehingga Anda dapat mengadopsi <memory-budget> tanpa memengaruhi kompatibilitas mundur.

Mendeklarasikan anggaran dasar

Untuk sebagian besar aplikasi, yang diperlukan hanyalah menentukan satu anggaran untuk aplikasi. Deklarasikan elemen <memory-budget> langsung di dalam tag <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>

Tindakan ini menetapkan anggaran memori residen 256 MB di semua proses dan status untuk paket. Jika jejak memori aplikasi melebihi 256 MB, sistem operasi akan memangkas halaman memori yang tidak aktif menggunakan penghapusan dan penukaran.

Memvariasikan anggaran menurut status proses

Aplikasi memerlukan jumlah memori yang berbeda, bergantung pada visibilitas penggunanya:

  • Latar depan: Proses menghosting Aktivitas yang terlihat dan berinteraksi dengan pengguna. Status ini biasanya memiliki jejak terbesar karena UI dan grafis yang aktif.
  • Terasa: Proses terasa oleh pengguna, tetapi tidak menghosting jendela yang terlihat (misalnya, menghosting layanan latar depan pemutaran media, navigasi belokan demi belokan, atau metode input aktif).
  • Latar belakang: Proses menjalankan tugas latar belakang, penerima, atau sinkronisasi data. Diharapkan memiliki jejak yang minimal.

Anda dapat mendeklarasikan beberapa klausa <memory-budget> untuk mencocokkan status ini:

<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>

Klausa dasar tanpa android:state tidak diperlukan; jika Anda hanya menentukan klausa khusus negara bagian (misalnya, android:state="background"), negara bagian lain tetap tidak dibatasi oleh anggaran aplikasi. Jika disertakan, klausa tanpa android:state akan bertindak sebagai penggantian default untuk status yang tidak ditentukan (seperti latar depan), yang kemudian digantikan oleh klausa yang lebih ketat saat aplikasi bertransisi ke status perceptible atau background.

Aplikasi multi-proses

Jika aplikasi Anda membagi tugasnya di beberapa proses, konfigurasikan anggaran proses khusus menggunakan tag <process> di dalam <processes>.

Misalnya, pertimbangkan aplikasi streaming musik (com.example.radio):

  1. Proses utama: Menghosting UI yang terlihat dan mesin pemutaran audio (MediaSessionService dengan layanan latar depan mediaPlayback). Jika terlihat, proses beroperasi dalam anggaran latar depan 180 MB. Saat pengguna keluar dari aplikasi saat musik terus diputar, proses akan memasuki status perceptible, dengan anggaran 64 MB yang cukup untuk mesin pemutaran dan buffer audio.
  2. Proses sinkronisasi (:sync): Proses khusus yang menjalankan sinkronisasi metadata di latar belakang dan pengindeksan download. Karena proses ini hanya aktif di latar belakang, Anda tidak perlu mendeklarasikan state="background" secara eksplisit; satu anggaran berlaku.
<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>

Penggunaan memori dalam subproses apa pun dihitung terhadap anggaran proses dan anggaran paket yang melampirkannya. Proses mengalami tekanan memori saat runtime jika melanggar anggaran proses atau anggaran paket, mana pun batas yang tercapai lebih dulu.

Menskalakan anggaran untuk layar kepadatan tinggi

Untuk aplikasi yang jejak memorinya diskalakan secara signifikan dengan jumlah piksel yang harus digambar di layar sekaligus—seperti aplikasi galeri foto yang menyimpan bitmap dalam cache yang ukurannya disesuaikan dengan layar—Android menyediakan dua mekanisme alternatif untuk menskalakan anggaran secara dinamis dengan spesifikasi layar:

  • Menskalakan menurut bucket kepadatan tampilan (android:additionalMbPerDensity): Menambahkan megabyte secara proporsional ke rasio kepadatan tampilan relatif terhadap mdpi (1,0x / 160 dpi). Hal ini cocok saat penggunaan memori diskalakan dengan bucket kepadatan UI, seperti menyimpan dalam cache aset UI atau gambar vektor beresolusi lebih tinggi:

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

    Pada layar mdpi (1,0x), anggaran adalah 180 + 16 × 1 = 196 MB. Pada tampilan xxhdpi (3,0x), anggaran diskalakan menjadi 180 + 16 × 3 = 228 MB.

  • Skala menurut resolusi tampilan fisik (android:additionalBytesPerDisplayPixel): Menambahkan byte secara langsung per piksel tampilan fisik (Lebar × Tinggi). Hal ini ideal untuk aplikasi yang mengalokasikan platform grafis layar penuh, buffer render, atau cache foto beresolusi penuh yang konsumsi memorinya diskalakan secara langsung dengan jumlah piksel layar mentah, bukan bucket kepadatan UI:

    <!-- 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" />
    

    Pada layar 1080p (1080 × 2400 &approx; 2,59 M piksel), hal ini menambah &approx; 41,4 MB ke anggaran dasar. Pada layar 1440p (1440 × 3120 &approx; 4,49 juta piksel), aplikasi ini menambahkan &approx; 71,8 MB.

Kedua atribut ini adalah alternatif. Pilih atribut yang cocok dengan faktor penskalaan utama aplikasi Anda, dan hindari penggabungan keduanya dalam klausa yang sama.

Mengkhususkan diri untuk faktor bentuk perangkat

Saat mengirimkan APK di seluruh ponsel, tablet, dan Wear OS, gunakan atribut android:feature untuk menyesuaikan anggaran untuk target hardware yang berbeda.

Di smartwatch Wear OS, RAM terbatas, dan UI serta set fitur aplikasi jauh lebih sederhana. Anda dapat menyatakan anggaran yang lebih ketat yang dikhususkan untuk fitur 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" />

Aturan penyelesaian: klausa terakhir yang berlaku akan diterapkan

Saat menentukan beberapa elemen <memory-budget> untuk aplikasi atau proses, sistem akan mengevaluasinya sesuai urutan deklarasinya dalam manifest. Klausul anggaran terakhir yang berlaku adalah klausul yang diterapkan.

Karena anggaran terakhir yang berlaku akan menang, urutan menjadi penting. Selalu tempatkan anggaran dasar yang paling umum terlebih dahulu, diikuti dengan penggantian yang lebih spesifik (seperti klausul khusus negara bagian atau khusus hardware).

Referensi atribut XML

Semua atribut ukuran memori dinyatakan dalam Megabyte (MB) dan dipetakan ke biaya memory.current cgroup Linux (yang tidak termasuk memori bersama seperti Zygote).

Atribut Format Default Deskripsi
android:maxMb Bilangan bulat (> 0) Wajib Batas anggaran memori residen dasar dalam MB.
android:state Enum Semua Status proses yang berlaku untuk anggaran ini: foreground, perceptible, atau background. Jika tidak ada, klausa ini akan bertindak sebagai penggantian untuk status yang tidak ditentukan.
android:additionalMbPerDensity Bilangan bulat (≥ 0) 0 Megabita tambahan yang akan ditambahkan per unit rasio kepadatan tampilan relatif terhadap mdpi (1.0x).
android:additionalBytesPerDisplayPixel Bilangan bulat (≥ 0) 0 Byte tambahan yang dialokasikan per piksel layar fisik (Lebar × Tinggi), berguna untuk buffer dan bitmap permukaan.
android:feature String Semua Membatasi klausa ke perangkat yang mendeklarasikan fitur hardware tertentu: watch, automotive, atau leanback.

Runtime API (opsi dinamis sekunder)

Mendeklarasikan anggaran secara statis di AndroidManifest.xml adalah solusi yang disarankan untuk hampir semua aplikasi. Namun, untuk aplikasi dengan workload dinamis atau untuk eksperimen runtime, Android menyediakan API SDK dan NDK runtime sebagai opsi sekunder.

API runtime memungkinkan Anda:

  • Kueri penggunaan memori saat ini dan anggaran efektif.
  • Menyesuaikan anggaran proses Anda secara dinamis ke bawah.
  • Memproses peristiwa melebihi anggaran untuk memangkas cache secara proaktif sebelum sistem operasi memicu reklamasi langsung.

Android SDK API (MemoryBudgetManager)

Layanan sistem MemoryBudgetManager tersedia untuk aplikasi yang ditulis dalam Kotlin dan Java mulai di Android 17 QPR2 (rilis SDK minor, level API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Mengambil layanan

Sebelum Anda mengakses MemoryBudgetManager, periksa apakah perangkat tidak menjalankan versi yang lebih rendah dari Android 17 QPR2 dengan menggunakan SDK_INT_FULL:

if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

Penggunaan kueri dan anggaran

// 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

Menetapkan atau menghapus anggaran secara dinamis

Anda dapat menetapkan anggaran yang lebih ketat saat runtime untuk membatasi memori selama tugas ringan, atau menghapusnya saat tugas selesai:

// 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()

Dengarkan callback tekanan melebihi anggaran

Aplikasi dapat mendaftarkan pemroses untuk mendapatkan notifikasi saat penggunaan memori melebihi batas anggaran. Hal ini memungkinkan aplikasi melakukan pembersihan proaktif tingkat aplikasi (seperti menghapus cache bitmap dalam memori) sebelum sistem operasi memicu latensi perolehan langsung:

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)

Praktik terbaik untuk panggilan balik yang melebihi anggaran:

  • Segera: Operasi pemulihan harus menawarkan bantuan segera. Komputasi yang kompleks selama tekanan memperburuk performa.
  • Hindari alokasi: Jangan mengalokasikan objek baru atau memulai thread baru di dalam callback, karena tindakan ini dapat memicu pengklaiman langsung sistem operasi.
  • Berfokus pada target dengan hasil tinggi: Mengeluarkan Bitmap besar, buffer render, atau menutup file yang dipetakan memori jauh lebih efektif daripada melepaskan banyak objek kecil.

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

Aplikasi native dapat menggunakan C NDK API yang diekspos oleh libandroid.so mulai di Android 17 QPR2 (level API 37.2).

Konfigurasi CMake

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

Menyertakan penggunaan header dan kueri

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

Mengonfigurasi anggaran native secara dinamis

// 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();

Memantau peristiwa tekanan memori

NDK menyediakan dua cara untuk memantau peristiwa memori:

  1. Pengamat Tingkat Tinggi (AMemoryBudgetManager_Watcher_create): Memantau peristiwa pada ALooper dengan penghilangan getaran otomatis.
  2. Deskriptor File Tingkat Rendah: AMemoryBudgetManager_getProcessMemoryPressureFd menampilkan deskriptor file asli yang dapat diintegrasikan langsung ke loop mesin epoll kustom.
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);

Contoh Runtime API

Contoh berikut menunjukkan cara mengimplementasikan API runtime menggunakan Android SDK API (ditulis dalam Kotlin) dan Native NDK API (ditulis dalam C++).

Contoh Android SDK: Editor gambar adaptif

Contoh ini menunjukkan aplikasi pengeditan gambar (com.example.imageeditor) yang manifesnya menyatakan batas 256 MB untuk mengakomodasi kanvas multi-layer-nya:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

Saat pengguna menjelajahi galeri thumbnail ringan, aplikasi menggunakan Android SDK API di Kotlin untuk secara dinamis memperketat anggaran prosesnya menjadi 96 MB. Saat pengguna membuka kanvas pengeditan multi-layer, aplikasi akan menghapus anggaran dinamis untuk memulihkan batas manifes 256 MB penuh. Class ini juga mendaftarkan OnOverBudgetListener untuk mengeluarkan bitmap pratinjau yang di-cache saat ada tekanan.

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"
    }
}

Contoh C++ NDK: Mesin 3D native

Contoh ini menunjukkan mesin game C++ native yang mengelola anggaran memori berdasarkan tingkat kualitas grafis aktif, dengan asumsi manifes aplikasi mendeklarasikan batas 512 MB untuk mengakomodasi grafis Kualitas tinggi (android:maxMb="512"). Mesin secara dinamis memperketat anggaran untuk preset kualitas yang lebih rendah dan menggunakan AMemoryBudgetManager_Watcher_create pada ALooper untuk membatalkan pemuatan mipmap tekstur jika anggaran berlebih.

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