管理应用的内存

此页面说明如何主动减少应用中的内存使用。有关 Android 操作系统如何管理内存的信息,请参阅 内存管理概述

随机存取存储器 (RAM) 是任何软件开发环境中宝贵的资源,对于移动操作系统来说,它更是如此,因为移动操作系统的物理内存通常受到限制。尽管 Android 运行时 (ART) 和 Dalvik 虚拟机都执行例行的垃圾回收,但这并不意味着您可以忽略应用分配和释放内存的时间和地点。您仍然需要避免引入内存泄漏(通常是由在静态成员变量中保留对象引用引起的),并在生命周期回调定义的适当时间释放任何 Reference 对象。

监控可用内存和内存使用情况

您必须先找到应用的内存使用问题,才能修复它们。Android Studio 中的 内存分析器 可以帮助您通过以下方式查找和诊断内存问题

  • 查看应用如何随时间推移分配内存。内存分析器会显示应用使用的内存量、已分配 Java 对象的数量以及垃圾回收发生时间的实时图表。
  • 在应用运行时启动垃圾回收事件并拍摄 Java 堆快照。
  • 记录应用的内存分配,检查所有已分配的对象,查看每个分配的堆栈跟踪,并跳转到 Android Studio 编辑器中的相应代码。

响应事件释放内存

如需释放内存以供关键任务使用,Android 可能会回收应用的内存或完全停止应用,如内存管理概述中所述。为了进一步平衡系统内存并避免系统需要停止应用进程,您可以在 Activity 类中实现 ComponentCallbacks2 接口。提供的 onTrimMemory() 回调方法会通知您的应用有关生命周期或内存的相关事件,这些事件为您的应用提供了自愿减少内存使用量的良好机会。释放内存可以降低应用被低内存杀手终止的可能性。

您可以实现 onTrimMemory() 回调以响应不同的内存相关事件,如下例所示。

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

检查您需要的内存量

为了允许多个进程同时运行,Android 对每个应用分配的堆大小设置了硬性限制。确切的堆大小限制因设备而异,具体取决于设备可用的总 RAM 量。如果您的应用达到堆容量并尝试分配更多内存,系统将抛出 OutOfMemoryError 错误。

为了避免内存不足,您可以查询系统以确定当前设备上可用的堆空间量。您可以通过调用 getMemoryInfo() 来查询系统以获取此数据。这将返回一个 ActivityManager.MemoryInfo 对象,该对象提供有关设备当前内存状态的信息,包括可用内存、总内存和内存阈值(系统开始停止进程的内存级别)。ActivityManager.MemoryInfo 对象还公开了 lowMemory,这是一个简单的布尔值,用于告诉您设备是否内存不足。

以下示例代码片段显示了如何在应用中使用 getMemoryInfo() 方法。

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

使用更高效的代码结构

某些 Android 功能、Java 类和代码结构比其他结构使用更多的内存。您可以通过在代码中选择更有效的替代方案来最大程度地减少应用使用的内存量。

谨慎使用服务

我们强烈建议您在不需要时不要让服务保持运行。让不必要的服务保持运行是 Android 应用可能犯的最糟糕的内存管理错误之一。如果您的应用需要服务在后台工作,则除非它需要运行作业,否则不要让它保持运行。服务完成其任务后停止它。否则,您可能会导致内存泄漏。

启动服务时,系统更倾向于保持该服务的进程运行。这种行为使得服务进程非常昂贵,因为服务使用的 RAM 对于其他进程来说仍然不可用。这减少了系统可以在 LRU 缓存中保留的缓存进程的数量,从而使应用切换效率降低。当内存紧张且系统无法维护足够的进程来承载当前正在运行的所有服务时,它甚至可能导致系统出现抖动。

通常,由于持久服务对可用内存的持续需求,因此避免使用它们。相反,我们建议您使用替代实现,例如 WorkManager。有关如何使用 WorkManager 调度后台进程的更多信息,请参阅持久性工作

使用优化的数据容器

编程语言提供的一些类未针对移动设备进行优化。例如,通用 HashMap 实现可能效率低下,因为它需要为每个映射创建一个单独的条目对象。

Android 框架包含几个优化的数据容器,包括 SparseArraySparseBooleanArrayLongSparseArray。例如,SparseArray 类效率更高,因为它们避免了系统需要自动装箱键和有时值,这会为每个条目再创建一到两个对象。

如有必要,您可以始终切换到原始数组以获得精简的数据结构。

谨慎使用代码抽象

开发人员通常将抽象用作良好的编程实践,因为它们可以提高代码的灵活性和可维护性。但是,抽象的成本要高得多,因为它们通常需要更多需要执行的代码,需要更多时间和 RAM 将代码映射到内存中。如果您的抽象没有显着的好处,请避免使用它们。

对序列化数据使用 lite protobufs

Protocol buffers (protobufs) 是一种由 Google 设计的语言中立、平台中立、可扩展的机制,用于序列化结构化数据——类似于 XML,但更小、更快且更简单。如果对数据使用 protobufs,请始终在客户端代码中使用 lite protobufs。常规 protobufs 会生成极其冗长的代码,这可能会导致应用出现许多问题,例如 RAM 使用量增加、APK 大小显著增加以及执行速度变慢。

有关更多信息,请参阅protobuf 自述文件

避免内存抖动

垃圾回收事件不会影响应用的性能。但是,在短时间内发生的许多垃圾回收事件可能会迅速耗尽电池电量,并可能略微增加设置帧所需的时间,因为垃圾回收器和应用线程之间需要进行必要的交互。系统在垃圾回收上花费的时间越多,电池电量消耗得就越快。

通常,内存抖动会导致大量垃圾回收事件发生。在实践中,内存抖动描述了在给定时间内分配的临时对象的数量。

例如,您可能会在 for 循环中分配多个临时对象。或者,您可能会在视图的 onDraw() 函数内创建新的 PaintBitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些对象可能会很快消耗掉年轻代中的所有可用内存,从而迫使垃圾回收事件发生。

使用内存分析器查找代码中内存抖动较高的位置,然后才能修复它们。

在确定代码中的问题区域后,尝试减少性能关键区域内的分配次数。考虑将内容移出内部循环,或者可能将其移入基于工厂的分配结构。

您还可以评估对象池是否对用例有益。使用对象池,在不再需要对象实例时,不会将其丢弃,而是将其释放到池中。下次需要该类型的对象实例时,可以从池中获取它,而不是分配它。

彻底评估性能以确定对象池在给定情况下是否合适。在某些情况下,对象池可能会降低性能。即使池避免了分配,它们也会引入其他开销。例如,维护池通常涉及同步,这具有不可忽略的开销。此外,清除池化对象实例以避免在释放期间发生内存泄漏,然后在获取期间对其进行初始化,也会产生非零开销。

在池中保留比需要更多的对象实例也会给垃圾回收带来负担。虽然对象池减少了垃圾回收调用的次数,但它们最终会增加每次调用所需的工作量,因为这与活动(可到达)字节的数量成正比。

删除内存密集型资源和库

代码中的某些资源和库可能会在您不知情的情况下消耗内存。应用的整体大小(包括第三方库或嵌入式资源)会影响应用消耗的内存量。您可以通过删除冗余、不必要或臃肿的组件或代码中的资源和库来改善应用的内存消耗。

减少 APK 的整体大小

通过减少应用的整体大小,您可以显著减少应用的内存使用量。位图大小、资源、动画帧和第三方库都会影响应用的大小。Android Studio 和 Android SDK 提供多种工具来帮助减小资源和外部依赖项的大小。这些工具支持现代代码压缩方法,例如R8 编译

有关减少应用整体大小的更多信息,请参阅减小应用大小

使用 Hilt 或 Dagger 2 进行依赖项注入

依赖项注入框架可以简化您编写的代码,并提供一个自适应环境,该环境可用于测试和其他配置更改。

如果打算在应用中使用依赖项注入框架,请考虑使用HiltDagger。Hilt 是一个基于 Dagger 的 Android 依赖项注入库。Dagger 不使用反射来扫描应用的代码。您可以在 Android 应用中使用 Dagger 的静态编译时实现,而无需不必要的运行时成本或内存使用量。

其他使用反射的依赖项注入框架通过扫描代码中的注释来初始化进程。此过程可能需要更多 CPU 周期和 RAM,并且在应用启动时可能会导致明显的延迟。

谨慎使用外部库

外部库代码通常不是为移动环境编写的,并且在移动客户端上运行效率可能不高。使用外部库时,可能需要为移动设备优化该库。提前计划此工作,并在使用库之前根据代码大小和 RAM 占用量分析库。

即使是一些针对移动设备优化的库也可能由于不同的实现而导致问题。例如,一个库可能使用 lite protobufs,而另一个库使用 micro protobufs,从而导致应用中存在两种不同的 protobuf 实现。这可能会发生在日志记录、分析、图像加载框架、缓存以及许多您意想不到的其他方面的不同实现中。

虽然ProGuard可以通过正确的标志帮助删除 API 和资源,但它无法删除库的大型内部依赖项。您在这些库中需要的功能可能需要较低级别的依赖项。当您使用库中的 Activity 子类(可能具有广泛的依赖项)时,尤其会出现此问题——当库使用反射(很常见)时,需要手动调整 ProGuard 才能使其工作。

避免为几十个功能中的一个或两个功能使用共享库。不要引入大量您不使用的代码和开销。在考虑是否使用库时,请寻找与您需求高度匹配的实现。否则,您可能决定创建自己的实现。