随机存取存储器(RAM)在任何软件开发环境中都是宝贵的资源,但是在物理内存常常受到限制的移动操作系统中变得更加珍贵。尽管Android运行时(ART)和Dalvik虚拟机都会例行地执行垃圾回收,但这不意味着应用可以不顾时间和位置地分配和释放内存。仍然要避免引入内存泄漏,通常的原因是static变量持有一个对象的引用。并且在恰当的时机(如定义好的生命周期回调方法)所有的释放所有Reference
对象。
本文讲解释如何主动地减少应用使用的内存。关于Android系统如何管理内存的信息,请查看Android内存管理概述。
监控可用内存 和 内存使用量
在修复应用内存使用量的问题之前,需要先找到它们。Android Studio 的Memory Profiler
(内存分析器)可以通过以下方法帮助你发现且诊断内存问题:
- 查看应用在一段时间内如何分配内存。内存分析器会提供一个实时图像,图像里包括应用使用的内存,分配的java对象数,以及垃圾回收触发的时间。
- 发起一次垃圾回收事件,并且抓取一张应用运行时的Java堆的快照。
- 记录应用的内存分配然后检查所有分配的对象,查看每一次分配的调用栈,在Android Studio的编辑器中跳转到相关的代码中。
在响应事件中释放内存
正如Android内存管理概述一文所述,Android有很多种办法可以回收应用的内存,如果必要的话,也会终止整个应用进程来释放资源。为了进一步帮助平衡系统内存,避免系统需要终止应用进程的情况发生,应用可以在Activity
中实现ComponentCallback2
接口。该接口中提供了onTrimMemory()
回调方法,这个方法允许应用监听内存相关的事件,不论应用是在前台还是后台,并且通过释放对象来响应表示系统需要回收内存的事件,这些事件包括应用生命周期的事件和系统事件。
例如下面的代码,应用可以实现onTrimMemory()
接口回调来响应不同的内存相关的事件,
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.
释放当前占用内存你的UI对象,当前UI已经移到后台。
*/
}
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.
释放所有应用不需要的内存
设备处于低内存状态,这时应用还在运行。
该事件突出了内存相关事件的严重程度。
如果当前事件是`TRIM_MEMORY_RUNNING_CRITICAL`,那么系统将开始终止后台进程
*/
}
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.
尽可能地释放进程的内存。
应用处于`LRU`列表中,且系统处于低内存状态。该事件指明了当前应用处于`LRU`列表中。
如果事件是`TRIM_MEMORY_COMPLETE`,应用进程将第一个被终止。
*/
}
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.
释放非必要数据结构
应用收到了一个未识别的系统内存事件,把该是当做通常的低内存信息来处理。
*/
}
}
}
}
onTrimMemory()
回调方法在Android4.0(API level 14)被添加。更早期的系统版本,可以使用onLowMemory()
,这个回调方法几乎等同于TRIM_MEMORY_COMPLETE
事件。
查看应用该使用多少内存
为了允许多个进程同时运行,Android对每个应用进程堆内存的大小设置了硬性的限制。这个限制的具体值会根据设备总体可用RAM的不同而变化。如果应用到达了对内存容量的极限,这时尝试分配更多的内存,系统会抛出OutOfMemory
错误。
为了避免运行时超过内存限制,应用可以查询系统来获知当前设备应用可用的最大堆内存空间。应用可以通过getMemoryInfo()
方法向系统查询该值。该方法会返回一个ActivityManager.MemoryInfo
对象,这个对象提供了关于设备当前内存状态的信息,包括可用的内存,总内存以及触发系统终止进程的内存阈值。ActivityManager.MemoryInfo
对象还会提供一个简单的lowMemory
布尔值,它表示当前设备是否内存不足。
以下的代码片段是一个如何使用getMemoryInfo()
的例子:
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.
//获取一个代表了当前内存状态的`MemoryInfo`对象
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return ActivityManager.MemoryInfo().also { memoryInfo ->
activityManager.getMemoryInfo(memoryInfo)
}
}
使用内存效率更高的代码结构
一些Android特性,Java类和代码结构会使用更多的内存。应用代码可以选择更高效的替代方案来缩减占用的内存。
谨慎使用Service
让不需要的Service
持续运行,这是Android内存管理中犯下的最严重的错误之一。如果应用需要一个Service
以便在后台执行任务,除非真实需要执行任务,否则请它停止运行。请注意在Service
完成工作后使其停止运行。否则,您可能无意中造成了一起内存泄漏。
当应用启动一个Service
,系统会尽可能让它持续运行。由于被Service
使用的内存不再会被其他内存使用,这种策略导致Service
进程是成本很高。这降低了系统可以在LRU
缓存中维护的进程数量,从而降低应用切换效率。当内存紧张,系统无法维护足够的进程来托管当前运行的所有服务时,甚至会导致系统出现内存抖动。
因为对于内存的持续性需求,通常要避免使用持续性的Service
。我们推荐您使用JobScheduler
等替代实现方案。关于如何使用JobScheduler
来调度后台进程的更多信息,请查看后台优化。
如果应用必须使用Service
,限制生命周期的最佳方案是使用IntentService
,它会在完成启动时的意图后就停止运行。更多信息请查看在后台服务中运行。
使用优化过的数据容器
编程语言提供的一些类没有针对移动设备进行优化。例如,常规的HashMap
实现内存效率很低,因为每一个映射都需要对应一个单独的实体对象。
Android狂阿基包含了多个优化过的数据容器,包括SparseArray
,SparseBooleanArray
,和LongSparseArray
.例如,SparseArray
这个类是更高效的,因为它避免了系统需要对键(有时候值也需要)自动装箱的操作(这个操作会为每个实体创建额外的一个或两个对象)。
如果必须,您可以随时切换回原始数组已获得非常精简的数据结构。
谨慎对待代码抽象
开发者会经常把抽象作为一个好的编程实践,因为抽象可以提高代码的灵活性和可维护性。但是,抽象带来一个显著的成本:它需要更多的代码才能执行,这需要更多的时间和RAM(空间)把代码映射到内存。所以如果你的抽象没有带来显著的好处,应该避免它们。
数据的序列化使用精简版的Protobuf
Protocol buffers
是一种跨语言、跨平台、可扩展的机制,Google研发它用于序列化结构性数据,与XML
类似,但是更小,更快,更简单。如果应用决定使用ProtoBufs
来序列化数据,您应该在客户端上始终使用精简版的ProtoBufs
。常规版本的ProtoBufs
会生成非常冗长的代码,这会导致应用出现很多问题,如增加了RAM使用,显著增大了APK的尺寸,执行速度变慢。
关于精简版本ProtoBufs
的更多信息,请查看ProtoBufs
自述文件。
避免内存抖动
正如之前提到的,垃圾回收时间通常不会影响应用的性能。但是,短时间内触发多次垃圾回收事件可以消耗掉一帧的绘制时间。系统用在垃圾回收的时间越多,它可以用在绘制或传输音频上的时间就越少。
通常,"内存抖动"会触发大量的垃圾回收事件。实际上,内存抖动描述了给定时间内分配临时对象的数量。
例如,您可能在for
循环中分配了多个对象。或者在View的onDraw()
方法中创建了新的Paint
或Bitmap
对象。在这两种情况下,应用都会快速创建大量的对象。这会导致快速消耗新生代中所有的内存,强制触发一个垃圾回收事件。
当然,在修复内存抖动之前,你需要先在代码中找到它们。为此,您应该使用Android Studio提供的Memory Profiler
。
确定了代码中的问题区域后,尝试减少在性能的关键区域分配内存的数量。可以考虑把相关点从最内存的循环中移出,或许可以使用一个基于工厂模式的对象创建结构中。
移除占用大量内存的资源和依赖库
一些资源和库会在你不知情的情况下吃掉大量内存。增大apk的总体大小,包括第三方的库或内嵌的资源可以影响应用消耗内存的大小。您可以提优化应用的内存消耗,通过在代码中移除所有重复的,不需要的或者臃肿的模块。
缩减总体apk的大小
缩减apk的总体大小可以显著地缩减应用的内存使用。位图大小,资源,动画帧和第三方库都会增加apk的大小。Android Studio 和 Android SDK提供了很多工具,这些工具可以帮助您减小资源和外部库的体积。这些工具支持现代的代码裁剪方法,例如R8
编译。(Android Studio 3.3之前的版本使用Proguard
代替R8编译)
关于缩减整体apk大小的更多信息,请查看如何缩减应用大小。
依赖注入使用Dagger2
依赖注入狂阿基可以简化代码的编写,并且提供了一个可用于测试和配饰更改的自适应环境。
如果您打算在应用中使用依赖注入框架,请考虑使用Dagger2
。Dagger
并没有使用反射来扫描代码。Dagger
的静态,编译时实现方案意味着它在Android应用中不会增加运行时的成本和内存使用。
其他由反射实现的依赖注入框架倾向于扫描代码中的注解来初始化进程。这个进程会要求更多的CPU时间片和RAM,并且会导致一个应用启动明显的延迟。
谨慎使用外部库
外部库的代码通常没有针对移动环境优化,用于移动客户端可能很低效。当您决定使用一个外部库时,您可能需要针对移动环境优化该库。在决定使用这个库之前,要提前规划,并且分析这个库在代码大小和内存消耗方面的表现。
即使是针对移动场景做过优化的库,也可能因为实现方式的不同而导致问题。例如,一个库使用了精简版的ProtoBufs
,当另一个库使用了Micro ProtoBufs
,导致应用内实现了两种ProtoBufs
。在日志记录,统计工具,图片加载框架,缓存以及很多想象不到的方面,都可能发生这种多个实现的问题。
尽管Proguard
可以通过恰当的标签来移除API和资源,但是不能移除一个库的大型内部依赖项。在用这些库的时候,您需要的功能可能需要低级别的依赖项。下面的情况中,这会造成显著的问题:当您从一个库中使用一个Activity
子类(往往需要大量的依赖项)时这个库使用了反射(这很常见,也意味着您需要花很多时间手动调整Proguard
规则来让它正常工作)等等。
另外,避免使用只用到一两个功能的共享库,但是它提供了十几个功能。您一定不希望引入大量的用不到的代码。当您考虑是否使用一个库时,请寻找与你需求最匹配的实现。否则,您可以考虑创建自己的实现。