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 paket atau untuk aplikasi yang menargetkan perangkat dengan batasan memori, di 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 penghapusan 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 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 sambil menggunakan memori tidak lebih dari 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.

Mendeklarasikan anggaran dasar

Untuk sebagian besar aplikasi, Anda hanya perlu 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.

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

Anda tidak perlu menentukan android:state="foreground" pada klausa pertama. Klausul tanpa android:state berfungsi sebagai penggantian default untuk semua status. Klausul yang lebih ketat di bawahnya menggantikan anggaran saat aplikasi bertransisi ke status perceptible atau background.

Aplikasi multi-proses

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

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

  1. Proses utama: Menampung 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 footprint 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:

  • Skala 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 tampilan mdpi (1,0x), anggarannya adalah 180 + 16 × 1 = 196 MB. Pada layar 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 M 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 menggabungkannya dalam klausa yang sama.

Mengkhususkan 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 mendeklarasikan 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.
android:additionalMbPerDensity Bilangan bulat (≥ 0) 0 Tambahan Megabyte 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.
  • Memantau peristiwa melebihi anggaran untuk memangkas cache secara proaktif sebelum sistem operasi memicu reklamasi langsung.

Kotlin API (MemoryBudgetManager)

Layanan sistem MemoryBudgetManager tersedia mulai dari Android 17 QPR2 (rilis SDK Android 26Q4, level API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Mengambil layanan

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

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 saat anggaran terlampaui:

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

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
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 duplikasi otomatis.
  2. Deskriptor File Tingkat Rendah: AMemoryBudgetManager_getProcessMemoryPressureFd menampilkan deskriptor file asli yang dapat diintegrasikan langsung ke dalam 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 menerapkan API runtime di Kotlin dan C++.

Contoh Kotlin: Editor gambar adaptif

Contoh ini menunjukkan aplikasi pengeditan gambar (com.example.imageeditor) yang menaikkan anggaran memorinya secara dinamis saat pengguna membuka kanvas pengeditan multi-layer, dan menghapus anggaran dinamis saat kembali ke tampilan galeri thumbnail. Selain itu, metode 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)
    }

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

Contoh C++ NDK: Mesin 3D native

Contoh ini menunjukkan mesin game C++ native yang mengelola anggaran memori berdasarkan tingkat kualitas grafis aktif, menggunakan AMemoryBudgetManager_Watcher_create pada ALooper untuk membatalkan pemuatan mipmap tekstur saat melebihi anggaran.

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