设置应用内存预算

应用内存预算允许应用自行声明内存预算,这样当应用使用的内存超出其设置的预算时,系统就会减少其内存用量。这对于系统应用和捆绑应用或以内存受限设备为目标的应用尤其有用,因为开发者知道其应用的预期内存工作集,并希望确保其应用不会使用过多的系统共享 RAM 资源。

通过使用内存逐出和交换来移除最近未使用的内存页,从而使预算保持平衡,并将应用的内存占用空间集中在其当前工作集上。当应用超出其声明的预算时,操作系统会专门针对该应用回收以下目标:

  1. 有文件支持的干净内存页(例如不活跃的代码和映射的资源)会先被逐出,因为系统可以在需要时从存储空间重新读取这些内存页。
  2. 脏文件支持的页面会被写回存储空间并逐出。
  3. 匿名内存页(例如堆分配)会被压缩并交换到 zRAM。

只要预算不超过工作集,应用就能正常运行,同时使用的内存不会超过其预算集中的内存。操作系统会逐出未使用的内存并压缩非活跃堆页面以进行交换,从而使内存分配保持在有限范围内,而不会终止进程。

在 Android 清单中声明预算

AndroidManifest.xml 中声明内存预算是定义预算的主要方法,也是推荐的方法。它不需要运行时代码,在进程启动时立即生效,并为操作系统提供清晰的合约。

声明基准预算

对于大多数应用,只需为应用定义一个预算即可。在 <application> 标记内直接声明 <memory-budget> 元素:

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

此标志会为相应软件包的所有进程和状态设置 256MB 的常驻内存预算。当应用的内存占用量超过 256MB 时,操作系统会使用逐出和交换来削减非活跃内存页。

按流程状态调整预算

应用所需的内存量因其用户可见性而异:

  • 前台:进程正在托管与用户互动的可见 Activity。由于界面和图形处于活跃状态,因此此状态通常具有最大的内存占用空间。
  • 可感知:进程可被用户感知,但不托管可见窗口(例如,托管媒体播放前台服务、精细导航或有效输入法)。
  • 后台:进程正在运行后台作业、接收器或数据同步。预计可保持极小的占用空间。

您可以声明多个 <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 的子句充当所有状态的默认回退。当应用转换到 perceptiblebackground 状态时,下面的限制性更强的子句会覆盖预算。

多进程应用

如果您的应用将其工作分配到多个进程中,请使用 <processes> 内的 <process> 标记配置专用进程预算。

例如,假设有一个在线播放音乐应用 (com.example.radio):

  1. 主进程:托管可见的界面和音频播放引擎(MediaSessionService,带有 mediaPlayback 前台服务)。当进程可见时,它会在 180MB 的前台预算下运行。当用户在音乐继续播放时离开应用时,进程会进入 perceptible 状态,此时 64MB 的预算足以支持播放引擎和音频缓冲区。
  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 提供了两种替代机制,可根据显示屏规格动态调整预算:

  • 按显示密度分桶 (android:additionalMbPerDensity) 进行缩放:根据显示屏相对于 mdpi(1.0 倍 / 160 dpi)的密度比,按比例增加内存(以 MB 为单位)。如果内存使用量随界面密度桶而变化,例如缓存更高分辨率的栅格可绘制对象或界面资源,则适合使用此配置:

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

    mdpi 显示屏 (1.0x) 上,预算为 180 + 16 × 1 = 196 MB。在 xxhdpi 显示屏 (3.0x) 上,预算会扩展到 180 + 16 × 3 = 228 MB。

  • 按物理显示分辨率缩放 (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 &approx; 259 万像素)上,这会使基准预算增加 &approx; 41.4 MB。在 1440p 显示屏(1440 × 3120,约 449 万像素)上,它会增加约 71.8 MB。

这两个属性是互斥的。选择与应用的主要缩放比例相匹配的属性,并避免在同一子句中同时使用这两个属性。

针对设备规格进行专门化设计

在手机、平板电脑和 Wear OS 设备上发布 APK 时,请使用 android:feature 属性来调整不同硬件目标的预算。

在 Wear OS 手表上,RAM 受到限制,应用界面和功能集也简单得多。您可以声明一个专门针对 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) 为单位表示,并映射到 Linux cgroup memory.current 费用(不包括 Zygote 等共享内存)。

属性 格式 默认 说明
android:maxMb 整数(大于 0) 必需 以 MB 为单位的基准常驻内存预算限制。
android:state 枚举 不限 相应预算适用的流程状态:foregroundperceptiblebackground
android:additionalMbPerDensity 整数(≥ 0) 0 相对于 mdpi (1.0x) 的每个显示密度比率单位要添加的额外兆字节数。
android:additionalBytesPerDisplayPixel 整数(≥ 0) 0 为每个物理显示像素(宽度 × 高度)分配的额外字节数,适用于 Surface 缓冲区和位图。
android:feature 字符串 不限 将子句限制为声明特定硬件功能的设备:watchautomotiveleanback

运行时 API(次要动态选项)

AndroidManifest.xml 中静态声明预算是几乎所有应用的首选解决方案。不过,对于具有动态工作负载的应用或运行时实验,Android 提供运行时 SDK 和 NDK API 作为次要选项。

借助运行时 API,您可以:

  • 查询当前内存用量和有效预算。
  • 动态调低流程预算。
  • 监听超出预算的事件,以便在操作系统触发直接回收之前主动清理缓存。

Kotlin API (MemoryBudgetManager)

MemoryBudgetManager 系统服务从 Android 17 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()

监听超预算压力回调

应用可以注册监听器,以便在内存用量超出预算阈值时收到通知。这样一来,应用就可以在操作系统触发直接回收延迟之前执行主动应用级清理(例如清除内存中的位图缓存):

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 API (<android/memory_budget_manager.h>)

原生应用可以使用 libandroid.so 公开的 C NDK API。

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 上的事件,并自动去抖动。
  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);

Runtime API 示例

以下示例演示了如何在 Kotlin 和 C++ 中实现运行时 API。

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

NDK C++ 示例:原生 3D 引擎

此示例展示了一个原生 C++ 游戏引擎如何根据有效图形质量级别管理内存预算,并在超出预算时使用 ALooper 上的 AMemoryBudgetManager_Watcher_create 来卸载纹理 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;
};