本页面介绍了如何在您的应用程序中主动减少内存使用量。有关 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) { // Determine which lifecycle or system event is raised. when (level) { ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> { /* Release any UI objects that currently hold memory. The user interface moves to the background. */ } ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE, ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> { /* Release any memory your app doesn't need to run. The device is running low on memory while the app is running. The event raised indicates the severity of the memory-related event. If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system begins stopping background processes. */ } ComponentCallbacks2.TRIM_MEMORY_BACKGROUND, ComponentCallbacks2.TRIM_MEMORY_MODERATE, ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> { /* Release as much memory as the process can. The app is on the LRU list and the system is running low on memory. The event raised indicates where the app sits within the LRU list. If the event is TRIM_MEMORY_COMPLETE, the process is one of the first to be terminated. */ } else -> { /* Release any non-critical data structures. The app receives an unrecognized memory level value from the system. Treat this as a generic low-memory message. */ } } } }
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) { // Determine which lifecycle or system event is raised. switch (level) { case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: /* Release any UI objects that currently hold memory. The user interface moves to the background. */ break; case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: /* Release any memory your app doesn't need to run. The device is running low on memory while the app is running. The event raised indicates the severity of the memory-related event. If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system begins stopping background processes. */ break; case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: case ComponentCallbacks2.TRIM_MEMORY_MODERATE: case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: /* Release as much memory as the process can. The app is on the LRU list and the system is running low on memory. The event raised indicates where the app sits within the LRU list. If the event is TRIM_MEMORY_COMPLETE, the process is one of the first to be terminated. */ break; default: /* Release any non-critical data structures. The app receives an unrecognized memory level value from the system. Treat this as a generic low-memory message. */ break; } } }
检查您需要多少内存
为了允许多个正在运行的进程,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 框架包含几个优化后的数据容器,包括SparseArray
、SparseBooleanArray
和LongSparseArray
。例如,SparseArray
类效率更高,因为它们避免了系统需要自动装箱键,有时还有值,这会为每个条目创建另外一到两个对象。
如有必要,您可以始终切换到原始数组以获得精简的数据结构。
小心代码抽象
开发人员通常将抽象用作良好的编程实践,因为它们可以提高代码灵活性和可维护性。但是,抽象的成本要高得多,因为它们通常需要执行更多代码,需要更多时间和 RAM 将代码映射到内存中。如果您的抽象没有显著的益处,请避免使用它们。
使用 lite protobufs 存储序列化数据
Protocol buffers (protobufs)是由 Google 设计的一种语言中立、平台中立、可扩展的机制,用于序列化结构化数据,类似于 XML,但更小、更快且更简单。如果您在数据中使用 protobufs,请始终在客户端代码中使用 lite protobufs。常规 protobufs 会生成极其冗长的代码,这可能会导致您的应用程序出现许多问题,例如 RAM 使用量增加、APK 大小明显增加以及执行速度变慢。
有关更多信息,请参阅protobuf 自述文件。
避免内存抖动
垃圾回收事件不会影响应用程序的性能。但是,在短时间内发生的许多垃圾回收事件会导致电池快速消耗,并且还会略微增加设置帧所需的时间,因为垃圾回收器和应用程序线程之间需要进行必要的交互。系统在垃圾回收上花费的时间越多,电池消耗得越快。
通常,内存抖动会导致大量垃圾回收事件发生。实际上,内存抖动描述的是在给定时间内分配的临时对象的数量。
例如,您可能在一个for
循环内分配多个临时对象。或者,您可能会在视图的onDraw()
函数内部创建新的Paint
或Bitmap
对象。在这两种情况下,应用程序都会以高容量快速创建大量对象。这些对象会很快消耗年轻代中所有可用的内存,迫使垃圾回收事件发生。
使用内存分析器查找代码中内存抖动高的位置,以便您能够修复它们。
在您确定代码中的问题区域后,尝试减少性能关键区域内的分配次数。考虑将某些内容移出内部循环,或者可能将其移入基于工厂的分配结构。
您还可以评估对象池是否对用例有益。使用对象池,您可以将对象实例释放到池中,而不是将其丢弃。下次需要该类型的对象实例时,您可以从池中获取它,而不是分配它。
彻底评估性能以确定对象池在给定情况下是否适用。在某些情况下,对象池可能会降低性能。即使池避免了分配,它们也会引入其他开销。例如,维护池通常会涉及同步,这具有不可忽略的开销。此外,在释放期间清除池对象实例以避免内存泄漏,然后在获取期间初始化它也会产生非零开销。
在池中保留比需要更多的对象实例也会给垃圾回收带来负担。虽然对象池减少了垃圾回收调用的次数,但它们最终会增加每次调用所需的工作量,因为它与活动(可达)字节数成正比。
删除占用内存的资源和库
代码中的某些资源和库可能会在您不知情的情况下消耗内存。应用程序的总大小,包括第三方库或嵌入式资源,会影响应用程序消耗的内存量。您可以通过从代码中删除冗余、不必要或臃肿的组件或资源和库来改善应用程序的内存消耗。
减少 APK 的总大小
您可以通过减少应用程序的总大小来显著减少应用程序的内存使用量。位图大小、资源、动画帧和第三方库都会影响应用程序的大小。Android Studio 和 Android SDK 提供了多种工具来帮助减少资源和外部依赖项的大小。这些工具支持现代的代码缩减方法,例如R8 编译。
有关减少应用程序总大小的更多信息,请参阅减小应用程序大小。
使用 Hilt 或 Dagger 2 进行依赖注入
依赖注入框架可以简化您编写的代码,并提供一个适应性环境,这对于测试和其他配置更改很有用。
如果您打算在应用程序中使用依赖注入框架,请考虑使用Hilt或Dagger。Hilt 是一个基于 Dagger 的 Android 依赖注入库。Dagger 不使用反射来扫描应用程序的代码。您可以在 Android 应用程序中使用 Dagger 的静态编译时实现,而无需不必要的运行时成本或内存使用量。
其他使用反射的依赖注入框架通过扫描代码以查找注释来初始化进程。此过程可能需要显著更多的 CPU 周期和 RAM,并且在应用程序启动时会导致明显的延迟。
小心使用外部库
外部库代码通常不是针对移动环境编写的,对于在移动客户端上运行可能效率低下。当您使用外部库时,您可能需要针对移动设备优化该库。提前计划这项工作,并在使用库之前根据代码大小和 RAM 占用量分析该库。
即使是一些针对移动设备优化的库也会因实现方式不同而导致问题。例如,一个库可能使用 lite protobufs,而另一个库可能使用 micro protobufs,从而导致您的应用程序中出现两种不同的 protobuf 实现。这可能发生在日志记录、分析、图像加载框架、缓存以及您无法预料的许多其他方面的不同实现中。
虽然ProGuard可以通过正确的标志来帮助删除 API 和资源,但它无法删除库的大型内部依赖项。您希望在这些库中使用的功能可能需要更低级别的依赖项。当您使用库中的Activity
子类时,这会变得特别成问题,因为该子类可能具有广泛的依赖项,而库使用反射(这很常见,并且需要手动调整 ProGuard 使其正常工作)。
避免为几十个功能中的仅仅一两个功能使用共享库。不要引入大量你不会用到的代码和开销。当你考虑是否使用库时,寻找一个与你的需求高度匹配的实现。否则,你可能需要考虑自己创建实现。