本页面介绍了如何主动减少应用中的内存使用量。如需了解 Android 操作系统如何管理内存,请参阅内存管理概览。
随机存取存储器 (RAM) 是任何软件开发环境的宝贵资源,对于物理内存通常受限的移动操作系统而言,它更显宝贵。尽管 Android Runtime (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 框架包含多个优化数据容器,包括 SparseArray
、SparseBooleanArray
和 LongSparseArray
。例如,SparseArray
类效率更高,因为它们避免了系统需要:自动装箱键,有时是值,这会为每个条目额外创建一个或两个对象。
如有必要,您始终可以切换到原始数组以获得精简的数据结构。
谨慎使用代码抽象
开发者通常将抽象作为一种良好的编程实践,因为它们可以提高代码的灵活性和可维护性。然而,抽象的成本要高得多,因为它们通常需要执行更多代码,需要更多的时间和 RAM 将代码映射到内存中。如果您的抽象没有显著的好处,请避免使用它们。
对序列化数据使用精简版 protobuf
Protocol buffers (protobuf) 是 Google 设计的一种与语言无关、与平台无关、可扩展的结构化数据序列化机制,类似于 XML,但更小、更快、更简单。如果您的数据使用 protobuf,请务必在客户端代码中使用精简版 protobuf。常规 protobuf 会生成极其冗长的代码,这可能会导致应用中出现许多问题,例如 RAM 使用量增加、APK 大小显著增加以及执行速度变慢。
如需了解更多信息,请参阅 protobuf readme。
避免内存抖动
垃圾回收事件不会影响您应用的整体性能。然而,短时间内发生的许多垃圾回收事件会迅速耗尽电池电量,并且由于垃圾回收器与应用线程之间的必要交互,还会略微增加帧设置时间。系统花在垃圾回收上的时间越多,电池耗电就越快。
通常,内存抖动会导致大量垃圾回收事件的发生。实际上,内存抖动描述的是在给定时间内分配的临时对象的数量。
例如,您可以在 for
循环中分配多个临时对象。或者,您可以在视图的 onDraw()
函数中创建新的 Paint
或 Bitmap
对象。在这两种情况下,应用都会以高吞吐量快速创建大量对象。这些对象会迅速消耗年轻代中所有可用的内存,从而强制触发垃圾回收事件。
使用 内存分析器 在解决问题之前找出代码中内存抖动高的地方。
识别出代码中的问题区域后,尝试减少性能关键区域内的分配数量。考虑将内容移出内部循环,或者将其移入基于工厂的分配结构。
您还可以评估对象池是否对用例有益。使用对象池,您不再需要某个对象实例时,将其释放到池中,而不是丢弃它。下次需要该类型的对象实例时,您可以从池中获取它,而不是重新分配它。
彻底评估性能以确定对象池在给定情况中是否适用。在某些情况下,对象池可能会使性能变差。尽管池避免了分配,但它们引入了其他开销。例如,维护池通常涉及同步,这会产生不可忽略的开销。此外,为了避免在释放时发生内存泄漏而清除池化对象实例,然后在获取时对其进行初始化,也可能产生非零开销。
在池中保留比所需更多对象实例也会给垃圾回收带来负担。虽然对象池减少了垃圾回收调用的次数,但它们最终增加了每次调用所需的工作量,因为这与活动(可达)字节的数量成正比。
移除内存密集型资源和库
您的代码中的某些资源和库可能会在您不知情的情况下消耗内存。应用的整体大小(包括第三方库或嵌入式资源)会影响您的应用消耗的内存量。您可以通过从代码中移除冗余、不必要或臃肿的组件、资源和库来改善应用的内存消耗。
减小整体 APK 大小
您可以通过减小应用的整体大小来显著减少应用的内存使用量。位图大小、资源、动画帧和第三方库都可能影响您的应用大小。Android Studio 和 Android SDK 提供了多种工具来帮助减小资源和外部依赖项的大小。这些工具支持现代代码压缩方法,例如R8 编译。
有关减小整体应用大小的更多信息,请参阅减小应用大小。
使用 Hilt 或 Dagger 2 进行依赖注入
依赖注入框架可以简化您编写的代码,并提供一个适应性强的环境,这对于测试和其他配置更改非常有用。
如果您打算在应用中使用依赖注入框架,请考虑使用 Hilt 或 Dagger。Hilt 是一个基于 Dagger 运行的 Android 依赖注入库。Dagger 不使用反射扫描您的应用代码。您可以在 Android 应用中使用 Dagger 的静态编译时实现,而无需不必要的运行时开销或内存使用。
其他使用反射的依赖注入框架通过扫描代码中的注解来初始化进程。此过程可能需要显著更多的 CPU 周期和 RAM,并可能导致应用启动时出现明显的延迟。
谨慎使用外部库
外部库代码通常并非为移动环境编写,在移动客户端上工作可能效率低下。当您使用外部库时,您可能需要针对移动设备优化该库。提前规划这项工作,并在使用前分析库的代码大小和 RAM 占用量。
即使某些针对移动设备优化的库也可能由于实现差异而导致问题。例如,一个库可能使用精简版 protobuf,而另一个库使用微型 protobuf,导致您的应用中有两种不同的 protobuf 实现。日志记录、分析、图片加载框架、缓存以及许多您意想不到的其他功能的实现差异也可能导致这种情况。
虽然 ProGuard 可以通过正确的标志帮助移除 API 和资源,但它无法移除库的庞大内部依赖项。您在这些库中想要的功能可能需要较低级别的依赖项。当您使用来自库的 Activity
子类(这可能具有广泛的依赖项)时,这会变得特别有问题,尤其是在库使用反射时,这种情况很常见,需要手动调整 ProGuard 才能使其工作。
避免为了几十个功能中的一两个而使用共享库。不要引入大量您不使用的代码和开销。当您考虑是否使用某个库时,请寻找与您需求高度匹配的实现。否则,您可能决定创建自己的实现。