管理应用内存

本页面介绍如何积极减少应用的内存用量。如需了解 Android 操作系统如何管理内存,请参阅内存管理概览

随机存取存储器 (RAM) 在任何软件开发环境中都是一项宝贵资源;在移动操作系统中,物理内存通常有限,因此 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,那么这部分 RAM 就不再可供其他进程使用。这会减少系统可以在 LRU 缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的所有服务时,这甚至可能导致系统出现抖动。

通常,您应该避免使用持久性服务,因为它们会持续请求使用可用内存。我们建议您采用 WorkManager 等替代实现方式。如需详细了解如何使用 WorkManager 调度后台进程,请参阅持久性工作

使用经过优化的数据容器

编程语言所提供的部分类并未针对移动设备做出优化。例如,常规 HashMap 实现的内存效率可能较低,因为对于每个映射,都需要有一个单独的条目对象。

Android 框架包含几个经过优化的数据容器,包括 SparseArraySparseBooleanArrayLongSparseArray。例如,SparseArray 类的效率更高,因为在使用这些类时,系统不需要对键(有时还对值)进行自动装箱(这会为每个条目分别再创建 1-2 个对象)。

如果需要,您可以随时切换到原始数组以获得精简的数据结构。

谨慎对待代码抽象

开发者往往会将抽象简单地当作一种良好的编程做法,因为抽象可以提高代码灵活性,且更便于维护。不过,抽象的代价很高:通常它们需要执行更多代码,因而需要更多的时间和 RAM 才能将代码映射到内存中。如果抽象不能带来显著的好处,应尽量少用。

针对序列化数据使用精简版 Protobuf

协议缓冲区 (protobuf) 是 Google 设计的一种无关语言和平台且可扩展的机制,用于对结构化数据进行序列化。该机制与 XML 类似,但更小、更快也更简单。如果您要针对数据使用 protobuf,请始终在客户端代码中使用精简版 protobuf。常规 protobuf 会生成极其冗长的代码,这会导致应用出现许多问题,例如 RAM 使用量增多、APK 大小显著增加以及执行速度变慢。

如需了解详情,请参阅 protobuf 自述文件

避免内存抖动

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

通常,“内存抖动”可能会导致出现大量的垃圾回收事件。实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量。

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

使用内存分析器在代码中找到内存抖动问题较严重的位置,然后才能进行修复。

确定代码中的问题区域后,请尝试减少对性能至关重要的区域中的分配数量。您可以考虑将某些代码逻辑从内部循环中移出,或将其移到基于工厂的分配结构中。

您还可以评估对象池对用例是否有益。借助对象池,您可以在不再需要某个对象实例时将其释放到池中,而不是将其丢弃。下次需要此类对象实例时,您可以从对象池中获取,而无需进行分配。

全面评估性能,以确定某个对象池是否适合指定场景。在某些情况下,对象池可能会导致性能下降。虽然对象池可以避免分配,但它们会产生其他开销。例如,维护对象池通常涉及到同步,这会产生较大的开销。此外,在释放期间清除放入池中的对象实例(以免内存泄漏),然后在获取期间对其进行的初始化可能会产生一定的开销。

在对象池中保留的对象实例数量超出所需也会给垃圾回收带来负担。尽管对象池可以减少垃圾回收调用次数,但最终会增加每次调用时所需完成的工作量,因为它与活跃(可访问的)字节数成比例。

移除会占用大量内存的资源和库

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

缩减总体 APK 大小

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

如需详细了解如何缩减应用的总体大小,请参阅缩减应用大小

使用 Hilt 或 Dagger 2 实现依赖项注入

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

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

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

谨慎使用外部库

外部库代码通常不是针对移动环境编写的,在移动客户端上运行时效率可能并不高。使用外部库时,您可能需要针对移动设备优化相应库。在使用外部库前,请提前规划,并在代码大小和 RAM 用量方面对库进行分析。

即使是一些针对移动设备进行了优化的库,也可能因实现方式不同而导致问题。例如,一个库可能使用的是精简版 protobuf,而另一个库使用的是 micro protobuf,这会导致您的应用出现两种不同的 protobuf 实现。日志记录、分析、图像加载框架和缓存以及许多您意料之外的其他功能的不同实现都可能导致这种情况。

虽然 ProGuard 可以使用适当的标志移除 API 和资源,但无法移除库的大型内部依赖项。您需要通过这些库实现的功能可能需要较低级别的依赖项。这会以下情况特别容易出现问题:您使用某个库中的 Activity 子类(这个库往往会有大量的依赖项),而库使用反射(这很常见,并且意味着您需要手动调整 ProGuard 以使其运行)。

请避免仅针对数十个功能中的一两个功能使用共享库。不要引入大量您不使用的代码和开销。在考虑是否使用某个库时,请查找与您的需求十分契合的实现。否则,您可能需要自行创建实现。