Đặt ngân sách bộ nhớ ứng dụng

Ngân sách bộ nhớ ứng dụng cho phép các ứng dụng tự khai báo ngân sách bộ nhớ, nhờ đó, hệ thống sẽ biết để giảm mức sử dụng bộ nhớ của ứng dụng khi ứng dụng sử dụng nhiều hơn ngân sách đã đặt. Điều này đặc biệt hữu ích đối với các ứng dụng hệ thống và ứng dụng đi kèm hoặc đối với các ứng dụng nhắm đến các thiết bị có bộ nhớ hạn chế, trong đó nhà phát triển biết tập hợp hoạt động dự kiến của bộ nhớ và muốn đảm bảo ứng dụng của họ không sử dụng quá nhiều tài nguyên RAM dùng chung của hệ thống.

Ngân sách được cân bằng bằng cách sử dụng giải phóng bộ nhớ và hoán đổi để xoá các trang bộ nhớ không được dùng gần đây, tập trung dấu vết bộ nhớ của ứng dụng vào nhóm hoạt động hiện tại. Khi một ứng dụng vượt quá ngân sách đã khai báo, hệ điều hành sẽ nhắm đến việc thu hồi cụ thể tại ứng dụng đó:

  1. Các trang được sao lưu bằng tệp sạch (chẳng hạn như mã không hoạt động và các thành phần được liên kết) sẽ bị loại bỏ trước vì có thể đọc lại từ bộ nhớ nếu cần.
  2. Các trang được sao lưu bằng tệp có sửa đổi sẽ được ghi lại vào bộ nhớ và bị loại bỏ.
  3. Các trang bộ nhớ ẩn danh (chẳng hạn như việc phân bổ vùng nhớ đệm) được nén và hoán đổi sang zRAM.

Miễn là ngân sách không vượt quá tập hợp đang hoạt động, ứng dụng sẽ hoạt động tốt trong khi không sử dụng bộ nhớ nhiều hơn ngân sách đã đặt. Hệ điều hành sẽ loại bỏ bộ nhớ không dùng đến và nén các trang heap không hoạt động để hoán đổi, nhờ đó, việc phân bổ bộ nhớ vẫn bị giới hạn mà không làm chấm dứt quy trình.

Khai báo ngân sách trong tệp kê khai Android

Khai báo ngân sách bộ nhớ trong AndroidManifest.xml là phương thức chính và được đề xuất để xác định ngân sách. Thao tác này không yêu cầu mã thời gian chạy, có hiệu lực ngay lập tức khi khởi động quy trình và cung cấp một hợp đồng rõ ràng cho hệ điều hành.

Các khai báo <memory-budget> có hiệu lực trên các thiết bị chạy Android 17 QPR2 (API cấp 37.2) trở lên. Trên các phiên bản Android thấp hơn, trình phân tích cú pháp tệp kê khai nền tảng sẽ bỏ qua một cách an toàn các phần tử XML không được nhận dạng, vì vậy bạn có thể áp dụng <memory-budget> mà không ảnh hưởng đến khả năng tương thích ngược.

Khai báo ngân sách cơ sở

Đối với hầu hết các ứng dụng, bạn chỉ cần xác định một ngân sách duy nhất cho ứng dụng. Khai báo một phần tử <memory-budget> ngay bên trong thẻ <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>

Thao tác này đặt ngân sách bộ nhớ thường trú là 256 MB trên tất cả các quy trình và trạng thái cho gói. Khi mức sử dụng bộ nhớ của ứng dụng vượt quá 256 MB, hệ điều hành sẽ cắt bớt các trang bộ nhớ không hoạt động bằng cách sử dụng tính năng loại bỏ và hoán đổi.

Thay đổi ngân sách theo trạng thái xử lý

Một ứng dụng yêu cầu lượng bộ nhớ khác nhau tuỳ thuộc vào khả năng hiển thị của người dùng:

  • Nền trước: Quy trình đang lưu trữ một Hoạt động hiển thị tương tác với người dùng. Trạng thái này thường có mức sử dụng bộ nhớ lớn nhất do giao diện người dùng và đồ hoạ đang hoạt động.
  • Dễ nhận biết: Người dùng có thể nhận biết quy trình này nhưng không lưu trữ một cửa sổ hiển thị (ví dụ: lưu trữ một dịch vụ phát nội dung nghe nhìn ở nền trước, chỉ đường từng chặng hoặc một phương thức nhập đang hoạt động).
  • Nền: Quy trình đang chạy các tác vụ, bộ nhận hoặc quá trình đồng bộ hoá dữ liệu ở chế độ nền. Dự kiến sẽ duy trì mức sử dụng bộ nhớ tối thiểu.

Bạn có thể khai báo nhiều mệnh đề <memory-budget> để so khớp các trạng thái này:

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

Bạn không cần chỉ định android:state="foreground" trên mệnh đề đầu tiên. Một mệnh đề không có android:state sẽ đóng vai trò là phương án dự phòng mặc định cho tất cả các trạng thái. Các mệnh đề hạn chế hơn bên dưới sẽ ghi đè ngân sách khi ứng dụng chuyển sang trạng thái perceptible hoặc background.

Ứng dụng đa tiến trình

Nếu ứng dụng của bạn phân chia công việc trên nhiều quy trình, hãy định cấu hình ngân sách quy trình chuyên dụng bằng cách sử dụng thẻ <process> bên trong <processes>.

Ví dụ: xem xét một ứng dụng phát nhạc trực tuyến (com.example.radio):

  1. Quy trình chính: Lưu trữ giao diện người dùng hiển thị và công cụ phát âm thanh (MediaSessionService có dịch vụ nền trước mediaPlayback). Khi hiển thị, quy trình này hoạt động trong ngân sách nền trước 180 MB. Khi người dùng rời khỏi ứng dụng trong lúc nhạc vẫn tiếp tục phát, quy trình sẽ chuyển sang trạng thái perceptible, trong đó ngân sách 64 MB là đủ cho công cụ phát và bộ đệm âm thanh.
  2. Quy trình đồng bộ hoá (:sync): Quy trình chuyên dụng chạy tính năng đồng bộ hoá siêu dữ liệu ở chế độ nền và lập chỉ mục nội dung tải xuống. Vì quy trình này chỉ hoạt động ở chế độ nền, nên bạn không cần khai báo rõ ràng state="background"; chỉ áp dụng một ngân sách.
<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>

Mức sử dụng bộ nhớ trong mọi quy trình con đều được tính vào cả ngân sách quy trình và ngân sách gói bao quanh. Một quy trình gặp phải tình trạng thiếu bộ nhớ trong thời gian chạy nếu quy trình đó vi phạm ngân sách quy trình hoặc ngân sách gói, tuỳ theo ngưỡng nào đạt được trước.

Chia tỷ lệ ngân sách cho màn hình có mật độ điểm ảnh cao

Đối với những ứng dụng có mức sử dụng bộ nhớ tăng đáng kể theo số lượng pixel cần vẽ trên màn hình cùng một lúc (chẳng hạn như ứng dụng thư viện ảnh lưu vào bộ nhớ đệm các bitmap có kích thước phù hợp với màn hình), Android cung cấp 2 cơ chế thay thế để điều chỉnh ngân sách một cách linh hoạt theo thông số kỹ thuật của màn hình:

  • Điều chỉnh tỷ lệ theo nhóm mật độ hiển thị (android:additionalMbPerDensity): Thêm megabyte tương ứng với tỷ lệ mật độ của màn hình so với mdpi (1,0x / 160 dpi). Điều này phù hợp khi mức sử dụng bộ nhớ tăng theo các nhóm mật độ giao diện người dùng, chẳng hạn như lưu vào bộ nhớ đệm các thành phần có thể vẽ raster độ phân giải cao hoặc thành phần giao diện người dùng:

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

    Trên màn hình mdpi (1.0x), ngân sách là 180 + 16 × 1 = 196 MB. Trên màn hình xxhdpi (3,0x), ngân sách sẽ tăng lên 180 + 16 × 3 = 228 MB.

  • Điều chỉnh tỷ lệ theo độ phân giải màn hình thực (android:additionalBytesPerDisplayPixel): Thêm trực tiếp các byte cho mỗi pixel trên màn hình thực (Chiều rộng × Chiều cao). Đây là lựa chọn lý tưởng cho các ứng dụng phân bổ vùng đồ hoạ toàn màn hình, vùng đệm kết xuất hoặc bộ nhớ đệm ảnh có độ phân giải đầy đủ, trong đó mức tiêu thụ bộ nhớ tăng tỷ lệ thuận với số lượng pixel hiển thị thô thay vì các nhóm mật độ giao diện người dùng:

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

    Trên màn hình 1080p (1080 × 2400 &approx; 2,59 triệu pixel), điều này sẽ thêm &approx; 41,4 MB vào ngân sách cơ bản. Trên màn hình 1440p (1440 × 3120 &approx; 4,49 triệu pixel), kích thước này sẽ tăng thêm &approx; 71,8 MB.

Hai thuộc tính này là các thuộc tính thay thế. Chọn thuộc tính phù hợp với hệ số tỷ lệ chính của ứng dụng và tránh kết hợp cả hai trong cùng một mệnh đề.

Chuyên biệt hoá cho các hệ số hình dạng thiết bị

Khi vận chuyển một APK trên điện thoại, máy tính bảng và Wear OS, hãy sử dụng thuộc tính android:feature để điều chỉnh ngân sách cho các mục tiêu phần cứng khác nhau.

Trên đồng hồ Wear OS, RAM bị hạn chế và giao diện người dùng cũng như bộ tính năng của ứng dụng đơn giản hơn nhiều. Bạn có thể khai báo một ngân sách chặt chẽ hơn dành riêng cho tính năng 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" />

Quy tắc giải quyết: điều khoản áp dụng gần đây nhất sẽ có hiệu lực

Khi xác định nhiều phần tử <memory-budget> cho một ứng dụng hoặc quy trình, hệ thống sẽ đánh giá các phần tử đó theo thứ tự được khai báo trong tệp kê khai. Mệnh đề ngân sách áp dụng gần đây nhất là mệnh đề được thực thi.

Vì ngân sách áp dụng sau cùng sẽ được ưu tiên, nên thứ tự là yếu tố quan trọng. Luôn đặt ngân sách cơ sở chung nhất trước, sau đó là các chế độ ghi đè cụ thể hơn (chẳng hạn như các điều khoản dành riêng cho tiểu bang hoặc phần cứng).

Tài liệu tham khảo về thuộc tính XML

Tất cả các thuộc tính kích thước bộ nhớ đều được biểu thị bằng Megabyte (MB) và ánh xạ đến mức phí memory.current cgroup của Linux (không bao gồm bộ nhớ dùng chung như Zygote).

Thuộc tính Định dạng Mặc định Mô tả
android:maxMb Số nguyên (> 0) Bắt buộc Giới hạn ngân sách bộ nhớ thường trú cơ sở tính bằng MB.
android:state Enum Khán giả có Trạng thái quy trình mà ngân sách này áp dụng: foreground, perceptible hoặc background.
android:additionalMbPerDensity Số nguyên (≥ 0) 0 Số megabyte bổ sung cần thêm cho mỗi đơn vị tỷ lệ mật độ hiển thị so với mdpi (1,0x).
android:additionalBytesPerDisplayPixel Số nguyên (≥ 0) 0 Số byte bổ sung được phân bổ cho mỗi pixel trên màn hình thực (Chiều rộng × Chiều cao), hữu ích cho các vùng đệm và bitmap trên bề mặt.
android:feature Chuỗi Khán giả có Hạn chế điều khoản đối với các thiết bị khai báo các tính năng phần cứng cụ thể: watch, automotive hoặc leanback.

Runtime API (lựa chọn động thứ cấp)

Giải pháp ưu tiên cho hầu hết mọi ứng dụng là khai báo ngân sách một cách tĩnh trong AndroidManifest.xml. Tuy nhiên, đối với các ứng dụng có khối lượng công việc động hoặc để thử nghiệm trong thời gian chạy, Android cung cấp SDK thời gian chạy và API NDK dưới dạng lựa chọn thứ hai.

API thời gian chạy cho phép bạn:

  • Truy vấn mức sử dụng bộ nhớ hiện tại và ngân sách hiệu quả.
  • Điều chỉnh ngân sách quy trình theo hướng giảm.
  • Theo dõi các sự kiện vượt quá ngân sách để chủ động cắt bớt bộ nhớ đệm trước khi hệ điều hành kích hoạt tính năng thu hồi trực tiếp.

API Kotlin (MemoryBudgetManager)

Dịch vụ hệ thống MemoryBudgetManager có sẵn kể từ Android 17 QPR2 (bản phát hành SDK phụ, cấp độ API 37.2/Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Truy xuất dịch vụ

Trước khi truy cập vào MemoryBudgetManager, hãy kiểm tra để đảm bảo thiết bị không chạy phiên bản thấp hơn Android 17 QPR2 bằng cách sử dụng SDK_INT_FULL:

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

Mức sử dụng cụm từ tìm kiếm và ngân sách

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

Đặt hoặc xoá ngân sách một cách linh hoạt

Bạn có thể đặt ngân sách chặt chẽ hơn trong thời gian chạy để hạn chế bộ nhớ trong các tác vụ đơn giản hoặc xoá ngân sách khi tác vụ hoàn tất:

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

Theo dõi các lệnh gọi lại áp lực vượt quá ngân sách

Các ứng dụng có thể đăng ký một trình nghe để nhận thông báo khi mức sử dụng bộ nhớ vượt quá ngưỡng ngân sách. Điều này cho phép ứng dụng thực hiện quy trình dọn dẹp chủ động ở cấp ứng dụng (chẳng hạn như xoá bộ nhớ đệm bitmap trong bộ nhớ) trước khi hệ điều hành kích hoạt độ trễ thu hồi trực tiếp:

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)

Các phương pháp hay nhất cho lệnh gọi lại khi vượt quá ngân sách:

  • Thực hiện nhanh chóng: Các hoạt động khôi phục phải mang lại hiệu quả ngay lập tức. Các phép tính phức tạp trong quá trình chịu áp lực sẽ làm giảm hiệu suất.
  • Tránh phân bổ: Đừng phân bổ các đối tượng mới hoặc bắt đầu các luồng mới bên trong lệnh gọi lại, vì việc này có thể kích hoạt tính năng thu hồi trực tiếp ngay lập tức của hệ điều hành.
  • Tập trung vào các mục tiêu có mức sinh lợi cao: Việc loại bỏ các Bitmap lớn, kết xuất vùng đệm hoặc đóng các tệp được ánh xạ trong bộ nhớ sẽ hiệu quả hơn nhiều so với việc phát hành nhiều đối tượng nhỏ.

API NDK gốc (<android/memory_budget_manager.h>)

Các ứng dụng gốc có thể sử dụng C NDK API do libandroid.so cung cấp kể từ Android 17 QPR2 (cấp độ API 37.2).

Cấu hình CMake

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

Bao gồm cả tiêu đề và mức sử dụng truy vấn

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

Định cấu hình ngân sách gốc một cách linh hoạt

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

Theo dõi các sự kiện áp lực bộ nhớ

NDK cung cấp 2 cách để theo dõi các sự kiện liên quan đến bộ nhớ:

  1. High-Level Watcher (AMemoryBudgetManager_Watcher_create): Giám sát các sự kiện trên ALooper bằng tính năng loại bỏ trùng lặp tự động.
  2. Chỉ số mô tả tệp cấp thấp: AMemoryBudgetManager_getProcessMemoryPressureFd trả về một chỉ số mô tả tệp gốc có thể được tích hợp trực tiếp vào một vòng lặp công cụ epoll tuỳ chỉnh.
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);

Ví dụ về Runtime API

Các ví dụ sau đây minh hoạ cách triển khai các API thời gian chạy trong Kotlin và C++.

Ví dụ về Kotlin: Trình chỉnh sửa hình ảnh thích ứng

Ví dụ này cho thấy một ứng dụng chỉnh sửa hình ảnh (com.example.imageeditor) tự động tăng hạn mức bộ nhớ khi người dùng mở một canvas chỉnh sửa nhiều lớp và xoá hạn mức động khi quay lại chế độ xem thư viện hình thu nhỏ. Thao tác này cũng đăng ký một OnOverBudgetListener để loại bỏ các bitmap xem trước được lưu vào bộ nhớ đệm khi cần.

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

Ví dụ về C++ trong NDK: Công cụ 3D gốc

Ví dụ này cho thấy một công cụ trò chơi C++ gốc quản lý ngân sách bộ nhớ dựa trên cấp chất lượng đồ hoạ đang hoạt động, sử dụng AMemoryBudgetManager_Watcher_create trên ALooper để huỷ tải mipmap kết cấu khi vượt quá ngân sách.

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