原文链接
https://developer.android.com/topic/performance/memory-overview
https://developer.android.com/topic/performance/memory
一. 内存管理概述
Android运行时(ART)和Dalvik虚拟机使用分页和内存映射(映射)来管理内存。这意味着应用程序的任何内存操作(无论是分配新对象还是访问mmap页面)都会驻留在RAM中,无法换页。从应用程序释放内存的唯一方法是释放应用程序持有的对象引用,让内存可供垃圾收集器使用。 有个例外:如果系统想要在其他地方使用该内存,则可以将任何未经修改而映射到的文件(如代码)从RAM中换页。
二. 管理应用程序进程和内存分配。
1. 垃圾收集
托管内存环境(如ART或Dalvik虚拟机)会跟踪每次内存的分配情况。一旦它确定一段内存不再被程序使用,就会将它释放回堆中,这个过程不需要程序员的任何干预。回收托管内存环境中未使用内存的机制称为垃圾回收。垃圾收集有两个目标:在程序中找出之后不会被访问的数据对象并回收这些对象使用的资源。
Android的内存堆是属于分代堆,这意味着会有不同的内存分配区块,每个分区会都会根据对象的生命周期和对象大小来对其追踪。例如,最近分配的对象属于年轻代。当一个对象活跃很久后,它可能被提升到老年代,然后是永久代。
堆的每个分区都会有自己的对象可用内存上限。任何时候分区满了,系统会执行一个垃圾收集事件以释放内存。垃圾收集的持续时间取决于它正在收集哪一代对象以及每代有多少活动对象。
尽管垃圾回收速度可能相当快,但仍然会影响应用程序的性能。通常你无法在代码中控制产生垃圾回收事件的时间。垃圾收集器会有自己的一套标准,用于确定执行垃圾回收的时机。当条件满足时,系统停止执行进程并开始执行垃圾收集操作。如果在密集处理操作中(如执行动画或播放音乐)发生垃圾收集,则会拖慢进程执行。这可能会使你App的帧渲染时间超过能高效流畅显示的16ms建议阈值。
除此之外,代码中的不当操作也可能会导致垃圾收集操作频繁或执行时间长。例如,在alpha混合动画的每个帧周期内你在for循环中创建了多个对象,那么就会有大量对象占用内存堆区。这时候垃圾收集器会执行多次垃圾回收操作,从而降低应用程序的性能。
2. 共享内存
为了满足内存使用需求,Android会尝试跨进程共享RAM页面。它可以通过以下方式进行:
每一个App 进程都是从现有的Zygote进程fork出来的。当系统启动并加载公共框架代码和资源(例如activity主题)时,Zygote进程启动。为了启动一个新的应用进程,系统通过fork Zygote进程来创建新的进程来加载执行应用程序代码。这样有个好处,就是系统分配给framework层代码和资源的大部分内存页能被所有进程共享。
大多数静态数据被mmap 映射到同一进程中。这种技术允许数据在进程间共享,又可以在需要时被换页出去。静态数据包括:Dalvik代码(通过将其放置在预连接的.odex文件进行直接的内存映射),应用资源(将resource table设计成可以内存映射的数据结构,并对APK进行对齐处理),及.so文件中的本地代码。
在很多方面,Android通过使用显示分配的共享内存区域实现进程间共享同样的动态RAM(例如通过匿名共享内存Anonymous shared memory或者gralloc)。例如,window surface 在应用程序和屏幕合成器进行交互时使用共享内存,cursor buffers (游标缓存)使用content provider(内容提供者)和客户端间使用共享内存。
由于广泛使用了共享内存,所以计算应用内存占用时需要谨慎一些。调查你的RAM使用情况中讨论了正确计算应用程序内存使用情况的方法。
3. 分配和回收应用内存
Dalvik堆被限制为每个应用程序进程的单个虚拟内存范围。
每个应用程序进程的Dalvik堆都被限制在一个单一虚拟内存范围内。
这定义了堆的逻辑大小,可以根据需要增长,但只能达到系统赋给的上限。
堆的逻辑大小与堆占用的物理大小不同。 在检查应用程序的堆时,Android会计算出一个叫比例集大小(PSS)的值,包含了被其他进程共享的脏页和干净页 -但是大小是按比例分担计算的各进程共享内存得到的值。 这个(PSS)总数是系统认为是你的物理内存占用量。 关于PSS的更多信息,请参阅调查你的RAM使用情况。
Dalvik堆并不压缩堆的逻辑大小,这意味着Android不会整理堆以释放空间。Android只会在堆的末尾出现未使用的空间时才会去收缩逻辑堆大小。 但是,系统仍然可以减少堆占用的物理内存。 在垃圾收集之后,Dalvik会遍历堆并找到未使用的页面,然后使用madvise操作将这些页面返回给内核。 因此,大块成对分配和释放会回收所有(或几乎全部)使用的物理内存。然而,从较小规模的分配中回收内存的效率可能会低得多,因为用于小分配的页仍然可以被引用和共享,尚未被释放。
4. 限制应用程序内存
为了维持多任务的工作环境,Android为每一个应用程序堆大小做了硬性限制。具体值跟设备的实际物理内存有关。假如你的应用程序已经达到到堆容量上限还要尝试分配更多内存,就会返回一个OutOfMemoryError
内存溢出错误。
很多时候,你想获知当前设备上可用堆的大小。比如,你要确定缓存中可以保存多少数据是安全的。你可以调用getMemoryClass()
方法来向系统查询这个值。这个方法返回一个数字,表示你的应用程序有多少兆可用字节的堆内存。
切换应用程序
当用户在应用程序间进行切换时,Android会将那些不在前台的应用程序(对于用户不可见或运行的是前台服务比如播放音乐)保存在最近使用(LRU)的缓存中。当用户第一次启动一个应用的时候,系统为它创建了一个进程,但是当用户离开这个应用的时候,这个进程并没有被退出。系统会缓存这个进程。假如用户之后又回到这个应用程序,系统恢复这个进程,使得切换应用程序更快。
如果你的应用程序拥有缓存进程并保留了一些当前用不到的内存,那么即使用户没有使用应用程序,它也会影响系统的整体性能。当在低内存状态运行的时候,系统就会杀死那些在LRU缓存最近很少使用的进程。系统还会考虑杀死那些最占内存的进程来释放RAM。
注意:系统开始杀死LRU缓存中的进程时,主要按照从下到上的顺序执行。系统也会考虑杀死那些占用很多内存并且杀死它系统可以获取更多内存空间的进程。在LRU列表中占用的内存越少,在LRU列表中保留下来的机会就越大,也能可更快的恢复。
更多关于进程没有在前台运行,如果保存在缓存中,并且Android设备如何选择哪一个进程去杀死,请看进程和线程导读。
二. 管理应用程序的内存
随机存取存储器(RAM)在任何软件开发环境中都是宝贵的资源,对于物理内存有限的移动操作系统来说更是这样。尽管Android运行时(ART)和Dalvik虚拟机都会执行垃圾收集,但这并不意味着你可以忽略应用是如何分配和释放内存的。你仍然需要避免内存泄漏(内存泄露通常由静态成员变量持有引用引起)并需要在合适的生命周期回调方法中释放任何引用对象。
下面说以下如何优化应用程序中的内存占用。
1. 监视可用内存和内存使用情况
在修复应用程序中的内存使用问题之前,你要先定位出问题。
Android Studio中的Memory Profiler工具可以帮助你通过以下方式查找和诊断内存问题:
- 查看你的应用程序随着时间的推移是如何分配内存的。 Memory Profiler显示一个实时图,显示你的应用程序使用多少内存,分配Java对象的数量以及发生垃圾回收的时机。
- 启动垃圾收集事件并在应用运行时显示Java堆的快照。
- 记录应用程序的内存分配,然后检查所有分配的对象,查看分配堆栈,并能跳转到Android Studio编辑器中的相应代码行。
响应事件释放内存
如Android内存管理概述中所述,Android可以通过多种方式回收应用程序内存,或者在执行高优先级关键任务时完全杀掉应用程序来释放内存。为了平衡系统内存利用并避免系统杀掉应用程序进程,你可以在Activity
类中实现ComponentCallbacks2
接口。其中的onTrimMemory()
回调方法能够让你的应用程序处于前台或后台时监听到与内存相关的事件,然后你可以在此时释放对象以响应应用程序生命周期或指示系统需要回收内存的系统事件。
例如,您可以实现onTrimMemory()
回调来响应不同的内存相关事件,如下所示:
import android.content.ComponentCallbacks2;
// 其他声明 ...
public class MainActivity extends AppCompatActivity
implements ComponentCallbacks2 {
// 其他代码...
/**
*
* 当UI不可见或者系统资源紧张时释放内存
* @param level 内存相关的事件等级值
*/
public void onTrimMemory(int level) {
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
//释放任何当前持有内存的UI对象,此时用户界面已经在后台了
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
//释放任何你应用运行不需要的内存,这个时候 你的设备处于低内存状态运行,不用的等级标识了内存事件的严重性,如果产生了RIM_MEMORY_RUNNING_CRITICAL事件,系统将开始杀死后台进程
break;
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
// 释放的尽可能多的内存
触发了这些事件,表明该应用程序位于LRU列表中,系统内存不足。
不同的事件标识了应用程序在LRU列表中的位置。
如果事件是TRIM_MEMORY_COMPLETE,则该进程将是在LRU列表中第一个被终止的进程。
break;
default:
//释放任何非必要数据,app收到了一个系统发出的未识别的内存等级值, 将其视为通用的低内存消息。
break;
}
}
}
onTrimMemory()回调在Android 4.0(API级别14)中加入。对于较早的版本,可以使用onLowMemory(),它大致等同于TRIM_MEMORY_COMPLETE
事件。
检查你应该使用多少内存
为了让多进程能并发执行,Android对每个应用程序可用的最大堆容量做了限制。不同设备堆容量限制依据内存大小不同而不同。如果你的应用程序已达到最大堆容量并尝试分配更多内存,则系统会抛出OutOfMemoryError。
为避免内存不足,您可以查询系统来确定当前设备上有多少可用堆空间。你可以通过调用getMemoryInfo()
来查询系统的这个数字。这个函数会返回一个ActivityManager.MemoryInfo
对象,该对象能够提供设备当前内存状态的信息,包括可用内存,总内存和内存阈值——系统开始终止进程的内存级别。 ActivityManager.MemoryInfo对象还提供了一个简单的布尔值lowMemory,告诉你设备当前是否在低内存状态运行。
以下代码片段展示了如何在应用程序中使用getMemoryInfo()方法
public void doSomethingMemoryIntensive() {
// 在执行耗内存操作前检查设备是否处于低内存状态 ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
if (!memoryInfo.lowMemory) {
// Do memory intensive work ...
//执行内存密集型操作
}
}
//获取 一个标识设备当前内存状态的MemoryInfo 对象
private ActivityManager.MemoryInfo getAvailableMemory() {
ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
return memoryInfo;
}
使用更节省内存的代码和操作
一些Android功能,Java 类和代码使用不当会消耗更多的内存。你可以在选择更高效的方案来最大限度的降低内存使用量。
谨慎使用服务
在不需要服务时保持服务运行是内存管理中程序可能犯的最严重的错误之一。如果你的应用程序需要一个服务来在后台执行工作,除非它必须要执行任务,否则不要保持它一直运行。务必在完成任务时停止服务,否则会不经意间引发内存泄漏。
启动一个服务时,系统会倾向于始终保持该服务的运行进程。这会使得服务进程运行代价高昂,因为服务使用的RAM对其他进程仍然不可用。这会减少系统可以保留在LRU缓存列表中的缓存进程数量,从而降低了应用程序切换的效率。当内存很紧并且系统无法维护足够的进程来托管当前正在运行的所有服务时,甚至会导致系统发生抖动。
通常情况下,尽量不要使用持久服务,因为他们会持续消耗内存。建议使用诸如JobScheduler之类的来替代。有关如何使用JobScheduler执行后台任务的更多信息,请参阅后台优化。
如果您必须使用服务,那么限制服务生命周期的最好方式是使用IntentService,一旦处理完Intent后,IntentService就会自动停止运行。详细信息请参考在后台服务中运行。
使用优化过的数据容器
编程语言提供的某些类未针对移动设备进行优化。 例如,通用的HashMap实现是相当耗内存的,因为每个映射都需要一个单独的Entry对象。
Android框架提供了几个优化的数据容器,包括SparseArray,SparseBooleanArray和LongSparseArray。 例如,SparseArray类更加高效,因为它避免了key或是value的自动装箱(每个entry会多创建一个或两个对象)。
如有必要,你可以使用原始数组来获得更加精简的数据结构。
谨慎使用代码抽象
开发人员经常会把代码抽象看作一种很好的编程实践,因为抽象可以提高代码的灵活性和可维护性。 然而,抽象成本很高:通常他们需要执行更多的代码,需要更多的时间和更多的RAM来将代码映射到内存中。 所以如果你的抽象没有什么显著的好处,就应该避免它们。
使用nano protobufs进行数据序列化
Protocol buffers是Google 设计的语言无关,平台无关的一种轻便高效的序列化结构数据存储格式,类似于XML,但更小,更快,更简单。 如果你决定在项目中使用protobufs,你应该在的移动端代码中使用nano protobufs版本。 因为普通的protobufs会生成非常冗长的代码,这可能会导致应用程序出现多种问题,例如RAM使用量增加,APK体积增加或执行速度变慢。
有关更多信息,请参阅protobuf描述中的“Nano版本”部分。
避免内存抖动
如前所述,垃圾收集事件通常不会影响你应用程序的性能。但是,短时间内产生多次垃圾收集事件可能会很快耗尽你的帧时间。系统花费在垃圾收集上的时间越多,执行其他操作比如渲染或者处理音频的时间就越少。
内存抖动通常会导致大量垃圾收集事件发生。在实践中,内存抖动描述了在给定的时间内分配的临时对象的数量。
例如,你在for循环中分配了多个临时对象。或者,你在视图的onDraw()函数内创建新的Paint或Bitmap对象。在这两种情况下,应用程序都会快速创建大量对象。这会迅速消耗年轻代中的所有可用内存,导致产生垃圾回收。
当然,你需要在代码中找到内存抖动厉害的地方,然后才能修复它们。可以在Android Studio中使用Memory Profiler来定位问题。
一旦确定了代码中的问题区域,就要想办法减少关键部分的的对象创建次数。可以将对象创建移到循环外,或者采用工厂模式来创建对象。
移除占内存的资源和库
代码中的一些资源和库可能会在你不知道它的情况下吞噬内存。APK的整体大小(包括第三方库或内嵌的资源)可能会影响应用消耗的内存量。可以通过删除代码中多余臃肿的组件,资源或库来改善应用程序的内存消耗。
减少APK包大小
减少应用程序的整体大小可以显著减少应用程序的内存使用量。Bitmap尺寸,资源,动画帧和第三方库都可以影响APK的大小。 Android Studio和Android SDK提供了多种工具来帮助你减少资源和外部依赖的大小。
关于如何减少整体APK大小的更多信息,请参阅减少APK包大小。
使用Dagger 2进行依赖注入
依赖注入框架可以简化你的代码,并提供适用于测试和其他配置更改的自适应环境。
如果你打算在应用中引入依赖注入框架,请考虑使用Dagger2. Dagger不使用反射来扫描应用代码。 Dagger的静态编译时实现意味着它可以在Android应用程序中使用,而无需运行成本或占用内存。
其他使用反射的依赖注入框架执行初始化时需要用反射扫描代码中的注解。这个过程可能需要更多的CPU周期和RAM,并且在应用程序启动时可能会导致明显的延迟。
谨慎使用三方库
外部库代码通常不是为移动环境编写的,在用于移动客户端时可能效率低下。当决定使用外部库时,你需要为移动设备优化该库,提前针对库的代码量和内存使用情况进行分析。
即使一些针对移动端做过优化的库也可能由于不同的实现而产生问题。例如,一个库可能使用nano protobufs,而另一个库使用 micro protobufs,导致在应用中出现了两种不同的protobuf实现。这可能会造成日志记录,分析,图像加载框架,缓存以及许多其他无法预期的模块的不同实现。
虽然ProGuard可以通过正确的标记来帮助你去除无用的API和资源,但它不能删除库的庞大内部依赖关系。库中你需要的功能可能只有较低级别的依赖项。假如库中使用了反射(这很常见,意味着你需要花费大量时间手动调整ProGuard的设置),而你又使用了库中一个Activity的子类时(这往往会产生大量的依赖关系),这就很有问题了,可能会造成找不到实现class 造成应用崩溃。
还要避免只使用库中多个功能中一个或两个。你也肯定不希望引入大量无用的代码。当你准备使用三方库,尽量找那些与你需求高度匹配的,否则就自己写一个吧。