随机存取存储器(RAM)在任何软件开发环境中都是宝贵的资源,在物理内存受限的移动操作系统上更是如此。尽管Android运行时(ART)和Dalvik虚拟机均执行常规垃圾回收,但这并不意味着您可以忽略掉应用在何时何地分配和释放内存。您仍然需要避免引入内存泄露,这通常是由于在静态成员变量中保留对象引用而引起的,并且应在生命周期回调定义的适当时间释放任何Reference对象。
本页说明如何主动减少应用程序内的内存使用量。有关Android操作系统如何管理内存的信息,参见Android内存管理概述。
监视可用内存和内存使用情况
在解决应用程序中的内存使用问题之前是定位问题。Android Studio的Memory Profiler可以通过以下方式帮助你找到和诊断内存问题:
- 查看您的应用如何随着时间分配内存。Memory Profiler实时显示您的应用程序正在使用多少内存,分配的Java对象的数量以及如何进行垃圾回收。
- 启动垃圾回收事件,并在应用运行时获取Java堆的内存快照。
- 记录应用程序的内存分配,然后检查所有分配的对象,查看每个分配的堆栈跟踪,然后跳转到Android Studio编辑器中的相应代码。
响应事件释放内存
如Android内存管理概述所述,Android可以通过多种方式从您的应用程序中回收内存,或者在必要时完全杀死掉您的应用程序以释放关键任务的内存。为了进一步帮助平衡系统内存并避免系统需要终止您的应用程序,可以在Activity
类中实现ComponentCallback2
接口。提供的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 was raised.
*/
override fun onTrimMemory(level: Int) {
// Determine which lifecycle or system event was raised.
when (level) {
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
/*
Release any UI objects that currently hold memory.
The user interface has moved to the background.
*/
}
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
/*
Release any memory that 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 will
begin killing 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 will be one of
the first to be terminated.
*/
}
else -> {
/*
Release any non-critical data structures.
The app received 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 was raised.
*/
public void onTrimMemory(int level) {
// Determine which lifecycle or system event was raised.
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
/*
Release any UI objects that currently hold memory.
The user interface has moved 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 that 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 will
begin killing 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 will be one of
the first to be terminated.
*/
break;
default:
/*
Release any non-critical data structures.
The app received an unrecognized memory level value
from the system. Treat this as a generic low-memory message.
*/
break;
}
}
}
onTrimMemory()
方法在Android 4.0(API level 14) 中被添加。对于更早的版本,你可以使用onLowMemory()
, 这个基本等同于TRIM_MEMORY_COMPLETE
事件。
检查你应该使用的内存量
为了允许多个正在运行的进程,Android对分配给每个应用程序的堆大小设置了硬限制。确切的堆大小限制在设备之间会有所不同,具体取决于设备总体上有多少可用RAM。如果您的应用程序已经达到堆容量并尝试分配更多的内存,则系统将引发OutofMemoryError
。
为了避免内存不足,您可以查询系统以确定当前设备上有多少堆空间。您可以通过调用getMemoryInfo()
在系统中查询该数据。这将返回一个ActivityManager.MemoryInfo
对象,该对象提供有关设备当前内存状态的信息,包括可用内存,总内存和内存阈值(系统开始杀死进程的内存级别)。ActivityManager.MemoryInfo
对象还提供一个简单的布尔变量lowMemory
,它告诉您对象是否内存不足。以下的代码显示了如何在应用程序中使用getMemoryInfo()
方法的示例。
KOTLIN
fun doSomethingMemoryIntensive() {
// Before doing something that requires a lot of memory,
// check to see 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 to see 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缓存中的缓存进程的数量,从而降低了应用程序切换的效率。当内存不足并且系统无法维护足够的进程来承载正在运行的所有服务时,甚至可能导致系统崩溃。
通常,应避免使用持久性服务,因为它们对可用内存的持续需求。相反,我们建议你使用类似于JobScheduler
这些替代来实现。有关如何使用JobScheduler
安排后台进程的更多信息,请参阅后台优化。
如果你必须使用服务,则限制服务寿命的最佳方法是使用IntentService
,该服务会在处理启动它的意图后立即完成。有关更多信息,请阅读在后台服务中运行。
使用优化的数据容器
编程语言提供的某些类未针对在移动设备上进行优化。例如,通用的HashMap
实现可能在内存方面效率很低,因为他需要为每个映射使用单独的入口对象。
Android框架包括几个优化的数据容器,包括SparseArray
,SparseBooleanArray
和LoingSparseArray
。例如,SparseArray
类效率更高,因为他们避免了系统自动对键(有时设置对值)进行装箱(每个条目又创建一个或两个对象)的需求。
如有必要,您始终可以切换到原始阵列以获得真正精简的数据结构。
谨慎对待代码抽象
开发人员通常只是将抽象用作良好的编程习惯,因为抽象可以提高代码的灵活性和维护性。 但是,抽象要付出巨大的代价:通常,抽象需要大量的代码才能执行,需要更多的时间和更多的RAM才能将该代码映射到内存中。 因此,如果您的抽象不能带来很大的好处,则应避免使用它们。
对于序列化数据使用轻度protobuf
Protocol Buffer是Google设计的一种与语言无关,与平台无关的可拓展机制,用于序列化结构化数据(类似于XML,但更小,更快,更简单)。如果决定对数据使用protobuf,则应始终在客户端代码中使用lite protobuf。常规的protobuf会生成极其冗长的代码,这可能会在您的应用程序中引起许多问题,例如增加的RAM使用量,显著的APK大小增加以及执行速度降低。
有关更多信息,可以参加protobuf readme 的 “Lite Version”部分。
避免内存混乱
如前所述,垃圾回收事件通常不会影响您应用的性能。但是,在很短时间内发生多次垃圾回收会耗尽帧时间。系统花费在垃圾回收上的时间越长,执行诸如渲染或流音频之类的其他事情所花费的时间就越少。
通常,内存流失会导致发生大量垃圾回收事件。实际上,内存流失描述了在给定的时间内发生的已分配临时对象的数量。
例如,您可以在for循环中分配多个临时对象。或者,您可以在视图的onDraw()
函数创建新的Paint
或Bitmap
对象。在这两种情况下,应用程序都会快速大量创建大量对象。这些可以迅速消耗年轻一代中的所有可用内存,从而迫使发生垃圾回收事件。
当然,您需要在代码中找到内存流失率较高的地方,然后才能对其进行修复。为此,您应该在Android Studio中使用Memory Profiler。
在代码中确定问题区域后,将尝试减少性能关键区域内的分配数量。考虑将这些操作移出内层的循环,或者将其移入基于工厂的分配结构中。
删除占用大量内存的资源和库
代码中的一些资源或者库可能会在您不知情的情况下吞噬内存。APK的总体大小(包括第三方库或嵌入式资源)可能会影响您的应用消耗的内存量。通过从代码中删除任何多余的,不必要的或者臃肿的组件,资源或库,可以提高应用程序的内存消耗。
减少整体APK大小
通过减小应用程序的整体大小,可以大大减少应用程序的内存使用量。 位图的大小,资源,动画框架和第三方库都可以影响APK的大小。 Android Studio和Android SDK提供了多种工具来帮助您减少资源和外部依赖项的大小。 这些工具支持现代的代码缩减方法,例如R8编译。 (Android Studio 3.3及更低版本使用ProGuard而不是R8编译。)
有关如何减小APK总体大小的详细信息,请参阅有关减小应用程序大小的指南。
使用Dagger 2 进行依赖注入
依赖项注入框架可以简化您编写的代码,并提供适用于测试和其他配置更改的自适应环境。
如果您打算在应用程序中使用依赖项注入框架,请考虑使用Dagger 2。Dagger不使用反射来扫描应用程序的代码。 Dagger的静态,编译时实现意味着它可以在Android应用中使用,而无需不必要的运行时成本或内存使用。
其他使用反射的依赖项注入框架倾向于通过扫描代码中的注释来初始化进程。 此过程可能需要更多的CPU周期和RAM,并且可能会在应用启动时引起明显的延迟。
谨慎使用外部库
外部库代码通常不是为移动环境编写的,并且在移动客户端上使用时效率低下。当您决定使用外部库时,可能需要针对移动设备优化该库。在决定完全使用它之前,请先计划该工作,并根据代码大小和RAM占用空间分析该库。
甚至某些针对移动设备进行优化的库也可能由于实现方式不同而引起问题。例如,一个库可能使用精简协议,而另一个库则使用微型协议,从而在您的应用程序中实现两种不同的协议实现。日志记录,分析,图像加载框架,缓存以及许多您不期望的其他事情的不同实现可能会发生这种情况。
尽管ProGuard可以使用正确的标志来帮助删除API和资源,但是它不能删除库的大型内部依赖项。这些库中所需的功能可能需要较低级别的依赖性。当您使用库中的Activity
子类时(这往往会具有广泛的依赖关系),而库中使用反射时(这很常见,这意味着您需要花费大量时间来手动调整ProGuard才能使它到达),这尤其成问题工作),等等。
还应避免将共享库仅用于数十种功能中的一项或两项。您不想引入甚至不使用的大量代码和开销。在考虑是否使用库时,请寻找与您的需求完全匹配的实现。否则,您可能决定创建自己的实现。