本页面介绍如何主动减少应用的内存用量。如需了解 Android 操作系统如何管理内存,请参阅内存管理概览。
随机存取存储器 (RAM) 在任何软件开发环境中都是一项宝贵资源;在移动操作系统中,物理内存通常有限,因此 RAM 更为宝贵。虽然 Android 运行时 (ART) 和 Dalvik 虚拟机都会定期执行垃圾回收任务,但这并不意味着您可以忽略应用分配和释放内存的位置和时间。您仍然需要避免引入内存泄漏问题(通常因在静态成员变量中保留对象引用而引起),并在适当时间(如生命周期回调所定义)释放所有 Reference 对象。
减少应用的代码和资源占用空间
代码中的某些资源和库可能会在您不知情的情况下消耗内存。应用的总体大小(包括第三方库或嵌入式资源)可能会影响应用的内存消耗量。您可以通过从代码中移除冗余、不必要或臃肿的组件、资源和库,降低应用的内存用量。
通过启用 R8 缩减应用总大小
编译后的应用代码是运行时内存占用空间中的活跃部分。每个类、方法、库依赖项和字符串常量在运行时都必须加载到 RAM 中。编译后的代码库越大,应用所需的物理 RAM 就越多。
您可以使用 R8 来减少应用的内存占用空间。虽然 R8 传统上以缩减 APK 大小而闻名,但它对运行时内存 (RAM) 有直接的积极影响。R8 会分析应用的字节码,以剔除无用代码、合并冗余类、内联方法和缩减标识符。通过减少从 APK 加载到 RAM 中的已编译字节码,它可减少应用的总体基准内存占用。此外,将类、方法和字段名称缩小为更短的标识符可直接减少 RAM 开销。类合并和广泛的方法内嵌等优化措施还可以取代代价高昂的运行时查找和分配模式,从而优化堆内存和栈内存。
了解保留规则
保留规则是一种配置指令,用于告知 R8 在优化期间保留代码的哪些部分,以防止 R8 移除或缩小应用所依赖的代码。如需了解详情,请参阅保留规则概览。
如果保留规则编写不当,R8 将无法优化代码库中的大部分代码。避免使用过于宽泛的保留规则,并遵循以下最佳实践:
- 应避免的全局规则:
-dontoptimize:完全停用整个应用的优化,导致可执行文件更大、速度更慢。-dontshrink:防止移除未使用的代码和资源。-dontobfuscate:防止名称缩减,从而错失宝贵的内存节省机会(尤其是在大型应用中)。
避免使用软件包级通配符:
-keep class com.example.package.** { *; }等宽泛的规则会强制 R8 保留相应软件包中的每个类、字段和方法。这会完全阻止 R8 移除、优化或缩小相应软件包中的代码。使用默认 R8 配置文件:始终使用
proguard-android-optimize.txt。
如需详细了解如何编写保留规则,请参阅保留规则概览。 如需了解应使用和避免使用的具体模式,请参阅Keep 规则最佳实践。
R8 配置分析器可深入了解您的 R8 配置以及每条保留规则对应用的影响。如需详细了解如何识别阻止优化的规则,请参阅 R8 配置分析器。
谨慎使用外部库
外部库代码通常不是针对移动环境编写的,在移动客户端上运行时效率可能并不高。使用外部库时,您可能需要针对移动设备优化相应库。在使用外部库前,请提前规划,并在代码大小和 RAM 用量方面对库进行分析。
即使是一些针对移动设备进行了优化的库,也可能因实现方式不同而导致问题。例如,一个库可能使用的是精简版 protobuf,而另一个库使用的是 micro protobuf,这会导致您的应用出现两种不同的 protobuf 实现。日志记录、分析、图像加载框架和缓存以及许多您意料之外的其他功能的不同实现都可能导致这种情况。
虽然使用 R8 优化应用可以从依赖项中移除未使用的代码,但其效果通常会受到库的内部配置的限制。例如,宽泛的保留规则或在库中使用反射可能会阻止 R8 缩减其代码,从而导致内存占用空间更大。如需了解有关选择高效库的策略,请参阅明智地选择库。
请避免仅针对数十个功能中的一两个功能使用共享库。不要引入大量您不使用的代码和开销。在考虑是否使用某个库时,请查找与您的需求十分契合的实现。否则,您可能需要自行创建实现。
使用 Hilt 或 Dagger 2 实现依赖项注入
依赖项注入框架可以简化您编写的代码,并提供一个可供您进行测试及其他配置更改的自适应环境。
如果您打算在应用中使用依赖项注入框架,请考虑使用 Hilt 或 Dagger。Hilt 是 Android 的依赖项注入库,可在 Dagger 上运行。Dagger 不使用反射来扫描应用的代码。您可以在 Android 应用中使用 Dagger 的静态编译时实现,而不会带来不必要的运行时开销或内存用量。
其他使用反射的依赖项注入框架会通过扫描代码中的注释来初始化进程。此过程可能需要更多的 CPU 周期和 RAM,并可能在应用启动时导致明显的延迟。
使用依赖项注入时,请务必确保对象的作用域设置得当,以免发生内存泄漏。如果将对象绑定到错误的生命周期,导致对象保留时间过长,可能会导致内存泄漏。如需了解详情,请参阅有关使用作用域对象避免内存泄漏的指南。
有意识地加载图片
图形位图通常是应用内存中最大的常见对象。即使您处理的是 JPEG 等压缩文件,该文件也必须解压缩为未压缩的位图才能显示在屏幕上。一个小的压缩图片文件可以扩展为非常大的位图。
例如,大多数位图使用 ARGB_8888 配置,这意味着每个像素需要 4 字节的内存,分别用于红色、绿色、蓝色和 Alpha(透明度)。如果您有一个 100KB 的 JPEG,并将其显示在 1000×1000 像素的视图中,则位图将需要为每个像素分配 4 字节,总共需要 4MB 的内存。
您可以采取多种措施来优化图片使用。例如,使用图片加载库有助于在不需要时释放内存。如需了解如何高效处理图片,请参阅优化位图图片。
监控可用内存和内存用量
您必须先找到应用中的内存使用问题,然后才能修复问题。Android Studio 内存分析器可以通过以下方式帮助您查找和诊断内存问题:
- 了解您的应用在一段时间内是如何分配内存的。内存分析器可以显示实时图表,说明应用的内存用量、分配的 Java 对象数量以及垃圾回收事件发生的时间。
- 发起垃圾回收事件,并在应用运行时拍摄 Java 堆的快照。
- 记录应用的内存分配情况,检查所有分配的对象、查看每项分配的堆栈轨迹,并在 Android Studio 编辑器中跳转到相应代码。
内存分析器还与 LeakCanary 内存泄漏检测库集成。通过使用 LeakCanary,您可以将内存泄漏分析从测试设备转移到开发机器,从而显著加快工作流程。如需了解详情,请参阅 Android Studio 版本说明。
您还可以使用其他工具来诊断内存问题,这些工具基于运行生产版应用的用户提供的数据:
- 使用 Android Vitals 跟踪低内存终止 (LMK) 事件。
- 使用分析管理器跟踪内存不足错误,以及可能由内存泄漏引起的异常应用行为。
释放内存以响应事件
Android 可以从应用中回收内存,或在必要时完全终止应用,从而释放内存以执行关键任务,如内存管理概览中所述。为了进一步帮助平衡系统内存,避免系统需要终止您的应用进程,您可以在 Activity 类中实现 ComponentCallbacks2 接口。所提供的 onTrimMemory() 回调方法会向您的应用通知生命周期事件或内存相关事件,这些事件为您的应用主动减少内存使用量提供了绝佳机会。释放内存可以减少应用被低内存终止守护程序终止的频率。
onTrimMemory() 的实现应仅关注 TRIM_MEMORY_UI_HIDDEN 和 TRIM_MEMORY_BACKGROUND 事件。(从 Android 14 开始,系统不再针对其他旧版常量传送通知。这些常量已在 Android 15 中正式废弃。)
TRIM_MEMORY_UI_HIDDEN:此信号表示应用的界面已从用户的视图中移出。此过渡提供了一个机会,可以释放严格与界面相关的大量内存分配,例如位图、视频播放缓冲区或复杂的动画资源。TRIM_MEMORY_BACKGROUND:此信号表示您的进程驻留在后台,现在可以终止该进程以满足系统的全局内存需求。为了延长进程保持在缓存状态的时间,并减少应用冷启动的次数,您应积极释放用户恢复会话后可以轻松重建的任何资源。
此代码示例展示了如何实现 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;
}
监控低内存终止
当系统内存严重不足时,会发生用户可见的低内存终止 (LMK)。当内存不足时,lmkd(低内存终止守护程序)会根据进程的 oom_adj_score 终止进程。缓存的应用或运行没有关联界面的服务(例如作业)的应用具有最高得分,因此会率先被终止。如果内存仍然严重不足,守护程序会被迫从 oom_adj_score 为 0 的进程回收内存。由于该得分是为可见应用预留的,因此终止这些应用会导致进程立即以非正常方式退出。对于最终用户而言,这看起来就像应用崩溃了,通常会绕过标准生命周期状态保存机制,导致用户进度丢失。
Android Vitals 主要关注前台进程终止,因为它们是内存管理不当的高保真代理。虽然任何高于 1% 的 LMK 率都表明需要立即采取措施,但较低的 LMK 率并不一定表示健康状况良好。较低的用户感知的 LMK 触发率可能意味着 LMK 守护程序在进程处于后台时会频繁终止进程,这会降低“温启动”性能和多任务处理流畅度。因此,无论您当前的 LMK 分数是多少,我们都建议您遵循内存最佳实践,以确保长期稳定性和设备健康状况。
使用 ProfilingManager 跟踪内存问题
Android 平台提供 ProfilingManager,这是一种高级可观测性 API,可让您根据设置的触发器捕获生产环境中的用户数据。这样做有助于您发现难以重现的内存问题。
Android 17 中引入的两个新触发器对于发现内存问题特别有用:
TRIGGER_TYPE_OOM表示应用已抛出OutOfMemoryError。它会在应用崩溃后下次启动时触发,届时应用会注册分析触发器。- 当系统检测到应用存在异常行为时,
TRIGGER_TYPE_ANOMALY会触发。除其他情况外,内存用量过量也会触发此事件。它会在应用表现出过高的内存使用量之后,在系统采取任何措施来停止违规进程之前触发。例如,如果应用超出 Android 17 中引入的内存限制,则在系统终止应用之前,会触发TRIGGER_TYPE_ANOMALY。
如需详细了解如何使用 ProfilingManager 以编程方式注册和检索触发器,请参阅基于触发器的分析文档。
您还可以使用应用驱动的分析来手动定义开始和结束跟踪点。建议您执行此操作,以便在您怀疑可能存在内存泄漏或内存用量过多的区域手动捕获堆转储或堆配置文件。
使用内存效率更高的代码结构
某些 Android 功能、Java 类和代码结构所使用的内存多于其他功能、类和结构。您可以在代码中选择效率更高的替代方案,以尽可能降低应用的内存用量。
谨慎使用服务
我们强烈建议切勿在不需要服务时让其保持运行状态。 在不需要某项服务时让其保持运行状态,是 Android 应用最严重的内存管理错误之一。如果您的应用需要某项服务在后台工作,除了需要运行作业时,请不要让其保持运行状态。记得在服务完成任务后使其停止运行。否则,您可能会导致内存泄漏。
在您启动某项服务后,系统更倾向于让此服务的进程保持运行状态。这种行为会导致服务进程占用大量内存,因为一旦服务使用了某部分 RAM,那么这部分 RAM 就不再可供其他进程使用。这会减少系统可以在 LRU 缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的所有服务时,这甚至可能导致系统出现抖动。
通常,您应该避免使用持久性服务,因为它们会持续请求使用可用内存。我们建议您采用 WorkManager 等替代实现方式。如需详细了解如何使用 WorkManager 调度后台进程,请参阅持久性工作。
使用经过优化的数据容器
编程语言所提供的部分类并未针对移动设备做出优化。例如,常规 HashMap 实现的内存效率可能较低,因为对于每个映射,都需要有一个单独的条目对象。
Android 框架包含几个经过优化的数据容器,包括 SparseArray、SparseBooleanArray 和 LongSparseArray。例如,SparseArray 类的效率更高,因为在使用这些类时,系统不需要对键(有时还对值)进行自动装箱,这会为每个条目分别再创建 1-2 个对象。
如果需要,您可以随时切换到原始数组以获得精简的数据结构。
谨慎对待代码抽象
开发者往往会将抽象简单地当作一种良好的编程做法,因为抽象可以提高代码灵活性,且更便于维护。不过,抽象通常需要执行更多代码。如缩减应用的代码和资源占用空间中所述,已编译的代码库越大,应用所需的物理 RAM 就越多。如果抽象不能带来显著的好处,应尽量少用。
针对序列化数据使用精简版 Protobuf
协议缓冲区 (protobuf) 是 Google 设计的一种无关语言和平台且可扩展的机制,用于对结构化数据进行序列化。该机制与 XML 类似,但更小、更快也更简单。如果您要针对数据使用 protobuf,请始终在客户端代码中使用精简版 protobuf。常规 protobuf 会生成极其冗长的代码,这会增加应用在 RAM 中的代码占用空间(请参阅管理和优化应用的代码占用空间),并导致 APK 大小增加。
如需了解详情,请参阅 protobuf 自述文件。
谨慎处理内存泄漏问题
不当的引用管理可能会导致内存泄漏,即对象的使用寿命超过其有用寿命,从而阻止垃圾回收器回收泄漏对象的内存。为避免内存泄漏,请实现生命周期感知型设计。
如需了解详情,请参阅内存泄漏。
避免内存抖动
垃圾回收事件不会影响应用的性能。但是,由于垃圾回收器和应用线程之间需要进行交互,如果在短时间内发生许多垃圾回收事件,就可能会快速消耗电量,而设置帧所用的时间也会略微增加。系统花在垃圾回收上的时间越多,耗电越快。
通常,“内存抖动”可能会导致出现大量的垃圾回收事件。实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量。
例如,您可以在 for 循环中分配多个临时对象。
或者,您也可以在视图的 onDraw() 函数中创建新的 Paint 或 Bitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些操作可以快速消耗“新生代”区域中的所有可用内存,从而迫使垃圾回收事件发生。
使用内存分析器在代码中找到内存抖动问题较严重的位置,然后才能进行修复。
确定代码中的问题区域后,请尝试减少对性能至关重要的区域中的分配数量。您可以考虑将某些代码逻辑从内部循环中移出,或将其移到基于工厂的分配结构中。
您还可以评估对象池对用例是否有益。借助对象池,您可以在不再需要某个对象实例时将其释放到池中,而不是将其丢弃。下次需要此类对象实例时,您可以从对象池中获取,而无需进行分配。
全面评估性能,以确定某个对象池是否适合指定场景。在某些情况下,对象池可能会导致性能下降。虽然对象池可以避免分配,但它们会产生其他开销。例如,维护对象池通常涉及到同步,这会产生较大的开销。此外,在释放期间清除放入池中的对象实例(以免内存泄漏),然后在获取期间对其进行的初始化可能会产生一定的开销。
在对象池中保留的对象实例数量超出所需也会给垃圾回收带来负担。尽管对象池可以减少垃圾回收调用次数,但最终会增加每次调用时所需完成的工作量,因为它与活跃(可访问的)字节数成比例。